Linux 进程间通信 管道系列: 匿名和命名管道,自定义shell当中命令行管道的模拟实现

program-learner 2024-07-12 12:07:03 阅读 82

Linux 进程间通信1: 匿名和命名管道以及进程池的实现

一.进程间通信的介绍1.为什么要进程进程间通信?2.什么是进程间通信3.进程间通信的具体做法

二.管道1.从文件的角度理解什么是管道?

三.匿名管道1.验证代码2.四种情况1.写端不写,且不退2.读端不读,且不退3.写端不写,退了4.读端不读,退了5.小小总结

3.五种特性4.理解命令行管道

四.命名管道1.理论1.回顾匿名管道的原理2.命名管道理论

2.系统调用接口的介绍与使用1.介绍2.使用

3.代码编写1.Common.hpp2.pipeServer.cpp3.pipeClient.cpp4.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当中介绍过命令行管道的使用

在这里插入图片描述

而今天我们学习了管道之后,我们再回过头来重新认识一下命令行管道

<code>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函数中的代码拿出去了两部分

代码:

<code>#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <sys/wait.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <ctype.h>

#include <fcntl.h>

#include <vector>

#include <iostream>

#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]={ '\0'};

char env[1024][1024]={ '\0'};

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]='\0';

}

else//输出

{

redir=OutputRedir;

command[i]='\0';

}

pos=command+i+1;

SkipSpace(pos);

filename=pos;

break;

}

else if(command[i]=='<')//输入

{

redir=InputRedir;

command[i]='\0';

pos=command+i+1;

SkipSpace(pos);

filename=pos;

break;

}

else

{

i--;

}

}

}

void Exec(char* commandV[])

{

pid_t id=fork();

if(id==0)

{

if(redir==InputRedir)

{

int fd=open(filename,O_RDONLY);

dup2(fd,0);

}

else if(redir==OutputRedir)

{

int fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);

dup2(fd,1);

}

else if(redir==AppendRedir)

{

int fd=open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);

dup2(fd,1);

}

execvp(commandV[0],commandV);

}

wait(NULL);

}

void cd(char* path)

{

chdir(path);

char tmp[1024]={ '\0'};

getcwd(tmp,sizeof(tmp));

sprintf(cwd,"PWD=%s",tmp);

putenv(cwd);

}

void Export(char* s)

{

strcpy(env[my_index],s);

putenv(env[my_index++]);

}

int echo(char* commandV[])

{

//1.echo后面什么都没有,相当于'\n'

if(commandV[1]==NULL)

{

printf("\n");

lastcode=0;

return 1;

}

//2.echo $? echo $PWD echo $

char* cmd=commandV[1];

int len=strlen(cmd);

if(cmd[0]=='$' && len>1)

{

//echo $?

if(cmd[1]=='?')

{

printf("%d\n",lastcode);

lastcode=0;

}

//echo $PWD

else

{

char* tmp=cmd+1;

const char* env=getenv(tmp);

//找不到该环境变量,打印'\n',退出码依旧为0

if(env==NULL)

{

printf("\n");

}

else

{

printf("%s\n",env);

}

lastcode=0;

}

}

else

{

if(cmd[0]=='"' && cmd[len-1]=='"')

{

cmd[len-1]='\0';

printf("%s\n",cmd+1);

}

else

printf("%s\n",cmd);

}

return 1;

}

int doBulidIn(char* commandV[])

{

int ret=0;

if(strcmp(commandV[0],"cd")==0)

{

cd(commandV[1]);

ret=1;

}

else if(strcmp(commandV[0],"export")==0)

{

Export(commandV[1]);

ret=1;

}

else if(strcmp(commandV[0],"echo")==0)

{

echo(commandV);

ret=1;

}

return ret;

}

//打印提示符和修正redir,filename的函数

void step1()

