进程通信是指在进程间传输数据(交换信息)
管道
System V IPC
POSIX IPC
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“
例:统计云服务器上登录的用户个数
who和wc运行起来后变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据。
who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数。
作用: 创建匿名管道
函数原型:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
pipefd[0]表示管道读端的文件描述符
pipefd[1]表示管道写端的文件描述符
pipe函数调用成功时返回0,调用失败时返回-1。
2、父进程创建子进程。
站在文件描述符角度-深度理解管道
代码演示
#include
#include
#include
#include
#include
int main()
{
int pipe_fd[2]={0};
if(pipe(pipe_fd)<0){//使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id=fork();
if(id<0){
perror("fork");
return 2;
}
else if(id==0){ // write24 // child
close(pipe_fd[0]); // 子进程关闭读端
const char *msg="hello parent,I am child\n";
int count=5;
while(count)
{
//子进程向管道写入数据
write(pipe_fd[1],msg,strlen(msg));
sleep(1);
count--;
}
close(pipe_fd[1]);//子进程写入完毕,关闭文件
exit(0);
}
else{ // read41
// parent
close(pipe_fd[1]); //父进程关闭写端
char buffer[64];
//父进程从管道读取数据
while(1){
buffer[0]=0;
ssize_t size=read(pipe_fd[0],buffer,sizeof(buffer)-1);
if(size > 0){
buffer[size]=0;
printf("parent get message from child# %s",buffer);
}
else if(size==0){
printf("pipe file close , child quit!\n");
break;
}
else{
break;
}
}
int status=0;
if(waitpid(id,&status,0)>0){
printf("child quit,wait success!\n");
}
close(pipe_fd[0]);
}
return 0;
}
运行结果:
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务: 数据有明确的分割,拿数据按报文段拿。
同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
单工通信(Simplex Communication): 单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
半双工通信(Half Duplex): 半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
全双工通信(Full Duplex): 全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。
我们可以通过以下代码看看第4种情况中写段收到了何种信号
#include
#include
#include
#include
#include
#include
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0){ //创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork();
if (id == 0){
//child
close(fd[0]); //子进程关闭读端
//子进程向管道写入数据
const char* msg = "hello father, I am child...";
int count = 10;
while (count--){
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]); //关闭写端
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
int status = 0;
waitpid(id, &status, 0);
printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
return 0;
}
我们可以通过 kill -l 命令查看信号含义
管道的容量时有限的,那么管道究竟能同时存储多少数据呢?
方法一:
使用ulimit -a命令,查看当前资源限制的设定。
根据显示,管道的最大容量是 512 × 8 = 4096 字节。
方法二:
可以向管道中不断写数据,看最多能写多少数据
#include
#include
#include
#include
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0){ //创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork();
if (id == 0){
//child
close(fd[0]); //子进程关闭读端
char c = 'a';
int count = 0;
//子进程一直进行写入,一次写入一个字节
while (1){
write(fd[1], &c, 1);
count++;
printf("%d\n", count); //打印当前写入的字节数
}
close(fd[1]);
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
waitpid(id, NULL, 0);
close(fd[0]);
return 0;
}
不同操作系统的管道大小不同,一切以实际为准。
注意:
我们可以使用 mkfifo 命令创建一个命名管道。
我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉。我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。
在进程中创建命名管道需要用到mkfifo函数
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname
mode
创建命名管道文件的默认权限。
返回值:
创建成功,返回0,创建失败,返回-1。
我们先写一个服务端(server)和客户端(client)程序
服务端代码
#include
#include
#include
#include
#include
#define FIFO "./fifo"
int main()
{
int ret=mkfifo(FIFO,0644); // 创建命名管道文件
if(ret<0){
perror("mkfilo");
return 1;
}
int fd=open(FIFO,O_RDONLY);//以读的方式打开命名管道文件
if(fd<0){
perror("open");
return 2;
}
char buffer[128];
while(1){
buffer[0]=0;
//从命名管道当中读取信息到buffer中
ssize_t s=read(fd,buffer,sizeof(buffer)-1);
if(s>0){
buffer[s]=0;
printf("client# %s\n",buffer);//输出客户端发来的信息
}
else if(s==0){
printf("client quit...\n");
break;
}
else{
break;
}
}
close(fd);//关闭命名管道文件
return 0;
}
客户端代码
#include
#include
#include
#include
#include
#include
#define FIFO "./fifo"9
int main()
{
int fd=open(FIFO,O_WRONLY);
if(fd<0){
perror("open");
return 2;
}
char buffer[128];
while(1){
printf("Please Enter# ");
fflush(stdout);
buffer[0]=0;
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0){
buffer[s]=0;
write(fd,buffer,strlen(buffer));
}
else if(s==0){
printf("client quit...\n");
break;
}
else{
break;
}
}
return 0;
}
当服务端和客户端代码运行起来后,我们再客户端输入信息再服务端能够接收到。
system V IPC提供的通信方式有以下三种:
system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥。
共享内存为了让不同进程看到同一份资源,在物理内存中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射。在进程虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应映射关系,这样不同进程就看到了同一份物理内存,即共享内存。
在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
功能: 创建共享内存
函数原型:
int shmget(key_t key, size_t size, int shmflg);
返回值:
参数说明:
注意: 系统分配共享内存是按4KB整数倍分配的,如果创建共享内存的大小不为4KB整数倍会造成空间浪费。
注意: 传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取
ftok函数原型
key_t ftok(const char *pathname, int proj_id);
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。
shmflg
传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:
shmget函数的使用
我们用shmget函数创建共享内存,用ipcs命令查看相关信息。
ipcs指令可以查看进程间通信的有关信息
我们可以在ipcs后加上选项来查看指定通信设施的信息
例
我们可以用 ipcs -m 来查看共享内存相关信息。
ipcs命令输出的每列信息的含义如下:
标题 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的d |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
shmget函数创建共享内存
comm.h文件
#pragma once
#include
#define PATH_NAME "/home/nzb/lesson20"
#define PROJ_ID 0x6666
#define SIZE 4097
server.c文件
#include"comm.h"
#include
#include
int main()
{
key_t k=ftok(PATH_NAME,PROJ_ID);
if(k<0){
perror("ftok");
return 1;
}
printf("key:%x\n",k);
int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL);// 共享内存不存在创建,> 存在报错
if(shmid<0){
perror("shmget");
return 2;
}
printf("shmid:%d\n",shmid);
return 0;
}
从图中我们看到 server 进程退出后,新建的共享内存并没有被删除,这是因为
所有的ipc资源都是随内核的,不随进程
那么如何释放ipc资源呢?
功能: 删除共享内存
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
返回值:
shmctl函数的第二个参数传入的常用的选项有以下三个:
命令 | 说明 |
---|---|
IPC_STAT | 将shmid_ds结构中的设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
例:
下面代码中创建共享内存,2秒后释放掉。
#include"comm.h"
#include
#include
#include
int main()
{
// 创建key
key_t k=ftok(PATH_NAME,PROJ_ID);
if(k<0){
perror("ftok");
return 1;
}
printf("key:%x\n",k);
// 申请共享内存
int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL);// 共享内存不存在创建,存在报错
if(shmid<0){
perror("shmget");
return 2;
}
printf("shmid:%d\n",shmid);
sleep(2);
// 释放共享内存
shmctl(shmid,IPC_RMID,NULL);
printf("delete shm!\n");
return 0;
}
我们可以在程序运行时,使用以下监控脚本时刻关注共享内存的资源分配情况:
while :; do ipcs -m;echo "###################################";sleep 1;done
shmat函数
功能: 将共享内存连接到进程地址空间
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
返回值:
shmflg传入的常用的选项有以下三个
选项 | 作用 |
---|---|
SHM_RDONLY | 关联共享内存后只进行读取操作 |
SHM_RND | 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA) |
0 | 默认为读写权限 |
shmdt函数
功能: 取消共享内存与进程地址空间之间的关联
函数原型:
int shmdt(const void *shmaddr);
参数
返回值
使用演示
#include"comm.h"
#include
#include
#include
int main()
{
// 创建key
key_t k=ftok(PATH_NAME,PROJ_ID);
if(k<0){
perror("ftok");
return 1;
}
printf("key:%x\n",k);
// 申请共享内存
int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0644);// 共享内存不存在创建,存在> 报错
if(shmid<0){
perror("shmget");
return 2;
}
printf("shmid:%d\n",shmid);
sleep(1);
// 将当前进程好共享内存关联
char* start=(char*)shmat(shmid,NULL,0);
printf("server already attach on shared memory!\n");
// 可以使用共享内存通信了
sleep(1);
// 将当前共享内存去关联
shmdt(start);
printf("server already dattch off shared memory!\n");
sleep(1);
// 释放共享内存
shmctl(shmid,IPC_RMID,NULL);
printf("delete shm!\n");
return 0;
}
注意:
shmat函数将共享内存连接到进程地址空间需要共享内存有相应的权限,我们在使用shmget函数创建共享内存时,需要在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。
int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0644);
了解了共享内存的相关函数后我们可以尝试实现共享内存间的通信了
头文件 comm.h
#pragma once
#include
#define PATH_NAME "/home/nzb/lesson20"
#define PROJ_ID 0x6666
#define SIZE 4097
server.c 文件
#include"comm.h"
#include
#include
#include
int main()
{
// 创建key
key_t k=ftok(PATH_NAME,PROJ_ID);
if(k<0){
perror("ftok");
return 1;
}
printf("key:%x\n",k);
// 申请共享内存
int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0644);// 共享内存不存在创建,存在> 报错
if(shmid<0){
perror("shmget");
return 2;
}
printf("shmid:%d\n",shmid);
// 将当前进程好共享内存关联
char* start=(char*)shmat(shmid,NULL,0);
printf("server already attach on shared memory!\n");
// 可以使用共享内存通信了
while (1){
printf("%s\n", start);
sleep(1);
}
// 将当前共享内存去关联
shmdt(start);
printf("server already dattch off shared memory!\n");
// 释放共享内存
shmctl(shmid,IPC_RMID,NULL);
printf("delete shm!\n");
return 0;
}
client.c 文件
#include"comm.h"
#include
#include
#include
#include
int main()
{
// 获取同一个key
key_t k=ftok(PATH_NAME,PROJ_ID);
if(k<0){
perror("ftok");
return 1;
}
printf("%x\n",k);
//不需要自己创建shm,获取共享内存
int shmid=shmget(k,SIZE,IPC_CREAT);
if(shmid<0){
perror("shmget");
return 2;
}
// 关联共享内存
char* start=(char*)shmat(shmid,NULL,0);
//客户端不断向共享内存写入数据
int i = 0;
while (1){
start[i] = 'A' + i;
i++;
sleep(1);
}
// 去关联
shmdt(start):
return 0;
}
消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。