之前所学都是单个进程,多个进程之间如何运转?
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止
时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程是有独立性的,这增加了通信的成本。要让两个不同的进程进行通信的前提条件就是让两个进程看到同一份资源。这是操作系统直接或间接提供的。
对于任何通信手段,都要先让不同的进程看到同一份资源,然后一方写入,一方读取来完成通信。
管道
System V进程间通信
POSIX进程间通信
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
可以同时创建多个进程,通过管道连接,它们的父进程也都一样,bash。
管道也是文件,一个文件以写方式打开管道,将自己的标准输出重定向到管道,另一头的文件以读方式打开管道,将自己的标准输入重定向到管道。
文件进程开始时,结构体里有文件描述符的表,指向各个文件,除此之外,还会打开一个匿名管道,这是一个操作系统提供的纯粹的内存文件,不需要刷新内容到磁盘。通过进程调用,这个匿名管道会通过读和写打开同一个文件;当前进程会fork一下,把自己的文件描述符表拷贝给子进程,具体的内容会有对应更改,那打开的文件等需要拷贝吗?不拷贝。但没关系,因为文件描述表的原因,子进程依然指向父进程创建好的文件,包含那个匿名管道。这其实也相当于一个浅拷贝。父子进程指向的文件一样。所以父子都指向那个匿名管道,一方更改,一方就能得到新数据;不过这时候管道只支持单向通信,接下来要做的是确定数据流向,关闭不需要的fd。操作系统会把子进程的写方式和父进程的读方式都关掉,这样父写子读,就形成了一个管道。
管道确实只能单向通信,如果想要双向,就定义两个管道。
简单的代码,子进程向父进程发消息,就是一种管道。
10 int main()
11 {
12 int pipefd[2] = {0};
13 //1、创建管道
14 int n = pipe(pipefd);
15 if(n < 0)
16 {
17 std::cout << "pipe error, " << errno << ":" << strerror(errno) << std::endl;
18 return 1;
19 }
20 std::cout << "pipefd[0]: " << pipefd[0] << std::endl;
21 std::cout << "pipefd[1]: " << pipefd[0] << std::endl;
22 //2、创建子进程
23 pid_t id = fork();
24 assert(id != -1);
25 if(id == 0)
26 {
27 //3、关闭不需要的fd,让父进程进行读取,让子进程进行写入
28 close(pipefd[0]);
29 //4、开始通信
30 const std::string namestr = "hello, 我是子进程";
31 int cnt = 1;
32 char buffer[1024];
33 while(true)
34 {
35 snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d\n", namestr.c_str(), cnt++, getpid());
36 write(pipefd[1], buffer, strlen(buffer));
37 sleep(1);
38 }
39 close(pipefd[1]);
40 exit(0);
41 }
42 //父进程
43 //3、关闭不需要的fd
44 close(pipefd[1]);
45 //4、开始通信
46 char buffer[1024];
47 while(true)
48 {
49 int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
50 if(n > 0)
51 {
52 buffer[n] = '\0';
53 std::cout << "我是父进程,child give me message: " << buffer << std::endl;
54 }
55 }
56 close(pipefd[0]);
57 return 0;
58 }
链接: https://gitee.com/kongqizyd/linux-beginner/tree/master/pipe
特点
1、单向通信,一种半双工,意思为双方交替着工作;全双工则是双方可以同时工作
2、管道本质是文件,因为文件描述符和文件的生命周期随进程,所以管道的生命周期也随进程。父子进程退出,那么之前关掉的文件描述符又会回到之前的位置
3、父子进程能够通信也是有继承存在于其中的。管道通信通常用于具有“血缘”关系的进程,常用于父子进程。兄弟进程之间也可以通信。pipe打开管道时不需要关心打开了哪个文件,只要得到两个文件描述符就好,因为pipe打开的匿名管道
4、管道通信中,写入的次数和读取的次数不是严格匹配的,可能读了7个但是只读了1次把它们都读取了。读写没有强相关。读取是面向字节流的,读取只看应读的字节数
5、管道具有一定的协同能力,让读和写能按照一定的步骤进行通信----自带同步机制
场景
1、如果我们read读取完了所有的管道数据,对方不写入,读取方只能等待
2、write端写满后(一般是65535/65536,大约是64kb),就不会继续写了,等到读端读取。可以在子进程那里不写sleep,而父进程sleep(10),看实际现象
3、如果关闭了写端,读取完管道数据,再读就会返回0,表明读到了文件结尾
4、写端一直写,读端关闭,那么写入就变得没有意义;操作系统不会维护无意义,低效率,浪费资源的进程,所以进程会直接杀掉这个进程。系统会通过信号来终止进程,SIGPIPE -13关闭进程
父进程可以通过向子进程写入特定的消息,唤醒子进程,甚至让子进程定向的执行某种任务。父进程可以打开多个管道和子进程连接。父进程对自己创建的管道和进程会先描述再组织。
子进程在创建时会继承父进程的文件描述符,所以第一个父进程会有好多个子进程连接到它,这就混乱了。实际要实现的结构是一对一,一对父子进程间有自己独立的管道,而不受其他进程影响。
链接: https://gitee.com/kongqizyd/linux-beginner/tree/master/ctrlProcess
匿名管道有局限性,只能用于有血缘关系的进程之间进行通信,要让两个陌生的进程之间通信就需要用到命名管道。
创建命名管道要用到mkfifo命令,括号里就是命名管道参数,fifo意思就是先进先出
它的文件类型以p开头,说明是一个管道文件。
现在写个命令echo “字符串” >fifo,往这里面写内容,但是光标会停在下一行开头,一直闪烁,这是因为fifo文件只是一个符号,向里面写的东西不会真实存在在磁盘中,只写到管道文件中,我们可以用cat命名读,它默认从显示器读,cat < fifo就会从管道文件读,然后打印到显示器上。即使写一个一直输出内容的代码,也可以另开一个窗口用cat来获取内容。
在之前基础IO博客中的硬链接部分写到过引用计数这个东西,命名管道里也会用到引用计数。在磁盘中的一个文件,如果不打开,就待在磁盘里;打开后就会有自己的文件结构体,里面有各项参数,其中就有引用计数;操作者创建进程后,会有文件描述符表等东西,进程可以打开在内存中的文件,此时引用计数就变成了1;如果再开一个进程,打开同样的文件,系统不会在开一个这个文件的结构体,而是这两个进程指向同一个文件结构体,计数变为2,进程一个个消失,计数就从2变为0。但两个进程如何保证打开同一个文件,要想让文件唯一,文件路径+文件名都相同就可以。
创建管道文件,让读写端进程分别按照自己的需求打开文件,然后开始通信。
我们定义两个文件,要形成两个可执行程序需要这样写。
链接: https://gitee.com/kongqizyd/linux-beginner/tree/master/namepipe
结束。