{

redir=NoneRedir;

filename=NULL;

//1.打印提示符wzs@VM-16-10-ubuntu:~/ubuntucode/shell$

printf("%s@VM-16-10-ubuntu:%s$ ",getenv("USER"),getenv("PWD"));

}

//具体执行指令的函数

void step3(char* command)

{

//3.检查重定向

CheckRedir(command);

//4.解析字符串

char* commandV[1024]={ NULL};

CommandLineSplit(command,commandV);

//5.分析内建命令

int ret=doBulidIn(commandV);

if(ret==0)

{

//6.进程程序替换

Exec(commandV);

}

}

int main()

{

while(1)

{

step1();

char command[1024]={ '\0'};

fgets(command,sizeof(command),stdin);

int len=strlen(command);

command[len-1]='\0';

step3(command);

}

return 0;

}

2.思路

1.如何创建管道与进程

在这里插入图片描述

因此,我们只需要边创建管道,边创建进程,(最后一个进程单独创建)

并且用<code>vector<pair<int,int>>存储所有的管道的读写端方便后续管道对其进行关闭

(因为管道的特点是读/写端没有全都退出,另一端就会一直阻塞等待,如果不关闭读写端的话父进程回收时就必须要逆序回收)

又因为我们要让父进程回收子进程,所以在用一个vector<int>存储所有的子进程pid,后续还要waitpid回收他们呢

2.预处理

在检查是否需要重定向之前,我们需要先检查整个字符串有多少个’|‘命令行管道,并且

[1]用一个vector<int>保存它们的位置,并且将对应位置改为’\0’,方便后续把每一块指令传递给子进程去处理

[2]因为第一个指令左侧没有’|',所以我们在初始化vector<int>的时候,可以先存上一个-1,然后分配任务时就好分配了

3.实现

只有刚才的这两步,搞清楚这两点之后代码就能够很好的写出来了

在这里插入图片描述

4.演示

在这里插入图片描述

5.代码

<code>#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <sys/wait.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <ctype.h>

#include <fcntl.h>

#include <vector>

#include <iostream>

#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]={ '\0'};

char env[1024][1024]={ '\0'};

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]='\0';

}

else//输出

{

redir=OutputRedir;

command[i]='\0';

}

pos=command+i+1;

SkipSpace(pos);

filename=pos;

break;

}

else if(command[i]=='<')//输入

{

redir=InputRedir;

command[i]='\0';

pos=command+i+1;

SkipSpace(pos);

filename=pos;

break;

}

else

{

i--;

}

}

}

void Exec(char* commandV[])

{

pid_t id=fork();

if(id==0)

{

if(redir==InputRedir)

{

int fd=open(filename,O_RDONLY);

dup2(fd,0);

}

else if(redir==OutputRedir)

{

int fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);

dup2(fd,1);

}

else if(redir==AppendRedir)

{

int fd=open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);

dup2(fd,1);

}

execvp(commandV[0],commandV);

}

wait(NULL);

}

void cd(char* path)

{

chdir(path);

char tmp[1024]={ '\0'};

getcwd(tmp,sizeof(tmp));

sprintf(cwd,"PWD=%s",tmp);

putenv(cwd);

}

void Export(char* s)

{

strcpy(env[my_index],s);

putenv(env[my_index++]);

}

int echo(char* commandV[])

{

//1.echo后面什么都没有,相当于'\n'

if(commandV[1]==NULL)

{

printf("\n");

lastcode=0;

return 1;

}

//2.echo $? echo $PWD echo $

char* cmd=commandV[1];

int len=strlen(cmd);

if(cmd[0]=='$' && len>1)

{

//echo $?

if(cmd[1]=='?')

{

printf("%d\n",lastcode);

lastcode=0;

}

//echo $PWD

else

{

char* tmp=cmd+1;

const char* env=getenv(tmp);

//找不到该环境变量,打印'\n',退出码依旧为0

if(env==NULL)

{

printf("\n");

}

else

{

printf("%s\n",env);

}

lastcode=0;

}

}

else

{

if(cmd[0]=='"' && cmd[len-1]=='"')

{

cmd[len-1]='\0';

printf("%s\n",cmd+1);

}

else

printf("%s\n",cmd);

}

return 1;

}

