10 进程间通信
进程是资源分配的单元,不同进程(指用户进程 不是内核进程)之间的资源是独立的没有关联的,不能在一个进程中直接访问另一个进程的资源。
但是进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信IPC
进程间通信的目的:
数据传输:一个进程将它的数据发给另一个进程
通知时间:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时需要通知父进程)。
资源共享:多个进程之间共享同样的资源,为了做到这一点,需要内核提供互斥和同步机制
进程控制:有些进程希望完全控制另一个进程的执行(如DeBug进程 GDB进程 控制被GDB调试的那个进程),此时控制进程能个拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
Linux 进程间通信的方式
![]() |
|---|
11匿名管道概述
匿名管道,它是UNIX系统IPC进程间通信的最古老形式,所有的UNIX系统都支持这种通信机制
比如 统计一个目录中文件的数目命令 ls | wc -l 为了执行该命令,shell创建了两个进程来分别执行ls和wc
![]() |
|---|
ls获取当前目录的文件列表 |叫管道符 wc文件统计个数
ls进程将文件列表通过管道发给wc进程
ls默认是将文件列表显示到终端(即 将信息写到fd=1的文件中 对应的是stdout标准输出 输出到终端屏幕),管道有写入端和读取端,当加上|管道符后ls写的文件 fd=1 stdout指向的不再终端屏幕而是管道的写入端,wc同理wc读的文件 fd=0 stdin指向的不再终端屏幕而是管道的读入端。
fd=0 stdin默认指向终端屏幕 fd=1stdout默认指向的也是终端屏幕
管道的特点
管道其实是一个在内核内存中维护的缓冲区,这个缓冲区的大小是有限的,不同操作系统的管道大小不一定相同。
管道拥有文件的特质:读、写 管道的两端对应了两个文件描述符(写入端文件描述符,读取端文件描述符),匿名管道是没有文件实体的但是有名管道是有文件实体的(但这个文件是不存储数据的),可以按照造作文件的方式对管道进行操作。
匿名管道只能用于有公共祖先的进程之间比如父进程和子进程,爷爷进程和孙子进程,兄弟进程之间。
有名管道用于无关系的进程之间。
一个管道是一个字节流,使用管道时不存在消息或消息边界的概念(就是没有一个数据帧的头尾内容这些概念 就是单纯的字节),从管道读取数据的进程可以读取任意大小的数据块,而不管写进程写入的数据块的大小是多大(读写分离 互不影响)。
通过管道传递的数据是顺序的,管道实际为环形队列实现 先入先出 读取出来的字节的顺序和它们被写入管道的顺序完全一样。
在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工(遥控器单向发送 单工 电话通信 双工同时传递消息 可以同时读也可以同时写 也可以一个读一个写 对讲机 半双工 同一时间只能 读或写 不能同时读或同时写 在同一时刻内数据的流动是单向的,但是可以在一个时刻选择数据的流动方向)。
从管道读数据是一次性操作,数据一旦被读走它就从管道中被抛弃(pop),释放空间以便写更多的数据,在管道中无法使用lseek()来随机访问数据。
![]() |
|---|
为什么可以使用管道进行通信
为什么只有拥有公共祖先的进程可以通过匿名管道通信
fork后的与祖先进程相关的进程的文件描述符表(在PCB中)是相同的。父进程的3号fd是指向A文件的,4号fd是指向B文件的(在fork前就已经将3,4号fd的指向明确了),fork后的子进程的3号fd也是指向A文件的,4号fd也是指向B文件的。父子进程都可以通过fd操作同一文件,这样其实就可以完成进程间通信了,一个读一个写。匿名管道的原理和操作同一文件实现通信的原理是一样的,比如父进程的5号fd对应管道的写端,子进程的5号fd对应管道的写端,父进程的6号fd对应管道的读端,子进程的6号fd对应管道的读端(fork前的父进程已经将fd5,6的指向明确了),则fork后父子进程可以通过匿名管道通信。
![]() |
|---|
(fork后的父子进程文件描述符表是独立的(fork的时候 相当于为子进程初始化一份一摸一样的文件描述符表,但fork后想怎么改都随便),不需要完全相同,根据需要来让父子fd指向需要的文件)
1、内核缓冲区2、非文件3、不占用磁盘空间4、两部分:读端和写端5、默认是阻塞的6、操作管道进程结束后,自动释放7、半双工通信8、默认缓冲区是 4k
1、父进程和子进程可以共享打开的文件描述符。
2、父子进程共享文件描述符的条件:在fork之前打开文件。(两个进程的文件描述符表其实还是独立的,只不过fork子进程的时候给子进程的文件描述符表初始化为与父进程一摸一样)
3、对于两个完全不相关的进程,文件描述符不能共享。
4、父子进程文件描述符是共享的,但是关闭的时候可以分别关闭,也可以同时在公有代码中关闭。
文件表,也就是vnode数据结构,也可以说是内核对象,它里面有个计数器变量,表示有几个进程打开此文件,所以子进程关闭,父进程仍然可以读数据
![]() |
|---|
进程A的fd1和fd20这种就是dup类系统调用的结果
进程A的fd2和进程B的fd2就是fork的结果(fork之后指向相同的文件)
进程A的fd0和进程B的fd3最终指向同一个inode,这是这两个进程都调用过open的结果,此时两个文件不共享文件内的偏移
文件偏移是存放在第二个表中的,所以不管是dup还是fork,都是共享同一组偏移,
左边的是进程级别的,中间的是系统级别的,记录整个系统中打开的文件,inode可以理解成存在硬盘上的文件
匿名管道的底层实现为环形队列
![]() |
|---|
写指针可以写空白处(比如E-U)和读指针已经读过的部分(比如ABCD)(读完了以后就会被写指针的写数据覆盖 所以读管道相当于pop读完以后就没有了)
创建匿名管道
#include
int pipe(int pipefd[2]);
查看管道缓冲大小
ulimit - a
查看管道缓冲大小函数
#include
long fpathconf(int fd,int name);
12父子进程通过匿名管道通信
/*#include <unistd.h>int pipe(int pipefd[2]);创建一个匿名管道(半双工 同一时间内一段只能写或读)用于有共同祖先线程的线程间通信pipefd 传出参数 返回的管道两端 对应的文件描述符号[0]为读端 [1]为写端写数据到写端 会先存到内核中的缓冲区 直到读端读数据将这个数据取出(取出了就不在管道中了)返回 0 成功 -1 失败匿名管道默认阻塞 如果管道中没有数据 read阻塞(直到管道中有要求被读的那么多个字节 才不会阻塞 管道内数据被读出)管道满了 write阻塞(直到管道被读出要写的那么多个字节 腾出了那么大的空间才能继续写)*/#include <unistd.h>#include <sys/types.h>#include <stdio.h>#include <stdlib.h>#include <string.h> //strlenint main(){//父子进程通信//在fork之前创建匿名管道 创建出来的fd都存在父进程的文件描述符表中//fork之后 子进程的文件描述符表和父进程一摸一样 包括读写端的fd操作符//这样对于父子进程来说管道的读写fd指向是一样的 也就可以通过管道进行通信int pipefd[2];int ret = pipe(pipefd);if (ret == -1){perror("pipe:");exit(-1);}//创建子进程pid_t pid = fork();//发送一次// if (pid > 0) //父进程// {// // 父进程从读取端读取数据// char buf[1024] = {0};// int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据// //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来// printf("parent recv :%s,pid : %d \n", buf, getpid());// }// else if (pid == 0) //子进程// {// //子进程向管道发数据// char *str = "im child";// write(pipefd[1], str, strlen(str));// //如果管道满了 无法再写write是阻塞的 直到read读出的字节数大于要写的字节数 write才能继续写// // 成功返回str中被写入fd文件的字节数,返回0表示没有数据被写入fd文件中, 出现错误返回 - 1并设置errorno// }//循环发送和读 一端只发 一端只收// if (pid > 0) //父进程// {// // 父进程从读取端读取数据// char buf[1024] = {0};// while (1)// {// int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据// //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来// printf("parent recv :%s,pid : %d \n", buf, getpid());// }// }// else if (pid == 0) //子进程// {// //子进程向管道发数据// while (1)// {// //每1秒向管道写入一次// char *str = "im child";// write(pipefd[1], str, strlen(str));// sleep(1);// }// //如果管道满了 无法再写write是阻塞的 直到read读出的字节数大于要写的字节数 write才能继续写// // 成功返回str中被写入fd文件的字节数,返回0表示没有数据被写入fd文件中, 出现错误返回 - 1并设置errorno// }//一端又发又收 但同一时间内只收或只发if (pid > 0) //父进程{// 父进程从读取端读取数据char buf[1024] = {0};while (1){int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据//read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来printf("parent recv :%s,pid : %d \n", buf, getpid());memset(buf, 0, sizeof(buf)); //读后记得清除读的buf 否则会有上次的数据残留导致下次读的数据有问题//每1秒向管道写入一次char *str = "im parent";write(pipefd[1], str, strlen(str));sleep(1);}}else if (pid == 0) //子进程{//子进程向管道发数据char buf[1024] = {0};while (1){//每1秒向管道写入一次char *str = "im child";write(pipefd[1], str, strlen(str));sleep(1);int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据//read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来printf("child recv :%s,pid : %d \n", buf, getpid());memset(buf, 0, sizeof(buf)); //读后记得清除读的buf 否则会有上次的数据残留导致下次读的数据有问题}}//如果父子进程都是既要读又要写 就需要把读写的顺序错开 一个读一个写//不能出现同时读的情况 这样程序就阻塞了跑不起来//如果将两个线程中的sleep去掉 会出现parent recv:im parent 和 child recv:im child//出现这种情况的原因是//父进程写的 被父进程读了(父进程比子进程先读) 子进程写的 被子进程读了(子进程比父进程先读)//在实际开发中不可能通过sleep来阻止这种现象//在实际开发中匿名管道的数据流向是单向的(一个线程只读 一个线程只写 关闭另一个fd close(pipefd[0]/[1]))!!!!!!!!return 0;}//ulimit -a 中有pipe isze 512bytes 8 表示共8块一块512字节 总共4k字节 管道最大4k字节//ulimit -p 修改管道大小// 查看管道缓冲大小函数// #include <unistd.h>// long fpathconf(int fd, int name);// fd 传入管道一端对应的文件描述符 name:_PC_PIPE_BUF// 返回fd对应的大小
13匿名管道通信案例
/*实现ps aux | grep root 只显示root进程相关的信息| 管道符 利用父子进程通信实现子进程执行ps aux(execlp 默认将内容输出到标准输出中) 因为子进程的标准输出初始为屏幕所以要将 标准输出重定向到父进程(即管道写端),这样父进程才能得到execlp的返回值父进程 获取到数据 将数据打印*/#include <unistd.h>#include <sys/types.h>#include <stdio.h>#include <stdlib.h>#include <string.h> //strlenint main(){//创建一个匿名管道int fd[2];int ret = pipe(fd);if (ret == -1){perror("pipe:");exit(-1);}//一定要在fork子进程前 创建管道 这样才能使父子进程拥有相同的管道描述符pid_t pid = fork();if (pid > 0){//父进程 从管道中读取数据 再直接输出//父进程只读 关闭管道写端close(fd[1]);char buf[1024] = {0};int len = read(fd[0], buf, sizeof(buf) - 1); //buf中只读sizeof(buf) - 1个字节 因为最后一个字节必须是字符串结束符\0(十进制就是0)while (len > 0){printf("%s", buf);memset(buf, 0, sizeof(buf));len = read(fd[0], buf, sizeof(buf) - 1); //返回值len为读到的字符数}wait(NULL); //回收子进程资源}else if (pid == 0){//子进程// 子进程执行ps aux(execlp 默认将内容输出到标准输出中) 因为子进程的标准输出初始为屏幕// 所以要将 标准输出重定向到父进程(即管道写端) ,这样父进程才能得到execlp的返回值//子进程只写 关闭管道读端close(fd[0]);//首先重定向子进程的标准输出到管道的写端 stdout_fileno->fd[1]//int dup2(int oldfd, int newfd); //功能和dup类似//用于重定向文件描述符//oldfd指向a.txt newfd指向b.txt 在调用函数成功后将newfd->b.txt关闭并且再让newfd指向a.txtdup2(fd[1], STDOUT_FILENO); //newfd指向a.txt 这样STDOUT_FILENO就指向之前管道写端对应的那个虚拟文件// int execlp(const char *file, const char *arg, ... / (char *) NULL /);// 会到环境变量中的路径中查找指定的同名文件 找到了就执行 找不到返回 - 1// 参数:// file 可执行文件的文件名 a.out ps 可以不用写绝对路径 直接写文件名// arg(同execl)// 这个arg其实就是exec可执行文件的main的传入参数列表// 第一个arg一般没有作用 一般写的是执行的可执行文件的名称// 从第二个参数往后才是可执行文件main的真正参数列表// 这个参数列表一定要以NULL为结尾,这样才知道参数列表读入完毕// 返回值execlp("ps", "ps", "aux", NULL);//exec函数族的函数执行成功后不会返回,因为调用进程的实体,// 包括代码段数据段堆栈等(所有用户区内容)都被新的可执行文件的内容取代//但是文件描述符表在内核中并不会被替换}else{perror("fork:");exit(-1);}// 管道默认大小 4k字节// 子进程写管道,写满了就会被阻塞(这里的写是指调用write),然后轮到父进程读管道,// 所以子进程不需要while循环也可以把所有的超过4k的数据写完// execlp("ps", "ps", NULL)不是write ,它不可能管道写满了还阻塞着// 超过4096的数据是会分包,还是可以发送的。TLPI书上也说了,”写入不超过4096字节” 的操作保证是原子的// Linux 2.6.11后,管道的容量是65536,不同的操作系统实现可能不同。所以最后我试了一下,一次write() 确实最多只能写65536字节。记得将管道设置为非阻塞,才能看到无一次法写入大于65536字节的数据。// 不然,write是阻塞的,发一部分,阻塞一下,发一部分。。。多大都会发送完。return 0;}// #include <stdio.h>// #include <sys/types.h>// #include <sys/stat.h>// #include <fcntl.h>// #include <unistd.h>// #include <cstring>// #include <string>// #include <stdlib.h>// #include <limits.h>// #define BUF_SIZE 65544// int main()// {// int pipeFd[2];// if (pipe(pipeFd) == -1)// {// perror("pipe");// exit(-1);// }// // 设置管道为非阻塞// for (int i = 0; i < 2; ++i)// {// int flags = fcntl(pipeFd[i], F_GETFL);// if (fcntl(pipeFd[i], F_SETFL, flags | O_NONBLOCK) == -1)// perror("fcntl");// }// int pid = fork();// if (pid > 0)// {// sleep(1); // 确保子进程先写。管道才有数据能读// // 父进程读// close(pipeFd[1]);// char buf[BUF_SIZE + 1] = {0};// printf("父进程读到了:%ld字节\n", read(pipeFd[0], buf, BUF_SIZE));// }// else if (pid == 0)// {// // 子进程// close(pipeFd[0]);// // in.txt有65544字节的数据// int fd = open("in.txt", O_RDONLY);// char buf[BUF_SIZE + 1] = {0};// printf("从文件中读到: %ld字节\n", read(fd, buf, BUF_SIZE));// printf("子进程写入了:%ld字节\n", write(pipeFd[1], buf, BUF_SIZE));// }// else// {// perror("fork");// exit(-1);// }// return 0;// }
管道的读写特点以及将管道设置为非阻塞
使用管道时需要注意以下几种特殊情况(管道为阻塞 在执行阻塞IO的操作时),
1 所有的指向管道写端的文件描述符 都关闭了(管道写端的引用计数为0)
父进程创建管道,其内核区中的文件描述符表中新增两个fd一个指向管道读端一个指向管道写端,此时管道写端的引用计数为1。父进程fork出子进程,fork拷贝文件描述符表给子进程,所以子进程的文件描述符表中也有两个fd一个指向读端一个指向写端,此时管道写端的引用计数为2。
当指向管道写端的fd都close了,此时有进程从管道读端读数据,若此时管道中有剩余数据则读取剩余数据,直到剩余数据被读完后,再次read返回0(读到文件末尾read是返回0的 读出现异常是返回-1的)。
2 有指向管道写端的文件描述符没有关闭(管道写端的引用计数大于0)并且没有进程向管道写端写数据,此时将管道内数据读空后再继续读是出现read阻塞,直到有进程写了一些数据,read读到返回读到的字节数。
3 所有的指向管道读端的文件描述符 都关闭了(管道读端的引用计数为0),此时有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常这个信号会导致进程异常终止。
4 有指向管道读端的文件描述符没有关闭(管道读端的引用计数大于0)并且没有进程从管道读端读数据,此时有进程向管道中写数据,在管道写满后,再次调用写write会导致写进程阻塞,直到管道中有空位write才不会阻塞并返回写入的字节数。
设置管道非阻塞(设置文件描述符非阻塞)
设置管道读端非阻塞,在管道内无数据再读 read返回值为-1
/*设置管道非阻塞(设置文件描述符fd 非阻塞)flags = fcntl(fd[],F_GETFL) 获取原来的flagflags |= O_NONBLOCK;fcntl(fd[],F_SETFL,flags);//设置新的文件描述符属性*/#include <unistd.h>#include <fcntl.h>#include <sys/types.h>#include <stdio.h>#include <stdlib.h>#include <string.h> //strlenint main(){//父子进程通信//在fork之前创建匿名管道 创建出来的fd都存在父进程的文件描述符表中//fork之后 子进程的文件描述符表和父进程一摸一样 包括读写端的fd操作符//这样对于父子进程来说管道的读写fd指向是一样的 也就可以通过管道进行通信int pipefd[2];int ret = pipe(pipefd);if (ret == -1){perror("pipe:");exit(-1);}//创建子进程pid_t pid = fork();//一端又发又收 但同一时间内只收或只发if (pid > 0) //父进程{// 父进程从读取端读取数据char buf[1024] = {0};//父进程只读 关闭管道写端close(pipefd[1]);// 设置管道非阻塞(设置文件描述符fd 非阻塞)// 读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定//设置非阻塞读 当管道中没有要求的那么多的数据时 不会阻塞在read而是继续向下执行int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flagflags |= O_NONBLOCK; //设置文件(管道读端)为非阻塞fcntl(pipefd[0], F_SETFL, flags); //设置新的文件描述符属性while (1){int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据//read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来printf("parent recv :%s,pid : %d \n", buf, getpid());memset(buf, 0, sizeof(buf)); //读后记得清除读的buf 否则会有上次的数据残留导致下次读的数据有问题// //每1秒向管道写入一次// char *str = "im parent";// write(pipefd[1], str, strlen(str));// sleep(1);}}else if (pid == 0) //子进程{//子进程向管道发数据char buf[1024] = {0};//子进程只写 关闭管道读端close(pipefd[0]);while (1){//每1秒向管道写入一次char *str = "im child";write(pipefd[1], str, strlen(str));sleep(1);// int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据// //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来// printf("child recv :%s,pid : %d \n", buf, getpid());// memset(buf, 0, sizeof(buf)); //读后记得清除读的buf 否则会有上次的数据残留导致下次读的数据有问题}}//如果父子进程都是既要读又要写 就需要把读写的顺序错开 一个读一个写//不能出现同时读的情况 这样程序就阻塞了跑不起来//如果将两个线程中的sleep去掉 会出现parent recv:im parent 和 child recv:im child//出现这种情况的原因是//父进程写的 被父进程读了(父进程比子进程先读) 子进程写的 被子进程读了(子进程比父进程先读)//在实际开发中不可能通过sleep来阻止这种现象//在实际开发中匿名管道的数据流向是单向的(一个线程只读 一个线程只写 关闭另一个fd close(pipefd[0]/[1]))!!!!!!!!return 0;}






