学习系统编程No.20【进程间通信之命名管道】
引言:
北京时间:2023/4/15/10:34,今天起床时间9:25,睡了快8小时,昨天刷视屏刷了一个小时,本来12点的时候发完博客洗把脸就要睡了,可惜,看到了一个标题,说实话,现在的标题党是懂人性的,接下来就是无法自拔的一个小时快乐时光,但导致莫名间接熬夜,你说烦人不烦人!但是不怕,这个星期5天,几乎没有摆烂,只要今天和明天不摆烂 ,这个星期就是成功滴,一想美滋滋!所以让我们抓紧进入今天的学习吧! 今天我们就正式学习有关内存共享的知识,当然还有管道遗落的知识,因为管道真的是太经典了
,但是前提是,现在我要去把我的被单和毯子给晒一晒,不然晚上无法舒舒服服的睡觉啦!并且由于昨天买的羽毛球拍到了,所以下午是打羽毛球的快乐时光,所以该篇博客,今天能不能发,是未可知的!
回顾管道控制进程
之前开始学习进程间通信的时候,我们就学习了如何使用管道来进行进程间通信,了解了通过一个管道进行进程间通信之后,我们又通过管道的读写规则了解了"血缘关系"
进程之间的通信方法,此时我们通过一个 很经典的通信方式(当管道中没有数据的时候,对应读取数据的进程只能等待对应写数据的进程)
,所以通过这一重要的读写原理,此时我们就设计出了进程控制的结构
,一个进程(常常是父进程)通过创建多个管道和子进程,然后向对应的管道中写入数据,进而控制与该管道文件匹配的子进程,所以明白了这个原理之后,此时就可以进行具体的编码和优化,最终得到了上篇博客所示的进程控制代码,但是该代码中,还存在一个很隐蔽的问题,上篇博客,我们没有进行详细的讲解,所以在这里,我们进行补充讲解,如下:
回收子进程
1.让子进程退出
从进程控制代码中(上篇博客),我们看出,当我们构建好了进程控制代码,可以让父进程通过对管道文件的读写规则同时去控制子进程完成不同的任务时,在最后任务完成结束,代码要退出时,我们是需要将子进程进行回收(因为子进程占用的是内存上的空间,避免内存泄露),所以此时在回收子进程的问题上,就存在那个很隐蔽的问题,此时我们就需要解决这个问题,如下:
想要解决这个问题,首先明白一点,就是如何让子进程退出,这点想必大家都很清楚(上篇博客详细介绍了),想要让一个子进程退出(场景:上篇博客代码,子进程读取管道文件,父进程向管道文件写入),只要让父进程无法向对应子进程的管道文件中写入数据,此时操作系统就会因为不允许资源浪费,使 用13号信号
,让子进程退出,所以让一个子进程退出,就是将对应管道文件的写端关闭就行
明白了这点,此时我们距离解决这个隐蔽问题就剩下一个现象,看到了这个现象,此时一切都是So,So,如下图所示:
如上图所示,明白到了,除了第一次循环的子进程是只继承了一个写文件描述符,其它子进程的写文件描述符都是在累加,所以导致除了最后一次循环创建的那个管道文件的写端只有父进程指向之外(因为最后一次循环子进程为了构建单向信道,需要把写端关闭),其它的管道文件都被除了父进程以外的一个或多个进程以写的方式指向(如上图,第五次循环之后的子进程,此时对前面创建的4个管道文件的写端都有写入的能力),具体如下图所示:
所以上图的第一个管道文件的写端由于被不止一个进程打开,所以此时单单只是将父进程对应的写端关闭,还有剩下多个子进程可以向该管道文件写入数据,所以此时操作系统并不会使用13号信号
,将该子进程退出,所以最好的解决方法就是如上图所示:
从后向前关闭父进程对应管道文件的写端,这样就可以让子进程挨个退出了,进而最终达到回收子进程的目的
有关上述现象图的继承理解:
注意:
在常见的操作系统中,当子进程从父进程中继承文件描述符表时,子进程会得到指向同一文件表项的新的文件描述符副本(继承),关闭一个文件描述符只会减少该文件描述符所指向的文件表项的引用计数,如果仍然有其他文件描述符指向该文件表项,则该文件表项仍然存在;因此,无论父进程关闭其中的一个文件描述符或者两个,当子进程从它那里继承文件描述符表时,子进程都会得到对这两个打开文件的文件描述符
总结: 只要父进程分别以读写的方式打开了同一文件,创建了两个文件描述符,那么此时无论父进程是关闭一个文件描述符,还是两个都关闭,此时由于子进程继承了父进程pcb大部分的内容,并且因为进程间具有独立性,所以此时操作系统检测都父进程将要关闭文件描述符的时候(也就是修改数据的时候),就会进行写实拷贝(在内存中重新开辟一个空间给父进程去修改数据),所以导致子进程中继承的父进程pcb是不会被改变,所以子进程任然可以分别继承以读写方式打开的两个文件描述符
2.等待僵尸状态的子进程
明白了上述知识点之后,此时就可以明白除了最后一次循环创建的管道文件只被一个进程以写的方式打开之外,其它的管道文件都是同时被两个或者两个以上的进程以写的方式打开(父进程和子进程),所以最好的方法就是从后向前将父进程对应管道文件的写端关闭,此时所有的子进程就被很好的关闭了,所以当子进程被关闭之后,此时最后一步就是将处于僵尸状态的子进程回收(waitpid
),具体代码如下:
具体现象如下:
所以此时因为进程控制创建出的被控制进程和创建的管道文件就都被关闭和回收啦!利用管道控制进程的代码就大功告成啦! 具体代码贴在该博客最后(虽然上篇博客中有)
深入进程控制结构
通过上述的知识,我们知道,我们可以很好的利用进程间通信的知识和模板去构建出一个进程控制的结构,并且这个结构中存在的问题(上述问题),因为子进程会继承上一进程打开的所有文件描述符,导致一个子进程同时具有多个读端和写端,此时根据这个现象,就会存在很多的问题,不仅仅只是上述回收子进程的时候需要从后向前回收,更重要的是 ,此时还有引起另一个严重问题,就是后一个子进程也可以向前一个管道文件中写入数据,有甚者更是可以向多个管道文件中写入数据(虽然因为没有特定的文件描述符,子进程并不能写入,但是为了防止),所以需要避免这个问题,此时我们就可以通过将代码结构进行一定的改变,进而真正的构建出像进程间通信一般的单向信道,如下:
原理:本质上,在实现上述进程控制的前提下,让每一个管道和进程之间都互不干扰,实现单向通信是不难的,实际从问题出发,就是要解决子进程继承父进程文件描述符时,多余进程的文件描述符而已,所以此时我们只需要再创建一个数组,用这个数组专门来保存父进程创建的写端文件描述符,得到父进程所有的写端文件描述符,然后把这个数组中的数据拿给子进程使用,间接在子进程中遍历这个数组,并且使用close关闭子进程继承的文件描述符表中对应数组下标对应的写端文件描述符,这样子进程中多余进程到的写端文件描述符就被全部关闭,最终真正的实现单向信道进程控制结构
具体代码实现如下:
命名管道
无论是之前的进程间数据传送的知识,还是上述进程控制的知识,本质上都只是在利用管道进行而已,准确的来说也就是利用一个内存级的文件而已,并且要明白,此时的这个管道是使用系统调用接口 pipe
来创建的,所以本质上这个管道文件是操作系统给我们提供的,我们是摸不着,看不到的,所以准确的来说,我们之前学习的进程数据传送和进程控制利用的管道都是匿名管道,一个由内核创建,操作系统管理的文件
并且还可以得出一个结论,我们一直利用的都是具有"血缘关系"
的进程可以继承同一文件描述符的特性和同时以读写方式打开同一匿名管道文件进行学习有关进程间通信的知识,此时提出问题:那么如果是两个完全没有关系的进程,此时进程间还可以进行通信吗?首先答案是可以的,不然它怎么能叫进程间通信呢?就应该叫血缘关系进程进程间通信了,具体如下述所说:
如何创建命名管道
首先明白,想要让两个不同的进程支持相互通信,那么此时就不可以使用匿名管道,而要用命名管道,所以下面第一个知识点,我们就来了解一下命名管道的创建,注意
:命名管道的创建区别于匿名管道的创建,它不仅可以直接使用命令行创建,指令:mkfifo filename
,也可以使用系统调用创建,调用接口:int mkfifo(const char *filename,mode_t mode);
基本使用:mkfifo("filename", 0644);
具体使用方式如下图所示:
通过上图中对 mkfifo
系统调用接口的描述,此时我们就可以知道,该接口的功能就是用来创建一个命名管道,头文件为#include<sys/types.h>,#include<sys/stat.h>
等具体使用方式,此时我们就可以很好的自己创建一个命名管道出来啦!
注意:
虽然,mkfifo
是创建一个文件,并且这个文件不像是匿名管道文件一样是由内核创建的内存级文件(不占用磁盘空间),并且该文件是位于文件系统中,但该文件却并不对应任何物理磁盘上的文件,而只是在内存中分配一个空间来存储不同进程之间进行进程间通信时的读取或写入的数据而已(类似于文件对象自带的缓冲区),所以从这个意义上来说,我们可以将mkfifo创建的文件视为一种内存级文件,因为它们的存在仅限于进程之间的通信,当进程间完成通信之后,回收进程时,此时由于它们的生命周期与它们关联的进程相同,所以当这些进程终止时,这些文件也会被自动删除,所以本质上命名管道文件就是一个内存级缓冲区文件!
得出结论: 使用 mkfifo
创建的文件并不会占用磁盘空间,因为它只是一个命名管道(内存级文件),只有在进程进行通信时,也就是一个进程打开一个命名管道并向其中写入数据,另一个进程打开同一个命名管道并从中读取数据,此时才会产生实际的数据传输和占用磁盘空间,否则执行 mkfifo
命令只会在文件系统中创建一个新的文件节点,并不会分配任何实际的磁盘空间。
管道文件类型:
对文件类型和文件属性感兴趣的同学可以参考该链接博客:文件类型详解
文件类型 |
---|
普通文件 - |
目录文件 d |
链接文件 l |
块设备 b |
字符设备 c |
管道文件 p |
套接字文件 s |
深入命名管道
搞定了上述有关命名管道文件的知识,此时我们就可以具体的来看一看,是如何利用命名管道实现不同进程之间的通信了,如下图所示:
如上图所示:此时我们就构建出了一个和构建父子进程间通信结构类似的模板,此时按照这个原理,我们就可以进行代码的编写啦!简单理解就是两个进程打开了同一个文件,一个进程以读的方式打开,一个进程以写的方式打开,本质能够通信还是因为,操作系统为了节约资源,所有进程共享同一个已经打开的文件,而不是重复打开,并且 注意,
如果我们想要让不同的两个进程打开同一个文件,此时就需要让它们根据同一路径
去寻找该对应文件**,所以只要让不同的进程通过文件路径和文件名看到同一文件,并打开,这就是两个不同进程看到同一份资源的前提,也就是进程间通信的前提!
代码实现
原理: 1.创建一个管道文件 2.让读写端进程分别按照自己的需求打开文件 3.开始通信
首先创建两个文件,一个serves.cpp
,一个client.cpp
,用来分别表示两个不一样的进程,具体代码如下:
serves.cpp文件
client.cpp文件
command.hpp共享文件
两个进程代码通信现象:
如上图所示,此时一个进程就可以很好的把数据传送给另一个进程,也就类似于一个用户端可以把数据传送给服务端
总:明白了使用匿名管道的进程控制,玩一个命名管道So,So!
总结:快乐生活每一天,有关管道的知识就这样了吧!
使用匿名管道进行进程控制代码如下: