进程间通信(匿名管道、命名管道、共享内存)

进程间通信的作用

由于进程独立性的存在,两个进程想要直接交换数据是非常的困难的,所以需要进程间通信来解决进程与进程之间交换数据的问题

目前最大的进程间通信方式:网络

管道

匿名管道

命令感受匿名管道

在这里插入图片描述
这一个 “丨” 就是管道

作用:将 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   {
 11     perror("pipe fail\n");
 12     return 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   {
 11     perror("pipe fail\n");
 12     return 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   {
 11     perror("pipe fail\n");
 12     return 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   {
 21     perror("pipe fail");
 22     return 0;
 23   }
 24 
 25   ret = fork();
 26   if(ret < 0)
 27   {
 28     perror("fork fail");
 29     return 0;
 30   }
 31   else if(ret == 0)
 32   {
 33     //child
 34     close(fd[0]);//关闭读端,只留下写端
 35     SetNonBlock(fd[1]);
 36   } 
 37   else  
 38   {
 39     //father
 40     close(fd[1]);//关闭写端,只留下读端
 41     SetNonBlock(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   {
 18     perror("pipe fail");
 19     return 0;
 20   }
 21 
 22   ret = fork();
 23 
 24   if(ret < 0)
 25   {
 26     perror("fork fail");
 27     return 0;
 28   }
 29 
 30   else if(ret == 0)
 31   {
 32     //child
 33     close(fd[0]);//关闭读端,只留下写端
 34     SetNonBlock(fd[1]);
 35 
 36     //写端不关闭
 37     while(1)
 38     {
 39       sleep(1);
 40     }
 41   }
 42 
 43   else
 44   {
 45     //father
 46     close(fd[1]);//关闭写端,只留下读端
 47     SetNonBlock(fd[0]);
 48 
 49     char buf[1024] = {0};
 50     int read_size = read(fd[0], buf, sizeof(buf) - 1);
 51 
 52     while(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  
 47     close(fd[1]);//关闭写端,只留下读端  
 48     SetNonBlock(fd[0]);  
 49   
 50     while(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   {
 19     perror("pipe fail");
 20     return 0;
 21   }
 22 
 23   ret = fork();
 24 
 25   if(ret < 0)
 26   {
 27     perror("fork fail");
 28     return 0;
 29   }
 30 
 31   else if(ret == 0)
 32   {
 33     //child
 34     close(fd[0]);//关闭读端,只留下写端
 35     close(fd[1]);//写端关闭
 36 
 37     while(1)
 38     {
 39       sleep(1);
 40     }
 41   }
 42 
 43   else
 44   {
 45     //father
 46     close(fd[1]);//关闭写端,只留下读端
 47     SetNonBlock(fd[0]);
 48 
 49     char buf[1024] = {0};
 50     int read_size = read(fd[0], buf, sizeof(buf) - 1);
 51     printf("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   {
 19     perror("pipe fail");
 20     return 0;
 21   }
 22 
 23   ret = fork();
 24 
 25   if(ret < 0)
 26   {
 27     perror("fork fail");
 28     return 0;
 29   }
 30 
 31   else if(ret == 0)
 32   {
 33     //child
 34     close(fd[0]);//关闭读端,只留下写端
 35    
 36     int count = 0;
 37     while(1)
 38     {
 39       write(fd[1], "a", 1);
 40       printf("count : %d\n", count++);
 41     }
 42   }
 43 
 44   else
 45   {
 46     //father
 47     close(fd[1]);//关闭写端,只留下读端
 48     close(fd[0]);//关闭读端
 49 
 50     while(1)
 51     {
 52       sleep(1);
 53     }
 54   }
 55   return 0;
 56 }

在这里插入图片描述
在这里插入图片描述
因为此时读端已经被关闭了,而写端在进行写入,就好比一个水管不停的往里边输水,但是把出水口堵住,最终水管会破裂,也就导致了现在的僵尸进程

此时加上非阻塞

 31   else if(ret == 0)
 32   {                
 33     //child    
 34     close(fd[0]);//关闭读端,只留下写端
 35     SetNonBlock(fd[1]);                
 36 
 37     int count = 0;
 38     while(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   {
 19     perror("pipe fail");
 20     return 0;
 21   }
 22 
 23   ret = fork();
 24 
 25   if(ret < 0)
 26   {
 27     perror("fork fail");
 28     return 0;
 29   }
 30 
 31   else if(ret == 0)
 32   {
 33     //child
 34     close(fd[0]);//关闭读端,只留下写端
 35     SetNonBlock(fd[1]);
 36 
 37     int count = 0;
 38     while(1)
 39     {
 40       write(fd[1], "a", 1);
 41       printf("count : %d\n", count++);
 42     }
 43   }
 44 
 45   else
 46   {
 47     //father
 48     close(fd[1]);//关闭写端,只留下读端
 49     // close(fd[0]);不关闭读端
 50 
 51     while(1)
 52     {
 53       sleep(1);
 54     }
 55   }
 56   return 0;
 57 }

在这里插入图片描述
可以看到现在就不断的往里边写了

但是数值还是有点问题,于是修改一下

 31   else if(ret == 0)
 32   {
 33     //child
 34     close(fd[0]);//关闭读端,只留下写端
 35     SetNonBlock(fd[1]);
 36 
 37     int count = 0;
 38     while(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   {
 10     perror("open fail\n");
 11     return 0;
 12   }
 13 
 14   while(1)
 15   {
 16     write(fd, "oulaoula", 8);
 17     sleep(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   {
 10     perror("open fail\n");
 11     return 0;
 12   }
 13 
 14   while(1)
 15   {
 16     char buf[1024] = {0};
 17     read(fd, buf, sizeof(buf) - 1);
 18     printf("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 0x12121212
 6 
 7 int main()
 8 {
 9   int shmid = shmget(key, 1024, IPC_CREAT | 0664);
10   if(shmid < 0)
11   {
12     perror("shmget fail");
13     return 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 0x77777777
  7 
  8 int main()
  9 {
 10   int shmid = shmget(key, 1024, IPC_CREAT | 0664);
 11   if(shmid < 0)
 12   {
 13     perror("shmget fail\n");
 14     return 0;
 15   }
 16 
 17   //附加到当前的进程
 18   void* addr = shmat(shmid, NULL, 0);
 19   if(addr == NULL)
 20   {
 21     perror("shmat fail\n");
 22     return 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 0x77777777
  6 
  7 int main()
  8 {
  9   int shmid = shmget(key, 1024, IPC_CREAT | 0664);
 10   if(shmid < 0)
 11   {
 12     perror("shmget fail\n");
 13     return 0;
 14   }
 15 
 16   //附加到当前的进程
 17   void* addr = shmat(shmid, NULL, 0);
 18   if(addr == NULL)
 19   {
 20     perror("shmat fail\n");
 21     return 0;
 22   }
 23 
 24   while(1)
 25   {
 26     printf("%s\n", (char*)addr);
 27     sleep(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);         
 28     sprintf((char*)addr, "%s-%d", "i am write", count++);     
 29     sleep(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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值