文章目录
- 一、进程间通信
- 1.1 进程间通信的概念
- 1.2 进程间通信的目的
- 1.3 进程间通信的本质
- 1.4 进程间通信的分类
- 二、管道
- 2.1 匿名管道
- ① 匿名管道的使用场景
- ② 匿名管道实现通信的原理
- ③ 创建匿名管道 pipe函数
- ④ fork共享管道
- ⑤ 匿名管道的五个特点
- ⑥ 匿名管道的四种特殊情况
- 2.2 命名管道
- ① 命名管道的原理
- ② 命名管道的FIFO文件和一般文件有什么区别
- ③ 使用命令创建命名管道
- ④ 使用接口创建一个命名管道
- 三、小节
- 四、相关题目
一、进程间通信
1.1 进程间通信的概念
进程之间可能会存在特定的协同工作的场景!
一个进程要把自己的数据交付给另一个进程,让其进行处理-》进程间通信
由于进程具有独立性,一个进程看不到另一个进程的资源!所以交互数据的成本一定很高,这时就需要操作系统参与设计通信方式。两个进程要互相通信,就要能看到一份公共的资源,这里的资源就是一段内存!这个公共资源属于操作系统!
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
1.2 进程间通信的目的
- 数据传输: 一个进程需要将它的数据发送给另一个进程。
- 资源共享: 多个进程之间共享同样的资源。
- 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
- 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.3 进程间通信的本质
进程间通信的本质:其实就是由OS参与,提供一份所有通信进程都能看到的公共资源。
这段内存可能以文件的方式提供,也可能以队列的方式提供,也可能是原始的内存块。-》这也是通信方式有很多种的原因
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
1.4 进程间通信的分类
管道
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
本文主要介绍管道
二、管道
管道本身是一个文件。用管道实现进程间的通信,实际上是通过文件来实现进程间的通信。
管道又分为匿名管道和命名管道,顾名思义,匿名管道,创建的管道名字是不知道的,命名管道,创建管道的名字是知道的。
注意:管道实现通信只能进程单向通信。一个进程读,一个进程写。
2.1 匿名管道
① 匿名管道的使用场景
匿名管道通常使用于有亲缘关系的进程之间,常用于父子进程。看完原理再来理解一下这句话。
有亲缘关系的进程,会继承同一个祖先进程的部分内容。其中files_struct是继承祖先进程的。可以使两亲缘关系的进程指向同一文件。
所以为什么子进程会默认打开stdin,stdout,stderr,但是子进程并没有执行open操作?
只需要祖先进程打开了stdin,stdout,stderr就好了,子进程会拷贝父进程内容。
write做了两件事:把数据从用户缓冲区拷贝到内核缓冲区,触发底层硬件的写入函数,写进磁盘文件。父进程往缓冲区里写入,如果缓冲区里的数据没有写入文件,这时子进程就能读取到缓冲区里的数据,达成通信,这就是管道
注意:
- 这里父子进程看到的同一份文件资源是由操作系统来维护的,这个文件不属于任何一个进程,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
- 管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。
② 匿名管道实现通信的原理
- 管道也是一个文件,站在文件的角度来理解管道实现通信的原理。
管道是一个文件,当一个进程以读和写的方式打开一个管道。再创建一个子进程,子进程会以父进程为模板,拷贝父进程的部分内容。此时file_strcut里的数组(文件描述符与文件的映射关系)会是父进程的拷贝。此时,父子进程都指向了管道文件(同一块空间),并且子进程也是以读写方式打开的该文件(因为子进程会继承父进程代码,父进程在创建子进程之前以读写方式打开的文件),如果一个进程对文件进行写,一个进程对文件进行读,由于两个进程指向同一空间,所以读进程拿到的数据就是写进程写进去的数据。此时就完成了对文件的通信。
③ 创建匿名管道 pipe函数
pipe函数用于创建匿名管道,pip函数的函数原型,功能:创建匿名管道:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
数组元素 | 含义 |
---|---|
pipefd[0] | 管道读端的文件描述符 |
pipefd[1] | 管道写端的文件描述符 |
pipe函数调用成功时返回0,调用失败时返回-1,并设置错误码。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
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;
}
④ fork共享管道
原理
不同的进程,要用同一个匿名管道进行通信,则需要进程拥有该管道的读写两端的文件描述符,所以匿名管道只让能具有亲缘关系的进程进行进程间通信,且父进程需要先创建匿名管道,再创建子进程。如图所示:
上图中管道在内核空间,而父子进程的程序都在用户空间中。可以令子进程进行写,父进程进行读,关闭各自不用的文件描述符,仍可以进行单向通信。
程序
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include <fcntl.h>
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;
}
运行结果:
⑤ 匿名管道的五个特点
**① 管道内部自带同步与互斥机制 ** 👈
我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。
临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。
为了避免这些问题,内核会对管道操作进行同步与互斥:
- 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
- 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。
也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。
**② 管道的生命周期随进程 **👈
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
如果一个文件只被当前进程打开,相关进程退出了(会自动递减struct file的ref引用计数),被打开的文件会被操作系统自动关闭(ref为0)
③ 管道提供的是流式服务 👈
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
- 流式服务: 数据没有明确的分割,不分一定的报文段。
- 数据报服务: 数据有明确的分割,拿数据按报文段拿。
④ 仅限于父子通信 👈
具有血缘关系的进程之间可以进行通信,常用于父子间通信
⑤ 管道是半双工通信的 👈
在数据通信中,数据在线路上的传送方式可以分为以下三种:
- 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
- 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
- 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。
管道是半双工的,数据只能向一个方向流动(单向通信),需要双方通信时,需要建立起两个管道。
⑥ 匿名管道的四种特殊情况
在使用管道时,可能出现以下四种特殊情况:
- 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
- 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
- 如果写端关闭了,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾。
- 读端关闭,写端还在写入,此时OS会直接终止写入进程。如果父进程是读端,父进程能waitpid得到子进程的退出信号,父进程会收到SIGPIPE信号,13号信号。
前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
2.2 命名管道
为了解决匿名管道只能父子通信的问题,引入了命名管道
① 命名管道的原理
先创建一个命名管道。一个进程以读或者写的方式来打开该管道文件,另外一个进程不需要创建管道,只需要以写或者读的方式来打开管道文件。再调用读写系统调用来往文件写或者读,来进行进程间通信。
② 命名管道的FIFO文件和一般文件有什么区别
先说匿名管道的缺点,匿名管道无法让两个没有关系的进程通信。原因在于他是基于内存的,其他进程没有办法找到这块内存,也就没有办法通信了。那么命名管道的意义就在于,他有了名字,有了名字,每个进程就能找到他,也就可以完成通信了。具体的实现来说,命名管道的文件不是用来承载数据的,而且用来做地址用的,为了让每个进程都能通过这个地址进入通信的大门,假设他对应的文件名是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,表示要创建的命名管道文件。
- 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
- 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
将mode设置为0666,则命名管道文件创建出来的权限如下:
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask
函数将文件默认掩码设置为0。
umask(0); //将文件默认掩码设置为0
返回值
- 命名管道创建成功,返回0。
- 命名管道创建失败,返回-1。
代码演示
我们在一个进程(serve进程)中创建管道文件,并从中读取数据,在另一个进程(client进程)当中每秒向管道文件里面写入一次数据。
client进程运行后,进程serve会每秒从命名管道中读取数据。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
有了命名管道后,让通信双方进行文件操作即可,推荐使用系统调用接口,库函数接口也可以,但是会有用户缓冲区的一些问题。
共用头文件的代码如下:
命名管道的数据为了效率,不会刷新到磁盘!
为什么pipe叫匿名管道?因为它是通过父子继承的方式,看到同一份资源不需要名字来标识同一个资源!
为什么fifo叫命名管道?因为命名管道具有名字,这是为了保证不同的进程可以看到同一个文件,所以必须有名字!
三、小节
匿名管道还是命名管道都是通过文件的方式,来让两个进程看到同一份资源。通过一个进程对文件进行写操作,一个文件进程读操作,来实现进程间的通信。
管道实现进程通信是单向的。同步和互斥的。
匿名管道适用于具有血缘关系的进程,命名管道可以用于不相关的进程。
- 管道本质是内核中的一块缓冲区(命名管道和匿名管道),多个进程通过访问同一块缓冲区实现通信。
- 使用int pipe(int pipefd[2])接口创建匿名管道,pipefd[0]用于从管道读取数据,pipefd[1]用于向管道写入数据。
- 管道特性:半双工通信,自带同步与互斥,生命周期随进程,提供字节流传输服务。
- 在同步的提现中,若管道所有写段关闭,则从管道中读取完所有数据后,继续read会返回0,不再阻塞;若所有读端关闭,则继续write写入会触发异常导致进程退出
理解一个命令:
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选项正确,其他选项错误。