进程之间可能会存在特定的协同工作的场景!
一个进程要把自己的数据交付给另一个进程,让其进行处理-》进程间通信
由于进程具有独立性,一个进程看不到另一个进程的资源!所以交互数据的成本一定很高,这时就需要操作系统参与设计通信方式。两个进程要互相通信,就要能看到一份公共的资源,这里的资源就是一段内存!这个公共资源属于操作系统!
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
进程间通信的本质:其实就是由OS参与,提供一份所有通信进程都能看到的公共资源。
这段内存可能以文件的方式提供,也可能以队列的方式提供,也可能是原始的内存块。-》这也是通信方式有很多种的原因
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。

因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
管道
System V IPC
POSIX IPC
本文主要介绍管道
管道本身是一个文件。用管道实现进程间的通信,实际上是通过文件来实现进程间的通信。
管道又分为匿名管道和命名管道,顾名思义,匿名管道,创建的管道名字是不知道的,命名管道,创建管道的名字是知道的。
注意:管道实现通信只能进程单向通信。一个进程读,一个进程写。
匿名管道通常使用于有亲缘关系的进程之间,常用于父子进程。看完原理再来理解一下这句话。
有亲缘关系的进程,会继承同一个祖先进程的部分内容。其中files_struct是继承祖先进程的。可以使两亲缘关系的进程指向同一文件。
所以为什么子进程会默认打开stdin,stdout,stderr,但是子进程并没有执行open操作?
只需要祖先进程打开了stdin,stdout,stderr就好了,子进程会拷贝父进程内容。

write做了两件事:把数据从用户缓冲区拷贝到内核缓冲区,触发底层硬件的写入函数,写进磁盘文件。父进程往缓冲区里写入,如果缓冲区里的数据没有写入文件,这时子进程就能读取到缓冲区里的数据,达成通信,这就是管道
注意:
管道是一个文件,当一个进程以读和写的方式打开一个管道。再创建一个子进程,子进程会以父进程为模板,拷贝父进程的部分内容。此时file_strcut里的数组(文件描述符与文件的映射关系)会是父进程的拷贝。此时,父子进程都指向了管道文件(同一块空间),并且子进程也是以读写方式打开的该文件(因为子进程会继承父进程代码,父进程在创建子进程之前以读写方式打开的文件),如果一个进程对文件进行写,一个进程对文件进行读,由于两个进程指向同一空间,所以读进程拿到的数据就是写进程写进去的数据。此时就完成了对文件的通信。

![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bpZEIw9g-1673141600698)(null)]](/uploadfile/202405/9908742fd13369d.png)
pipe函数用于创建匿名管道,pip函数的函数原型,功能:创建匿名管道:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
| 数组元素 | 含义 |
|---|---|
| pipefd[0] | 管道读端的文件描述符 |
| pipefd[1] | 管道写端的文件描述符 |
pipe函数调用成功时返回0,调用失败时返回-1,并设置错误码。
#include
#include
#include
int main(){int fd[2] = {0}; //以读写方式打开管道int res = pipe(fd);if(res == 0){printf("fd[0]:%d,fd[1]:%d\n",fd[0],fd[1]);//打印:fd[0]:3,fd[1]:4 // 文件描述符fd:0 1 2 被标准输入 标准输出 标准错误 占据}else{perror("pipe error");exit(1);}return 0;
}
原理
不同的进程,要用同一个匿名管道进行通信,则需要进程拥有该管道的读写两端的文件描述符,所以匿名管道只让能具有亲缘关系的进程进行进程间通信,且父进程需要先创建匿名管道,再创建子进程。如图所示:

上图中管道在内核空间,而父子进程的程序都在用户空间中。可以令子进程进行写,父进程进行读,关闭各自不用的文件描述符,仍可以进行单向通信。

程序
#include
#include
#include
#include
int main()
{int pipefd[2] = {0};//父进程需要读写两端都打开,否则子进程继承下去就只有一个端口。if(pipe(pipefd)!=0){perror("pipe");return 1;}int id = fork();if(id > 0)//父进程{//parent写入close(pipefd[0]);//关闭父进程的pipe读端const char* msg = "i am father";while(1){sleep(1);write(pipefd[1],msg,strlen(msg));}printf("father quit...\n");return 0;}else if(id == 0)//子进程{//child读取close(pipefd[1]);//关闭子进程的pipe写端char buff[64] = {0};while(1){sleep(1);ssize_t s = read(pipefd[0], buff, sizeof(buff)-1);if(s>0){buff[s] = 0;//C语言里字符串以0表示结束printf("i am child,i read:%s\n",buff);}else if(s==0){break;}else{break;}}}else{//error}return 0;
}
运行结果:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mv9uLhQh-1673141600673)(null)]](/uploadfile/202405/dd6898bfc8008e3.png)
**① 管道内部自带同步与互斥机制 ** 👈
我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。
临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。
为了避免这些问题,内核会对管道操作进行同步与互斥:
实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。
也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。
**② 管道的生命周期随进程 **👈
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
如果一个文件只被当前进程打开,相关进程退出了(会自动递减struct file的ref引用计数),被打开的文件会被操作系统自动关闭(ref为0)
③ 管道提供的是流式服务 👈
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
④ 仅限于父子通信 👈
具有血缘关系的进程之间可以进行通信,常用于父子间通信
⑤ 管道是半双工通信的 👈
在数据通信中,数据在线路上的传送方式可以分为以下三种:
管道是半双工的,数据只能向一个方向流动(单向通信),需要双方通信时,需要建立起两个管道。
在使用管道时,可能出现以下四种特殊情况:
前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
为了解决匿名管道只能父子通信的问题,引入了命名管道
先创建一个命名管道。一个进程以读或者写的方式来打开该管道文件,另外一个进程不需要创建管道,只需要以写或者读的方式来打开管道文件。再调用读写系统调用来往文件写或者读,来进行进程间通信。
先说匿名管道的缺点,匿名管道无法让两个没有关系的进程通信。原因在于他是基于内存的,其他进程没有办法找到这块内存,也就没有办法通信了。那么命名管道的意义就在于,他有了名字,有了名字,每个进程就能找到他,也就可以完成通信了。具体的实现来说,命名管道的文件不是用来承载数据的,而且用来做地址用的,为了让每个进程都能通过这个地址进入通信的大门,假设他对应的文件名是a,进程1打开a的时候,拿到一个fd,file,inode,操作系统判断这个文件类型,知道是命名管道文件,会给他分配一块内存,这时候你操作这个文件的时候,数据是写到内存里的,同理,这时候进程2也打开这个文件,也拿到一个fd,file,inode。但是一个文件只有一个inode。所以他们操作的是同一个inode,那就意味着他们操作的是同一块内存,这样就可以通信了。最后,两个进程通过一个一般文件通信当然也是可以的。但是会涉及到硬盘的操作,效率自然就会低。
我们通常用 路径+文件名 标识一个磁盘文件,它们具有唯一性。
A,B两个进程如何看到并打开同一个文件的?先将文件数据打开到内存当中,如:写数据时不刷新到磁盘中,数据临时保存在内存,然后两个进程就能通过 路径+文件名 看到同一份资源。
管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信
命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
我们可以使用mkfifo命令创建一个命名管道。

可以看到,创建出来的文件的类型是p,代表该文件是命名管道文件。
简单使用这个命名管道:

在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:
int mkfifo(const char *pathname, mode_t mode);//在man的3号手册
参数
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
将mode设置为0666,则命名管道文件创建出来的权限如下:

但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
umask(0); //将文件默认掩码设置为0
返回值
代码演示
我们在一个进程(serve进程)中创建管道文件,并从中读取数据,在另一个进程(client进程)当中每秒向管道文件里面写入一次数据。
client进程运行后,进程serve会每秒从命名管道中读取数据。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
有了命名管道后,让通信双方进行文件操作即可,推荐使用系统调用接口,库函数接口也可以,但是会有用户缓冲区的一些问题。



