目录
进程间通信的背景
为什么要进行进程间通信?
管道
什么是管道?
匿名管道
匿名管道原理
如何创建匿名管道?
命名管道
进程间通信的背景
进程间通信就是在不同的进程之间进行的数据的交换,进程间通信又称为Interprocess communication,简称IPC,Linux下的IPC有多种方式,比如,管道,System V IPC,POSIX IPC。
POSIX的全称是 "Portable Operating System Interface",POSIX不仅仅是一个单一标准,而且是IEEE(Institute for Electrical and Electronics Engineers, Inc. IEEE)指定的一个标准族。POSIX IPC一共有三个,就是Message Queue(消息队列),semaphores(信号量),Shared Memory(共享内存)
为什么要进行进程间通信?
进程具有独立性,进程之间交互数据的成本比较高,通信的目的主要是为了进行数据传输、资源共享、通知事件,进程控制。
通信本质:让不同的进程看到同一个资源(比如文件,内存块,队列,网络...),不同资源决定不同种类的通信方式。
管道
什么是管道?
管道是Unix最古老的通信方式,比如 在命令行上启动who程序和wc程序:
who程序是查看当前登录的用户个数:
wc则是打印文件的相关信息,-l选项是打印文件有多少行
使用命令who | wc -l就可以知道有多少个用户正在登录了:
这个|就是管道,who进程把文件内容写入管道当中,wc进程再从管道当中读取内容,最终输出到用户客户端上,这个管道是内存级文件,管道文件也是一样的,因为内存级文件读取写入速度远大于磁盘文件,底层是将磁盘文件打开对应的内存上的文件缓冲区作为管道。
画图理解如下:
匿名管道
匿名管道原理
Linux中的文件描述符_且随疾风前行->的博客-CSDN博客
由Linux文件描述符篇,进程控制块PCB中是有文件指针指向被打开的文件的。
所以父进程打开文件,创建子进程, 子进程拷贝了父进程的文件指针数组,而文件指针指向的文件内部有缓冲区,于是父子进程就可以通过看到的同一个文件的缓冲区进行通信,画图理解创建进程过程如下:
在内核代码中看内核管道的数据结构也可以看见管道的缓冲区:
这个缓冲区就在内存中,可以看到缓冲区中的页指针:
如何创建匿名管道?
父进程创建管道文件,并打开管道文件的读写端,子进程继承父进程的管道文件,然后关闭父进程或子进程对应的读写端,完成管道的单向通信。之所以让父进程同时打开文件的读写端,是为了方便子进程继承父进程的读写端不用再读写打开文件。关闭读写端是因为这是管道的特性,管道在内核的底层实现就是单向通信,即一方读取一方写入的单向通信,谁关闭写端谁关闭读端由需求决定。
比如:要实现管道通信,让父进程做写端,子进程做读端,就是下面的图解,
前面提到在命令行上的管道通信本质上是我们的父进程,创建了管道文件,然后又创建了两个子进程继承了管道文件,然后两个子进程之间进行进程间通信。
使用系统调用pipe创建管道文件系统调用会将文件描述符写入pipefd数组,零号下标表示读端,一号下标表示写端。(联想记忆:0-嘴巴-读书-读端,1-笔-写字-写端)
测试代码:
输出:
可见打开的是3号文件和4号文件
创建管道文件代码示例:
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe error!" << endl;
return 1;
}
pid_t id = fork();
if (id == 0)
{
sleep(1);
// child,read,close write
close(pipefd[1]);
char buff[1024];
while (1)
{
ssize_t s = read(pipefd[0], (void *)buff, sizeof buff);
if (s > 0)
{
cout << "child process echo:" << buff << endl;
}
else if (s == 0) // 表示读取结束,因为写端关闭
{
cout << "read end" << endl;
exit(0);
}
else
{
cout << "read error" << endl;
exit(-1);
}
}
}
close(pipefd[0]);
// parent,write,close read
while (1)
{
char buff[1024] = "message from parent process---";
write(pipefd[1], buff, sizeof buff);
sleep(2);
}
return 0;
}
注意read函数使用:read函数读取文件读取成功,返回读取的字节数,读取失败返回负一,读取结束返回零,为何能知道读取结束呢?因为文件当中有类似引用计数的文件计数,记录了文件被打开的次数,当写端关闭的时候,计数减一,所以读端文件能够感知到,所以能够结束读取返回0,运行程序时也可以验证,比如杀死父进程,子进程会读取结束:
当代码运行时,父进程每隔两秒写入管道一次,而每隔两秒子进程输出一次,这说明当管道中没有数据的时候,子进程读端会堵塞式地等待读取。而且可以验证当写端写满的时候,写端将会阻塞,不再写入,这说明读写端管道文件是具有访问控制和同步机制的。阻塞等待的本质是将进程PCB放入等待队列中,这个等待队列就在管道文件的内部维护着。
匿名管道的特征总结:
管道只能用于具有血缘关系的进程之间的通信,常用于父子之间的通信。
管道只能进行单向通信,也即一方写入一方读取,这是半双工的一种特殊情况。(半双工指的是一方读取,一方写入,但不固定哪一方读取或者哪一方写入。)
管道是面向字节流的,先写入的先被读取,没有格式边界。
管道本质是文件,文件的生命周期是随进程的。
命名管道
命名管道可以用于让两个没有血缘关系的进程之间实现通信,在命令行上可以使用mkfifo创建管道文件,比如用mkfifo实现cat和echo进程之间的管道通信:
管道通信本质上是让这两个进程都看到了同一份的管道文件,注意是单向通信,而且管道文件在内存中是内存级文件,不需要刷新到内存中,也可以使用系统调用函数mkfifo创建管道文件。