Linux 进程间通信 管道系列: 匿名和命名管道,自定义shell当中命令行管道的模拟实现
Linux 进程间通信1: 匿名和命名管道以及进程池的实现
- 一.进程间通信的介绍
-
- 1.为什么要进程进程间通信?
- 2.什么是进程间通信
- 3.进程间通信的具体做法
- 二.管道
-
- 1.从文件的角度理解什么是管道?
- 三.匿名管道
-
- 1.验证代码
- 2.四种情况
-
- 1.写端不写,且不退
- 2.读端不读,且不退
- 3.写端不写,退了
- 4.读端不读,退了
- 5.小小总结
- 3.五种特性
- 4.理解命令行管道
- 四.命名管道
-
- 1.理论
-
- 1.回顾匿名管道的原理
- 2.命名管道理论
- 2.系统调用接口的介绍与使用
-
- 1.介绍
- 2.使用
- 3.代码编写
-
- 1.Common.hpp
- 2.pipeServer.cpp
- 3.pipeClient.cpp
- 4.makefile
- 4.动图演示
- 五.自定义shell当中添加命令行管道的功能
-
- 1.前言
- 2.思路
-
- 1.如何创建管道与进程
- 2.预处理
- 3.实现
- 4.演示
- 5.代码
一.进程间通信的介绍
1.为什么要进程进程间通信?
因为有些情况下需要进程完成以下任务:
而我们知道进程之间是不能进行"数据"的直接传递的,因为进程具有独立性
因此才有了我们今天要谈论的进程间通信
2.什么是进程间通信
通俗点讲,进程间通信就是一个进程能够把自己的数据交给另一个进程
进程间进程通信就是进行数据的交互,那么数据存放在哪里呢?
下面我们来分析一下
因此,关于进程间通信的一个非非非非非非常重要的结论就得出了:
进程间通信的本质是让不同的进程,看到同一份资源(一般都是要由OS提供)
3.进程间通信的具体做法
因为OS提供的"数据存放的空间"有着你不同的样式,就决定了进程间通信有着不同的通信方式
二.管道
管道的本质就是一个内存级文件,不存储在磁盘上,只存储在内存当中
1.从文件的角度理解什么是管道?
创建子进程之后log.txt的struct file对象的引用计数++
此时父子进程都对log.txt这个文件同时具有读写权限
也就是说父子进程看到了同一份资源(log.txt),这就是进程间通信
此时如果子进程对log.txt进行写入,父进程对log.txt进行读取
这不就完成了进程间通信了吗?
因此,这种
基于文件,让不同进程看到同一份资源的方式,就叫做管道!
如果让父子进程同时对log.txt既有读权限,又有写权限,那不就容易发生混乱吗?
因此设计管道的工程师规定:
管道只能被设计为单向通信!
那么我们如何把刚才的情况改为只能进行单向通信呢?
比方说我们要求父进程作为读端,子进程作为写端
只需要关闭父进程的’w’权限和子进程的’r’权限即可
log.txt的struct file对象的引用计数–
只有当引用计数减为0时,才会释放该struct file对象和其内核缓冲区
此时,父子进程的通信方式就叫做管道!!
回想一下,刚才我们是如何让不同的进程看到同一份资源的呢?
通过创建子进程,子进程会继承父进程的相关属性信息
子进程继承了父进程的相关信息,子进程的子进程也会继承子进程的相关信息
那么子进程的子进程不就也能跟父进程进行进程间通信了吗?
是的,因此进程之间只要具有血缘关系,那么就可以利用管道来进行进程间通信
而如果没有血缘关系,那么就无法利用管道来进行进程间通信了
三.匿名管道
如果此时我们想要让两个进程之间进行通信,但是不想在磁盘当中建立单独的文件,怎么办呢?
此时就可以使用匿名管道了
匿名管道:只能让具有血缘关系的进程之间进行进程间通信(常用于父子进程)
(祖先跟后代可以,兄弟之间也可以哦)
如何利用呢?
pipe这个函数就是用来创建匿名管道的,传入的参数是一个数组,是一个输出型参数
执行该函数后,会得到2个fd,分别存储在pipefd[0]和pipefd[1]的位置
pipefd[0]存储的是负责r(读)的fd,pipefd[1]存储的是负责w(写)的fd
pipe所创建的匿名管道是一个内存级的文件,并不存在于磁盘当中!!
创建成功返回值为0,创建失败返回值小于0
1.验证代码
下面我们来写一个验证代码
这份代码的含义是:
1.创建管道
2.创建子进程
3.子进程死循环向管道当中写入数据
4.父进程死循环从管道当中读取数据
然后我们编译生成可执行程序,开始运行
至此我们验证完毕
2.四种情况
经过刚才的演示,我们知道父子进程是如何通过管道来进行通信的了
但是还是不够细节,因为有些情况没有涉及到,下面我们来一一看看这4种情况吧
为了方便演示,我们改一下代码,让子进程发送消息时每次少发一些
不着急,我们一一分析
1.写端不写,且不退
2.读端不读,且不退
补充一点:
PIPE_BUF:管道的缓存大小是4096字节
当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性
当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性
关于原子性我们以后会还会见到的
3.写端不写,退了
4.读端不读,退了
其实发送的是13号信号:
SIGPIPE
验证:
5.小小总结
3.五种特性
4.理解命令行管道
我们之前在Linux常见指令2当中介绍过命令行管道的使用
而今天我们学习了管道之后,我们再回过头来重新认识一下命令行管道
ps ajx | head -1 && ps ajx | grep 可执行程序名字 | grep -v grep
还记得我们的监控脚本吗?
其实它就是一个命令行管道的典型应用
四.命名管道
匿名管道挺好的,只不过只能由具有血缘关系的进程才能够使用,还是有些局限性的
能不能让没有血缘关系的进程之间也能使用管道来通信呢?
是可以的,不过需要使用我们接下来要介绍的命名管道
1.理论
1.回顾匿名管道的原理
2.命名管道理论
2.系统调用接口的介绍与使用
1.介绍
跟匿名管道一样,这件事情OS不放心让用户完成,也是为了给用户一个良好的使用体验,因此OS提供了系统调用接口
mkfifo
2.使用
我们知道|是命令行当中的匿名管道,说明Linux支持在命令行当中创建匿名管道,那么命名管道呢?
如果不允许的话,Linux是不是就有点偏心了啊
Linux也允许在命令行当中使用mkfifo来创建命名文件
曾记否:我们之前在介绍Linux下的文件类型的时候见过这个管道文件哦
今天我们想说的是:创建了一个命名管道之后,就不能再创建同名的命名管道了
因此我们在创建了命名管道之后,当本次客户端和服务端通信结束之后,为了让下一次对应的代码还能正常运行(也就是创建命名管道成功)
而且肯定是客户端先退出,所以我们要在服务端退出时将这个命名管道删掉
如何删呢?总不能进程程序替换执行个rm -f xxx命名管道吧,那也太挫了吧
OS肯定要提供系统调用接口
同样的unlink也是一个指令哦
介绍完我们需要使用的新增的系统调用接口之后,下面我们来搞代码啦
3.代码编写
这里unlink,read,write,open等等都要判断是否成功,这里为了让代码更加简洁,就没怎么判断,大家可以加上,返回错误码是真的难受…
还是异常香
1.Common.hpp
命名管道是不是只有一份呢?
不是,可以同时有很多份,OS要不要管理,要
如何管理?先描述,在组织
走起
2.pipeServer.cpp
注意:
命名管道创建之后
如果只有读端被打开了,写端还没有被打开,那么读端会阻塞在open函数当中
如果只有写端被打开了,读端还没有被打开,那么写端会阻塞在open函数当中
3.pipeClient.cpp
4.makefile
因为要生成2个可执行程序,因此需要用一个伪目标
(当然你make xxx两次也可以,主要是不优雅)
4.动图演示
此时就能够随便玩了
当然你要是想换一下读写端的话,两个人约定一些暗号等等的信息,输入之后就先暂时退出,然后各自换一种权限打开该文件等等,
这一点上面比匿名管道好玩总之,你想怎么玩就怎么写
随意~
五.自定义shell当中添加命令行管道的功能
1.前言
前言 : 有些内建命令的确可以跟命令行管道一起使用,例如echo,不过某些内建命令无法跟命令行管道一起使用:比如cd
命令行管道可以跟重定向>>,>,<一起使用
拿出我们实现完重定向的shell,(我今天是由自己写了一遍,所以跟上一次的有些不同,但功能是一样的,大家知道能这么玩就行,有空的时候可以自己玩一下)
(还有就是那个echo $?返回最近一次错误码,因为最近刚学了异常,所以看到这种错误码就浑身难受,所以没有搞这个,不过不妨碍我们今天要实现的重定向)
这是一个cpp文件(因为如果用C添加命令行管道[太麻烦了,连个顺序表都没有]…写C++写惯了,不想用C…)
为了方便实现,我们把main函数中的代码拿出去了两部分
代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NoneRedir 0
#define OutputRedir 1
#define InputRedir 2
#define AppendRedir 3
using namespace std;
int lastcode=0;//上一次进程退出时的退出码
int redir=NoneRedir;
char* filename=NULL;
char cwd[1024]={
' '};
char env[1024][1024]={
' '};
int my_index=0;
//将字符串s按照空格作为分割符拆分后添加到vs当中(不过: 空格可能会连续出现)
void CommandLineSplit(char* command,char* commandV[])
{
char* s=strtok(command," ");
commandV[0]=s;
int i=1;
while(commandV[i++]=strtok(NULL," ")){
}
}
#define SkipSpace(pos) do{
while(isspace(*pos)){
pos++;}}while(0)
void CheckRedir(char* command)
{
int len=strlen(command);
char* pos;
for(int i=len-1;i>=0;)
{
if(command[i]=='>')
{
if(i>0 && command[i-1]=='>')//追加
{
redir=AppendRedir;
command[i-1]=' ';
}
else//输出
{
redir=OutputRedir;
command[i]=' ';