资讯动态

【Linux】进程间通信

发布时间:2026/8/19 23:00:09 来源:尧图企业网站定制
目录一、进程间通信1.1 为什么要进行进程间通信1.2 如何进行进程间通信1.3 管道1.3.1 什么是管道1.3.2 匿名管道1.4 深入理解管道1.5 系统调用 pipe1.6 代码验证1.7 一个进程控制一批进程个人主页矢望个人专栏C、Linux、C语言、数据结构、Coze-AI一、进程间通信我们之前学习进程的时候知道进程之间的独立性很强各自运行在隔离的内存空间中。那么如果进程之间要进行协同工作时该怎么办呢 这时候就需要进行进程间通信来完成。之前我们知道父子进程的数据是可以共享的父进程的数据子进程是可以看到的那么为什么还要有进程间通信呢 因为父子进程之间存在写时拷贝所以父子进程之间是无法进行相互传递数据的。1.1 为什么要进行进程间通信简单来说进程间通信是为了让独立的进程能够安全、可控地交换数据和协同工作。数据传输一个进程需要将它的数据发送给另一个进程。资源共享多个进程之间共享同样的资源。通知事件一个进程需要向另一个或一组进程发送消息通知它们发生了某种事件比如进程终止时要通知父进程。进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。数据传输解决了物质交换资源共享解决了空间利用通知事件解决了行动协调进程控制解决了集中管理。1.2 如何进行进程间通信进程进程间通信的本质是要让不同的进程看到同一份资源也就是不同的进程要看到同一份内存块。进程之间是具有独立性的进程A中进程B中自己的资源它们相互看不到。所以这个资源这个内存块只能有操作系统来提供而进程是不能直接从操作系统中获取数据的所以要进行进程间通信肯定有对应的系统调用。所以在精确一点进程间通信的本质就是通过操作系统提供的系统调用让隔离的进程能够安全地访问同一份由内核管理的资源从而打破独立性带来的壁垒实现数据交换和协同工作。1.3 管道1.3.1 什么是管道管道是类Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个管道。1.3.2 匿名管道现在呢我们有一个进程它在内存中打开了一个磁盘上的文件操作系统将它加载到了内存中这个文件有自己的struct file结构体有自己的文件描述符。后来这个进程fork()创建了子进程所以这个子进程就需要以父进程为模版进行子进程PCB的创建工作将task_struct和文件描述符表都复制过去了那么struct file结构体要不要复制呢 它是要的只不过struct file结构体指向的文件内容(如inode操作表文件内核级缓冲区)不需要给子进程拷贝。如下图所示。所以父子进程现在同时指向了同一个资源那么将来子进程向这个文件的内核级缓冲区写数据父进程就可以看到了。这就满足了形成进程间通信的基本条件。所以管道是基于文件进行内核级进程间通信。当然两个进程之间进行通信它们的通信内容能够传输即可不需要向磁盘进行写入也不需要磁盘这个角色同时也不需要文件的多余结构的出现它只是需要一块内核级缓冲区。所以真实的情况是这样的。实际上上面子进程的struct file结构体也不需要复制父进程fork()后不需要复制struct file结构体本身。内核的做法是将父进程的文件描述符表整张复制给子进程这使得子进程的描述符指向了内核中同一个struct file对象。同时内核会将这个struct file的引用计数增加1。这样只有当所有指向它的文件描述符都关闭时这个内核对象才会被回收。需要我们注意的是管道是单向通信的也叫做单工通信。补充单工通信是一方讲另一方听例如看视频全双工通信是两方同时在讲同时在听例如吵架的时候半双工通信是一方讲的时候另一方在听两方的角色可以互换是人类的正常沟通模式。由于只能进行单向通信所以我们把这种通信模式叫做管道。由于不需要磁盘文件也就不需要路径文件名找文件所以我们才把这种管道叫做匿名管道。所以管道是一个内存级的文件不需要打开磁盘文件之类的没有路径也不需要路径名所以它是匿名管道。1.4 深入理解管道如上就是管道的创建部分很容易看懂。首先父进程以同时读写的方式打开匿名管道然后父进程fork()创建子进程子进程也就以同时读写的方式同时看到文件了这样在依据读写的场景让父子进程合理的关掉读或写端这样就形成了单向信道能够进行通信了。那么父进程为什么要以同时读写的方式打开一个文件呢以只读或者只写不行吗如果是只读或只写那么之后子进程也和父进程拥有的读写端一样都是只读或只写那这样是无法进行通信的呀所以同时也读写的方式打开一个文件是为了方便让子进程也能同时看到读写。为什么最后要关掉对应的读写端呢其实不关闭也可以的只不过它容易进行误操作另一个你不适用的一端所以建议关闭掉对应的读写端。所以看待管道就像看待文件一样管道的使用和文件一致这也迎合了Linux下一切皆文件的思想。1.5 系统调用 pipe那么上面说的操作具体要怎么做呢 那就需要用到系统调用pipe了。这个系统调用的参数是输出型参数当你调用系统调用打开一个文件的时候它的读写端会被返回其中fd[0]一般存储的是读端r的文件描述符fd[1]一般存储的是写端w的文件描述符。关于它的返回值成功返回0失败返回-1并设置错误码。1.6 代码验证我们要实现的是子进程执行的是写入工作w父进程执行的是读取工作r。说明一下接下来写代码的平台是VSCode云服务器。要进行代码验证首先要执行的工作就是创建管道文件。先完成一下Makefile。#includestdio.h#includeunistd.hintmain(){intpipefd[2]{0};// 创建一个数组接收读写端的文件描述符intrpipe(pipefd);if(r0)// 如果执行失败{perror(pipe);return1;}// 打印输出两个文件描述符printf(pipefd[0]: %d, pipefd[1]: %d\n,pipefd[0],pipefd[1]);return0;}如上上面的代码简单使用pipe系统调用创建出了管道文件接下来我们编译运行看看文件的读写端的文件描述符。编译运行如上文件描述符0、1、2被占用我们形成的管道文件占用3、4符合预期。形成匿名管道之后接下来就是fork创建子进程让子进程执行写工作父进程进行读工作。也就是子进程需要关闭pipefd[0]而父进程需要关闭pipefd[1]。这里我们要将需要给父进程发送的消息写入到一个数组中然后再将信息写入到管道文件中让父进程时刻进行读取。所以我们需要用到snprintf、write、read。下面是创建子进程并进行通信的核心代码。// 创建子进程pid_t idfork();if(id0){// 子进程close(pipefd[0]);charoutbuffer[1024];constchar*chello world!;intcnt9;while(cnt){// 将消息写到数组中再写入管道中snprintf(outbuffer,sizeof(outbuffer),子-父%s, cnt: %d, pid: %d\n,c,cnt--,getpid());write(pipefd[1],outbuffer,strlen(outbuffer));sleep(1);}exit(0);}// 父进程close(pipefd[1]);while(1){charinbuffer[1024];ssize_t nread(pipefd[0],inbuffer,sizeof(inbuffer)-1);// 给 \0 留出一个位置if(n0){inbuffer[n]\0;printf(%s,inbuffer);}elseif(n0)// 读到文件结尾{printf(read pipe end of file!\n);break;}else{perror(read);break;}}如上当读端读完管道的数据后再读就会读到空表明读到结尾返回值n就是0了。编译运行如上图父子进程通过匿名管道通信成功了父进程读到了来自子进程发送的数据。总结匿名管道只能用来单向通信(单工通信)。我们看到一个进程fork创建出子进程和这个进程有血缘关系的进程都能看到同一个文件描述符表也就是都能看到同一份匿名管道所以匿名管道只能用来进行具有血缘关系的进程之间的通信。我们在Linux下运行的一条条指令最终都会变成进程指令使用|连接本质就是具有血缘关系的进程通过匿名管道进行通信。如下。三个指令最后就变成了三个具有血缘关系(兄弟关系)的进程通过匿名管道进行通信它们的父进程都是bash进程。管道是面向字节流的。管道不管你写的是什么数据字符串也好图片也好它都可以所以管道是面向字节的。面向字节流最核心的含义是写入和读取的次数没有必然联系数据之间没有天然的间隔或边界。上面我们的子进程写入和父进程的读取都是很有规律的所以我们感知不出来现在我们变动一下。// 创建子进程pid_t idfork();if(id0){// 子进程close(pipefd[0]);charoutbuffer[1024];char*chello world!;intcnt20;while(cnt){// 将消息写到数组中再写入管道中snprintf(outbuffer,sizeof(outbuffer),子-父%s, cnt: %d, pid: %d\n,c,cnt--,getpid());write(pipefd[1],outbuffer,strlen(outbuffer));if(cnt%30)// 打乱子进程的输入速率sleep(1);}exit(0);}// 父进程close(pipefd[1]);charinbuffer[1024];intsum0;while(1){// 打乱父进程的读取速率sum;if(sum%3!0){sleep(1);continue;}ssize_t nread(pipefd[0],inbuffer,sizeof(inbuffer)-1);// 给 \0 留出一个位置if(n0){inbuffer[n]\0;printf(%s\n,inbuffer);// 添加换行符区分每次的读取}elseif(n0)// 读到文件结尾{printf(read pipe end of file!\n);break;}else{perror(read);break;}}如上我们将它们的写入和读取的速率都变一下看看是什么结果。编译运行如上并不是说子进程写一条消息父进程就要读一条。写十次不一定非要读十次。简而言之管道就像一根水管数据像水流一样在里面连续流动。发送方可以一杯一杯地倒水接收方可以一桶一桶地接水。只要保证数据的先后顺序不变双方完全不需要协调倒水和接水的节奏与分量。这正是它被称为流的原因它像水流一样连续、无边界、有序且读写完全异步。所以管道是面向字节流的。那么当父进程不再从匿名管道中读数据子进程不再向匿名管道中写数据这个匿名管道会释放吗对于文件而言它的inode属性中有一个引用计数的东西当有一个进程指向它它的引用计数就要加一当它的引用计数降到零时它就要被释放掉了所以打开的文件的生命周期随进程。而管道也是文件所以管道的生命周期随进程。如果子进程写的慢那么父进程会如何呢// 创建子进程pid_t idfork();if(id0){// 子进程close(pipefd[0]);charoutbuffer[1024];char*chello world!;intcnt20;while(cnt){sleep(10);// 将消息写到数组中再写入管道中snprintf(outbuffer,sizeof(outbuffer),子-父%s, cnt: %d, pid: %d\n,c,cnt--,getpid());write(pipefd[1],outbuffer,strlen(outbuffer));}exit(0);}// 父进程close(pipefd[1]);charinbuffer[1024];intsum0;while(1){ssize_t nread(pipefd[0],inbuffer,sizeof(inbuffer)-1);// 给 \0 留出一个位置if(n0){inbuffer[n]\0;printf(%s\n,inbuffer);// 添加换行符区分每次的读取}elseif(n0)// 读到文件结尾{printf(read pipe end of file!\n);break;}else{perror(read);break;}}如上子进程每隔十秒向管道写一条数据。编译运行如上运行结果显示父进程等了十秒获取了子进程写的一条数据也就是父进程在等待子进程也就是父进程阻塞住了那么为什么呢管道通信对于多进程而言是带有互斥与同步机制的。管道通信的前提是让不同的进程看到同一份资源(内存块)如果子进程在向管道中写数据它才写了一半父进程就瞬间将这一半数据读走了那么此时就出现问题了这叫做由于并发访问所导致的问题。而什么是互斥呢 就是任何时刻只允许一个人访问资源。同步是访问数据具有一定的顺序性而不是它们同时去争夺一份资源。如何证明带有互斥和同步机制呢对于互斥我们可以同时创建三个进程让它们同时向一个管道文件写数据观察父进程读出的数据是不是乱的。如果完整且有序说明每条消息内部的数据是连续的不会被其他进程的数据打断。这就是互斥机制。#includestdio.h#includeunistd.h#includesys/wait.h#includestring.hintmain(){intpipefd[2];pipe(pipefd);// 创建3个子进程同时写入for(inti0;i3;i){if(fork()0){close(pipefd[0]);// 子进程关闭读端charmsg[100];// 每个子进程写50次每次写入固定格式的字符串for(intj0;j50;j){snprintf(msg,sizeof(msg),进程%d的第%d条消息\n,i,j);write(pipefd[1],msg,strlen(msg));// 故意不加延时让它们疯狂写入}exit(0);}}// 父进程关闭写端开始读取close(pipefd[1]);charbuffer[1024];ssize_t n;while((nread(pipefd[0],buffer,sizeof(buffer)-1))0){buffer[n]\0;// 直接打印观察消息是否完整printf(%s,buffer);}// 等待所有子进程结束while(wait(NULL)0);return0;}编译运行如上父进程读到的数据是完整且有序的证明了互斥机制。证明同步机制让子进程一直写父进程不读。匿名管道是有大小的如下可以通过man 7 pipe查看。上面主要说了原子性边界PIPE_BUF是内核保证单次write操作原子性的最大尺寸。在你的代码中每次写入的消息长度远小于4096字节所以内核保证每次写入都是一次性完整完成的这正是管道互斥机制的体现。非原子情况如果一次写入超过4096字节内核可能会将其拆分成多个数据块进行传输。这时如果有多个进程同时写入这些数据块就可能相互交织导致接收方读到乱序或交错的数据。多种影响因素最终行为还取决于是否设置了非阻塞标志、并发写入的进程数量以及具体的数据量。上面说的我们先不管它说了管道的大小是65536字节也就是64KB。接下来我们让父进程休眠一段时间子进程疯狂写并把写的字节数打印出来。// 创建子进程pid_t idfork();if(id0){// 子进程close(pipefd[0]);charchc;intsize0;while(1){write(pipefd[1],ch,1);size;printf(%d\n,size);}exit(0);}// 父进程close(pipefd[1]);charinbuffer[1024];intsum0;while(1){sleep(100);ssize_t nread(pipefd[0],inbuffer,sizeof(inbuffer)-1);// 给 \0 留出一个位置if(n0){inbuffer[n]\0;printf(%s\n,inbuffer);// 添加换行符区分每次的读取}elseif(n0)// 读到文件结尾{printf(read pipe end of file!\n);break;}else{perror(read);break;}}编译运行如上子进程写满匿名管道之后就停下来了子进程阻塞了。并且我们看到管道的大小的确是65536字节也就是64KB。管道为空读端阻塞给写机会管道写满写端阻塞给读机会。这就是同步。如果写端一直写但读端不读并且读端还把相关读的文件描述符关闭了那么写端会怎样// 创建子进程pid_t idfork();if(id0){// 子进程close(pipefd[0]);charoutbuffer[1024];char*chello world!;intcnt20;while(cnt){sleep(10);// 将消息写到数组中再写入管道中snprintf(outbuffer,sizeof(outbuffer),子-父%s, cnt: %d, pid: %d\n,c,cnt--,getpid());write(pipefd[1],outbuffer,strlen(outbuffer));}exit(0);}// 父进程close(pipefd[1]);close(pipefd[0]);sleep(10);intstatus0;pid_t ridwaitpid(id,status,0);if(rid0){printf(quit_code: %d, signal: %d\n,(status7)0xFF,status0x7F);}上面代码我们获取了子进程的退出信息包括退出码和退出信号。编译运行如上退出信号是13它是什么呢当一个进程试图向一个读端已经被关闭的管道写入数据时内核会向该写入进程发送SIGPIPE信号。也就是管道破裂没有读端写端继续写OS终止进程。1.7 一个进程控制一批进程那么将来一个进程可以提前fork创建出一批子进程由父进程担任各个管道的写端由子进程担任各个管道的读端当父进程没有写入数据时由上面的互斥规则所以子进程就会阻塞在那里。当父进程向管道中写入数据时子进程就会被唤醒读取父进程发送的内容。如上图父进程不写时子进程都在待命。将来父进程可以通过管道向子进程派发一个个任务让子进程去执行这种模式叫做Master Slaver主从模式。这就是主从模式的进程池所有的池化技术都是为了提高效率父进程提前创建出一批子进程然后当有任务要执行时就立刻交给子进程执行而不是有任务时再fork创建出子进程让子进程去执行这就提高了效率。总结以上就是本期博客分享的全部内容啦如果觉得文章还不错的话可以三连支持一下你的支持就是我前进最大的动力技术的探索永无止境! 道阻且长行则将至后续我会给大家带来更多优质博客内容欢迎关注我的CSDN账号我们一同成长(▽)

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价