共用头文件的代码如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uTQlzEgA-1673141600715)(null)]](/uploadfile/202405/f199607709dfdf.png)
命名管道的数据为了效率,不会刷新到磁盘!
为什么pipe叫匿名管道?因为它是通过父子继承的方式,看到同一份资源不需要名字来标识同一个资源!
为什么fifo叫命名管道?因为命名管道具有名字,这是为了保证不同的进程可以看到同一个文件,所以必须有名字!
匿名管道还是命名管道都是通过文件的方式,来让两个进程看到同一份资源。通过一个进程对文件进行写操作,一个文件进程读操作,来实现进程间的通信。
管道实现进程通信是单向的。同步和互斥的。
匿名管道适用于具有血缘关系的进程,命名管道可以用于不相关的进程。
理解一个命令:
who | wc - l中间的’|‘是一个匿名管道,who和wc是两个进程,’|'系统通过pipe创建一个匿名管道,bash创建子进程who,bash再创建子进程wc,who和wc是兄弟进程。who和wc都会继承bash的匿名管道文件。who和wc看到统一资源,who往管道写数据,wc从管道读数据。往后数据写完退出,wc读数据到最后也退出了。
1、以下描述正确的有:
A.进程之间可以直接通过地址访问进行相互通信
B.进程之间不可以直接通过地址访问进行相互通信
C.所有的进程间通信都是通过内核中的缓冲区实现的
D.以上都是错误的
答案解析
A错误: 进程之间具有独立性,拥有自己的虚拟地址空间,因此无法通过各自的虚拟地址进行通信(A的地址经过B的页表映射不一定映射在相同位置)
B正确
C错误: 除了内核中的缓冲区之外还有文件以及网络通信的方式可以实现
2、以下选项属于进程间通信的是()[多选]
A.管道
B.套接字
C.内存
D.消息队列
答案解析
典型进程间通信方式:管道,共享内存,消息队列,信号量。 除此之外还有网络通信,以及文件等多种方式
C选项,这里的内存太过宽泛,并没有特指某种技术,错误。
正确答案是:A,B,D
3、下列关于管道(Pipe)通信的叙述中,正确的是()
A.一个管道可以实现双向数据传输
B.管道的容量仅受磁盘容量大小限制
C.进程对管道进行读操作和写操作都可能被阻塞
D.一个管道只能有一个读进程或一个写进程对其操作
答案解析
A错误 管道是半双工通信,是可以选择方向的单向通信
B错误 管道的本质是内核中的缓冲区,通过内核缓冲区实现通信,命名管道的文件虽然可见于文件系统,但是只是标识符,并非通信介质
C正确 管道自带同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥
D错误 多个进程只要能够访问同一管道就可以实现通信,不限于读写个数
4、以下关于管道的描述中,正确的是 [多选]
A.匿名管道可以用于任意进程间通信
B.匿名管道只能用于具有亲缘关系的进程间通信
C.在创建子进程之后也可以通过创建匿名管道实现父子进程间通信
D.必须在创建子进程之前创建匿名管道才能实现父子进程间通信
答案解析
A错误,匿名管道只能用于具有亲缘关系的进程间通信,命名管道可以用于同一主机上的任意进程间通信
B正确
C错误,匿名管道需要在创建子进程之前创建,因为只有这样才能复制到管道的操作句柄,与具有亲缘关系的进程实现访问同一个管道通信
D正确
5、以下关于管道描述正确的有:
A.命名管道可以用于同一主机上的任意进程间通信
B.向命名管道中写入的数据越多,则管道文件越大
C.若以只读的方式打开命名管道时,则打开操作会报错
D.命名管道可以实现双向通信
答案解析
根据以上理解分析:A选项正确,其他选项错误。