int doBulidIn(char* commandV[])

{

int ret=0;

if(strcmp(commandV[0],"cd")==0)

{

cd(commandV[1]);

ret=1;

}

else if(strcmp(commandV[0],"export")==0)

{

Export(commandV[1]);

ret=1;

}

else if(strcmp(commandV[0],"echo")==0)

{

echo(commandV);

ret=1;

}

return ret;

}

void step1()

{

redir=NoneRedir;

filename=NULL;

//1.打印提示符wzs@VM-16-10-ubuntu:~/ubuntucode/shell$

printf("%s@VM-16-10-ubuntu:%s$ ",getenv("USER"),getenv("PWD"));

}

void step3(char* command)

{

//3.检查重定向

CheckRedir(command);

//4.解析字符串

char* commandV[1024]={ NULL};

CommandLineSplit(command,commandV);

//5.分析内建命令

int ret=doBulidIn(commandV);

if(ret==0)

{

//6.进程程序替换

Exec(commandV);

}

}

int CheckPipe(char* command,vector<int>& v)//返回管道个数,并且把|的位置填到数组当中

{

int len=strlen(command);

for(int i=0;i<len;i++)

{

if(command[i]=='|')

{

v.push_back(i);

command[i]='\0';//直接截断

}

}

return v.size();

}

void Execpipe(char* command)

{

vector<int> pos(1,-1);//为了后续分割方便

int sz=CheckPipe(command,pos);

if(sz==1)

{

step3(command);

return;

}

vector<int> child_id;

vector<pair<int,int>> v;//v[i].first:i号管道的读 second:写

for(int i=0;i<sz-1;i++)//创建sz个进程,sz-1个管道

{

int pipefd[2];

int n=pipe(pipefd);

pid_t id=fork();

if(id==0)

{

close(pipefd[0]);//关闭当前管道的读

int prev_fd_sz=v.size();

for(int j=0;j<prev_fd_sz;j++)

{

if(j<prev_fd_sz-1)

{

//把前面打开的读都关上(除了最近的那一个管道)

close(v[j].first);

}

//把前面打开的管道的写都关上

close(v[j].second);

}

dup2(pipefd[1],1);//当前管道的写重定向到我的1

if(!v.empty()) dup2(v.back().first,0);//上一个管道的读重定向到我的0

step3(command+pos[i]+1);

exit(0);

}

else

{

v.push_back({ pipefd[0],pipefd[1]});

child_id.push_back(id);

}

}

//最后一个进程:

pid_t id=fork();

if(id==0)

{

int prev_fd_sz=v.size();

for(int j=0;j<prev_fd_sz;j++)

{

if(j<prev_fd_sz-1)

{

//把前面打开的读都关上(除了最近的那一个管道)

close(v[j].first);

}

//把前面打开的管道的写都关上

close(v[j].second);

}

dup2(v.back().first,0);

step3(command+pos[sz-1]+1);

exit(0);

}

child_id.push_back(id);

for(auto& e:v)

{

//最后的时候父亲关闭所有的读写端

close(e.first);close(e.second);

}

for(auto& e:child_id)

{

pid_t rid=waitpid(e,nullptr,0);

if(rid>0) cout<<"wait success, pid: "<<rid<<endl;

}

}

int main()

{

while(1)

{

step1();

char command[1024]={ '\0'};

fgets(command,sizeof(command),stdin);

int len=strlen(command);

command[len-1]='\0';

Execpipe(command);

}

return 0;

}

以上就是Linux 进程间通信 管道系列: 匿名和命名管道,自定义shell当中命令行管道的模拟实现的全部内容,希望能对大家有所帮助!!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。