进程间通信
进程间通信的作用管道匿名管道命令感受匿名管道从内核角度去解释匿名管道代码创建匿名管道从PCB角度去分析匿名管道匿名管道的非阻塞读写特性创建管道,获取管道读写两端文件描述符的属性给读写两端的文件描述符设置非阻塞属性测试非阻塞读端进行读(非阻塞),写端不写(不操作)写端非阻塞进行写命名管道原理创建命名管道及使用命名管道进行进程间通信共享内存原理共享内存的接口创建共享内存附加共享内存到进程分离共享内存共享内存操作函数共享内存的代码消息队列&信号量接口进程间通信的作用
由于进程独立性的存在,两个进程想要直接交换数据是非常的困难的,所以需要进程间通信来解决进程与进程之间交换数据的问题
目前最大的进程间通信方式:网络
管道
匿名管道
命令感受匿名管道
这一个 “丨” 就是管道
作用:将 ps aux 命令的输出结果通过管道输入 grep 并作为 grep 的输入参数
从内核角度去解释匿名管道
管道就是内核当中的一块缓冲区(一块内存),进程A和进程B可以通过这个缓冲区进行交换数据
代码创建匿名管道
int pipe(int pipefd[2]);
参数:
pipefd:类型为整型数组,有两个元素,pipefd[0],pipefd[1]pipefd[0],pipefd[1]:当中保存的是一个文件描述符pipefd[0]:对应的文件描述符可以从管道当中进行读,不能写pipefd[1]:对应的文件描述符可以往管道当中写,但不能读pipefd[0],pipefd[1]当中的值是pipe函数进行赋值的,直白的说,当我们调用pipe函数的时候,只需要给pipe函数传递一个拥有两个元素的整型数据的数组,pipe函数在创建完毕管道后,会给pipefd[0],pipefd[1]进行赋值
返回值:
-1:创建失败0:创建成功
闪烁有两种情况:
1、软链接指向的源文件被删除2、软链接指向的是一块内存,而不是一个具体的文件
从PCB角度去分析匿名管道
1、匿名管道只适用于具有亲缘关系的进程,进行进程间通信
2、先创建管道,再创建子进程,父子进程才可以进行进程间通信
3、如果想要两个子进程使用匿名管道进行进程间通信,需要先创建管道,再创建子进程
4.管道的数据只能从写端流向读端,这是一种半双工的通信方式
全双工通信:数据可以从A端流向B端,也可以从B端流向A端
5、通过fd[0]从管道当中读取数据的时候,是将数据读走了(并不是拷贝了一份)
6、从管道当中去读数据的时候,可以指定读取任意大小的数据,如果管道当中没有数据,默认情况下,进行读则会阻塞
7、多次写入的数据之间是没有明显的分界的,上一条数据的末尾连接下一条数据的开头位置
8、匿名管道的生命周期跟随进程
匿名管道的非阻塞读写特性
非阻塞:
fcntl函数:设置/获取文件描述符的属性
int fcntl(int fd, int cmd, ...);
cmd 决定了 fcntl 函数究竟做什么事情
F_GETFL:获取文件描述符的属性,可变参数列表就可以不用传递任何值
F_SETFL:设置文件描述符的属性,需要制定设置文件描述符的属性,采用按位或的方式
非阻塞属性:O_NONBLOCK
返回值:
如果是获取(F_GETFL),返回文件描述符的属性
创建管道,获取管道读写两端文件描述符的属性
1 #include <stdio.h> 2 #include <fcntl.h>3 #include <unistd.h>4 5 int main()6 {7 int fd[2];8 int ret = pipe(fd);9 if(ret < 0)10 {11perror("pipe fail\n");12return 0;13 }14 15 //获取读端的文件描述符属性16 int flag = fcntl(fd[0],F_GETFL);17 printf("flag fd[0]: %d\n",flag);18 //获取写端的文件描述符属性19 flag = fcntl(fd[1],F_GETFL);20 printf("flag fd[1]: %d\n",flag);21 22 return 0;23 }
给读写两端的文件描述符设置非阻塞属性
首先是读端
1 #include <stdio.h>2 #include <fcntl.h>3 #include <unistd.h>4 5 int main()6 {7 int fd[2];8 int ret = pipe(fd);9 if(ret < 0)10 {11perror("pipe fail\n");12return 0;13 }14 15 int flag = fcntl(fd[0],F_GETFL);16 printf("flag fd[0]: %d\n",flag);17 18 fcntl(fd[0], F_SETFL, flag | O_NONBLOCK);19 20 flag = fcntl(fd[0],F_GETFL); 21 printf("flag fd[0]: %d\n",flag);22 23 return 0;24 }
随后是写端
1 #include <stdio.h>2 #include <fcntl.h>3 #include <unistd.h>4 5 int main()6 {7 int fd[2];8 int ret = pipe(fd);9 if(ret < 0)10 {11perror("pipe fail\n");12return 0;13 }14 15 int flag = fcntl(fd[1],F_GETFL);16 printf("flag fd[0]: %d\n",flag);17 18 fcntl(fd[1], F_SETFL, flag | O_NONBLOCK);19 20 flag = fcntl(fd[1],F_GETFL); 21 printf("flag fd[0]: %d\n",flag);22 23 return 0;24 }
测试非阻塞
1 #include <stdio.h> 2 #include <unistd.h>3 #include <fcntl.h>4 5 /* 1、创建匿名管道,之后创建子进程,让子进程进行进程间通信6 * 2、因为父子进程当中的文件描述符表都是拥有fd[0],fd[1],规定父进程读,子进程写7 * 3、再测试非阻塞属性*/8 9 void SetNonBlock(int fd)//提供一个函数给对应的fd[x]加上非阻塞属性10 {11 int flag = fcntl(fd, F_GETFL);12 fcntl(fd, F_SETFL, flag | O_NONBLOCK);13 }14 15 int main()16 {17 int fd[2];18 int ret = pipe(fd);19 if(ret < 0)20 {21perror("pipe fail");22return 0;23 }24 25 ret = fork();26 if(ret < 0)27 {28perror("fork fail");29return 0;30 }31 else if(ret == 0)32 {33//child34close(fd[0]);//关闭读端,只留下写端35SetNonBlock(fd[1]);36 } 37 else 38 {39//father40close(fd[1]);//关闭写端,只留下读端41SetNonBlock(fd[0]);42 }43 return 0;44 }
读端进行读(非阻塞),写端不写(不操作)
1、写端不关闭
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 5 void SetNonBlock(int fd)6 {7 int flag = fcntl(fd, F_GETFL);8 fcntl(fd, F_SETFL, flag | O_NONBLOCK);9 }10 11 int main()12 {13 int fd[2];14 int ret = pipe(fd);15 16 if(ret < 0)17 {18perror("pipe fail");19return 0;20 }21 22 ret = fork();23 24 if(ret < 0)25 {26perror("fork fail");27return 0;28 }29 30 else if(ret == 0)31 {32//child33close(fd[0]);//关闭读端,只留下写端34SetNonBlock(fd[1]);35 36//写端不关闭37while(1)38{39 sleep(1);40}41 }42 43 else44 {45//father46close(fd[1]);//关闭写端,只留下读端47SetNonBlock(fd[0]);48 49char buf[1024] = {0};50int read_size = read(fd[0], buf, sizeof(buf) - 1);51 52while(1)53{54 printf("read_size : %d\nbuf : %s\n", read_size, buf);55}56 }57 58 return 0;59 }
但是此时无法确定读端调用 read,read 函数返回-1,是因为管道当中没有内容还是由于调用函数出错表示的,因此需要更改
44 else 45 {46//father 47close(fd[1]);//关闭写端,只留下读端 48SetNonBlock(fd[0]); 49 50while(1) 51{52 char buf[1024] = {0}; 53 int read_size = read(fd[0], buf, sizeof(buf) - 1); 54 55 if(read_size < 0) 56 {57 if(errno == EAGAIN) 58 {59 printf("管道为空\n"); 60 printf("read_size : %d\nbuf : %s\n", read_size, buf); 61 } 62 } 63} 64 }
此时需要额外包一个头文件
#include <errno.h>
再运行一次
如果错误码为 EAGAIN ,应该认为是正常情况
2、写端关闭
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 #include <errno.h>5 6 void SetNonBlock(int fd)7 {8 int flag = fcntl(fd, F_GETFL);9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);10 }11 12 int main()13 {14 int fd[2];15 int ret = pipe(fd);16 17 if(ret < 0)18 {19perror("pipe fail");20return 0;21 }22 23 ret = fork();24 25 if(ret < 0)26 {27perror("fork fail");28return 0;29 }30 31 else if(ret == 0)32 {33//child34close(fd[0]);//关闭读端,只留下写端35close(fd[1]);//写端关闭36 37while(1)38{39 sleep(1);40}41 }42 43 else44 {45//father46close(fd[1]);//关闭写端,只留下读端47SetNonBlock(fd[0]);48 49char buf[1024] = {0};50int read_size = read(fd[0], buf, sizeof(buf) - 1);51printf("read_size : %d, buf = %s\n", read_size, buf);52 }53 54 return 0;55 }
调用 read 返回 -1
写端非阻塞进行写
1、读端关闭
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 #include <errno.h>5 6 void SetNonBlock(int fd)7 {8 int flag = fcntl(fd, F_GETFL);9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);10 }11 12 int main()13 {14 int fd[2];15 int ret = pipe(fd);16 17 if(ret < 0)18 {19perror("pipe fail");20return 0;21 }22 23 ret = fork();24 25 if(ret < 0)26 {27perror("fork fail");28return 0;29 }30 31 else if(ret == 0)32 {33//child34close(fd[0]);//关闭读端,只留下写端35 36int count = 0;37while(1)38{39 write(fd[1], "a", 1);40 printf("count : %d\n", count++);41}42 }43 44 else45 {46//father47close(fd[1]);//关闭写端,只留下读端48close(fd[0]);//关闭读端49 50while(1)51{52 sleep(1);53}54 }55 return 0;56 }
因为此时读端已经被关闭了,而写端在进行写入,就好比一个水管不停的往里边输水,但是把出水口堵住,最终水管会破裂,也就导致了现在的僵尸进程
此时加上非阻塞
31 else if(ret == 0)32 {33//child 34close(fd[0]);//关闭读端,只留下写端35SetNonBlock(fd[1]);36 37int count = 0;38while(1)39{40 write(fd[1], "a", 1);41 printf("count : %d\n", count++); 42}43 }
可以看到还是一样的情况,都是僵尸进程
即当前在通过 fd[1] 往管道当中去写的时候,会导致管道破裂,调用写的进程会被终止(信号终止)
2、读端不关闭
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 #include <errno.h>5 6 void SetNonBlock(int fd)7 {8 int flag = fcntl(fd, F_GETFL);9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);10 }11 12 int main()13 {14 int fd[2];15 int ret = pipe(fd);16 17 if(ret < 0)18 {19perror("pipe fail");20return 0;21 }22 23 ret = fork();24 25 if(ret < 0)26 {27perror("fork fail");28return 0;29 }30 31 else if(ret == 0)32 {33//child34close(fd[0]);//关闭读端,只留下写端35SetNonBlock(fd[1]);36 37int count = 0;38while(1)39{40 write(fd[1], "a", 1);41 printf("count : %d\n", count++);42}43 }44 45 else46 {47//father48close(fd[1]);//关闭写端,只留下读端49// close(fd[0]);不关闭读端50 51while(1)52{53 sleep(1);54}55 }56 return 0;57 }
可以看到现在就不断的往里边写了
但是数值还是有点问题,于是修改一下
31 else if(ret == 0)32 {33//child34close(fd[0]);//关闭读端,只留下写端35SetNonBlock(fd[1]);36 37int count = 0;38while(1)39{40 int write_size = write(fd[1], "a", 1);41 42 if(write_size < 0)43 {44 printf("write_size: %d\n",write_size);45 if(errno == EAGAIN)46 {47 printf("管道已满\n");48 break;49 }50 }51 printf("count : %d\n", count++);52}
命名管道
原理
也是在内核当中开辟了一块缓冲区,这块缓冲区是有标识符,可以被任何进程通过标识符找到
创建命名管道及使用命名管道进行进程间通信
命令创建:mkfifo
p 代表的是管道文件
写:
1 #include <stdio.h> 2 #include <unistd.h>3 #include <fcntl.h>4 5 int main()6 {7 int fd = open("./fifo_test", O_RDWR);8 if(fd < 0)9 {10perror("open fail\n");11return 0;12 }13 14 while(1)15 {16write(fd, "oulaoula", 8);17sleep(1);18 }19 close(fd);20 return 0;21 }
读:
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 5 int main()6 {7 int fd = open("./fifo_test", O_RDWR);8 if(fd < 0)9 {10perror("open fail\n");11return 0;12 }13 14 while(1)15 {16char buf[1024] = {0};17read(fd, buf, sizeof(buf) - 1);18printf("buf : %s\n", buf); 19 }20 21 close(fd);22 return 0;23 }
此时这两个进程便实现了进程间通信
命名管道的生命周期也跟随进程
小知识:fifo,为first in first out的缩写,即先进先出
因为命名管道有标识符,所以命名管道支持不同进程之间的进程间通信
其他特性同匿名管道一样
共享内存
原理
1、首先在物理内存中创建了一块内存2、不同的进程通过页表映射,将同一块物理内存映射到自己的虚拟地址空间3、不同的进程操作进程虚拟地址,通过页表的映射,就相当于操作同一块内存,从而完成了数据交换
共享内存的接口
创建共享内存
int shmget(key_t key, size_t shmflg);
key:共享内存的标识符,用来标识一块共享内存,在操作系统中,共享内存的标识是不能重复的,可以直接给一个32位的16进制数字size:共享内存的大小shmflg:IPC_CREAT:如果key标识的共享内存不存在,则创建IPC_EXCL | IPC_CREAT:如果key标识的共享内存存在,则新创建一个后报错权限:按位或 8进制数字 例:0664 创建用户可读可写,组内用户可读可写,其他用户可读返回值:-1:创建失败了>0:成功,返回的是共享内存的操作句柄,后续是通过操作句柄来操作共享内存的
查看共享内存的命令:ipcs -m
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/shm.h>4 5 #define key 0x121212126 7 int main()8 {9 int shmid = shmget(key, 1024, IPC_CREAT | 0664);10 if(shmid < 0)11 {12perror("shmget fail");13return 0;14 }15 16 17 return 0;18 }
key:标识符shmid:操作句柄owner:创建者perms:权限bytes:共享内存大小nattch:附加进程数量ststus:状态
共享内存的生命周期跟随操作系统内核
附加共享内存到进程
void *shmat(int shmind, const void *shmaddr, int shmflg);
shmid:共享内存操作句柄,即shmget的返回值shmaddr:将共享内存附加到shmaddr,一般情况下都不会自己去指定映射到共享区中的哪一个虚拟地址,而是传递NULL值,让操作系统去选择shmflg:标志将共享内存附加到进程后,进程对共享内存的读写属性0:读写SHM_RDONLY:只读返回值:附加成功:返回值为附加到共享区当中的虚拟地址附加失败:NULL
分离共享内存
int shmdt(const void *shmaddr);
shmaddr:刚刚附加的时候,返回的共享区的地址
共享内存操作函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存操作句柄cmd:IPC_STAT:获取共享内存参数IPC_SET :设置共享内存属性IPC_RMID:删除共享内存struct shmid_ds:共享内存属性对应的结构体
共享内存的代码
写:
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/shm.h>4 #include <string.h>5 6 #define key 0x777777777 8 int main()9 {10 int shmid = shmget(key, 1024, IPC_CREAT | 0664);11 if(shmid < 0)12 {13perror("shmget fail\n");14return 0;15 }16 17 //附加到当前的进程18 void* addr = shmat(shmid, NULL, 0);19 if(addr == NULL)20 {21perror("shmat fail\n");22return 0;23 }24 //写 25 strncpy((char*)addr, "i am write", 10);26 27 shmdt(addr);28 29 return 0;30 }
读:
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/shm.h>4 5 #define key 0x777777776 7 int main()8 {9 int shmid = shmget(key, 1024, IPC_CREAT | 0664);10 if(shmid < 0)11 {12perror("shmget fail\n");13return 0;14 }15 16 //附加到当前的进程17 void* addr = shmat(shmid, NULL, 0);18 if(addr == NULL)19 {20perror("shmat fail\n");21return 0;22 }23 24 while(1)25 {26printf("%s\n", (char*)addr);27sleep(1);28 }29 30 shmdt(addr);31 32 return 0;33 }
共享内存读取的时候采用的是拷贝,而不是类似于管道一样的读走
更改一下
23 int count = 0;24 //写 25 while(1) 26 {27//strncpy((char*)addr, "i am write", 10); 28sprintf((char*)addr, "%s-%d", "i am write", count++);29sleep(1);30 }
即共享内存在写的时候采用的是覆盖写的方式
使用 ipcrm -m [共享内存操作句柄] 可以删除共享内存
如果删除了一个被进程附加的共享内存当前共享内存的标识符会改变成为0x00000000,且共享内存的状态会变成dest(destory)可以通过 ipcs -m 这个命令查看到当前被删除共享内存的信息,说明在操作系统内核,描述该共享内存的结构体没有被释放,但是共享内存所使用的空间已经被释放了,所以附加的进程如果再次操作共享内存,则有崩溃的风险
消息队列&信号量
队列的特性:先进先出
消息队列本质上也是在内核当中维护的一个双向链表,但满足了先进先出的特性小,所以被称之为队列
消息队列当中的消息:消息只的是带有类型的数据,类型和类型之间是有优先级的
接口
int msgget(key_t key, int msgflg);
key: 消息队列的标识符返回值:成功返回消息队列的操作句柄
int msgsnd(int msqid, const void *msgp, size_t msgsz, int masgflg);
msqid:消息队列的操作句柄msgp:要发送到消息队列的消息msgsz:指定发送的数据大小,只计算自己发送数据的大小msgflg:IPC_NOWAIT:非阻塞发送方式0:阻塞发送
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msqid:消息队列的操作句柄msgp:将接收的数据放到哪里msgsz:最大的接收能力msgtyp:> 0:表示获取队列当中距离队首最近同类型的元素==0:直接拿队首的元素< 0:1、需要将小于0的msgtype的值取绝对值2、过滤从队首到[msgtype]区间的消息3、从区间中获取和[msgtype]一样的消息4、再去当中获取类型最小的数据msgflg:阻塞接收:0非阻塞接收:IPC_NOWAIT