Linux内核解析:进程间通信:管道

管道的定义
管道的用途
管道的操作
管道非法read与write内核实现解析
管道通信原理及其亲戚通信解析
父子进程通信解析
亲缘关系的进程管道通信解析
管道的注意事项及其性质
管道有以下三条性质
shell管道的实现
与shell命令进行通信
system函数与popen函数区别

管道的定义

  • 管道是第一个广泛应用的进程间通信手段。日常在终端执行shell命令时,会大量用到管道。但管道的缺陷在于只能在有亲缘关系(有共同的祖先)的进程之间使用。为了突破这个限制,后来引入了命名管道。

管道的用途

  • 管道是最早出现的进程间通信的手段。在shell中执行命令,经常会将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事情。而这就是通过管道来实现的。

    在图9-3中,进程who的标准输出,通过管道传递给下游的wc进程作为标准输入,从而通过相互配合完成了一件任务。

管道的操作

  • 管道的作用是在具有亲缘关系的进程之间传递消息,所谓有亲缘关系,是指有同一个祖先。所以管道并不是只可以用于父子进程通信,也可以在兄弟进程之间还可以用在祖孙之间等,反正只要共同的祖先调用了pipe函数,打开的管道文件就会在fork之后,被各个后代所共享
  • 不过由于管道是字节流通信,没有消息边界,多个进程同时发送的字节流混在一起,则无法分辨消息,所有管道一般用于2个进程之间通信,另外管道的内容读完后不会保存,管道是单向的,一边要么读,一边要么写,不可以又读又写,想要一边读一边写,那就创建2个管道,如下图

  • 管道是一种文件,可以调用read、write和close等操作文件的接口来操作管道。另一方面管道又不是一种普通的文件,它属于一种独特的文件系统:pipefs。管道的本质是内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。下面我们来看一下如何使用管道。
  1. #include<unistd.h>
  2. int pipe(int fd[2])

如果成功,则返回值是0,如果失败,则返回值是-1,并且设置errno。

成功调用pipe函数之后,会返回两个打开的文件描述符,一个是管道的读取端描述符pipefd[0],另一个是管道的写入端描述符pipefd[1]。管道没有文件名与之关联,因此程序没有选择,只能通过文件描述符来访问管道,只有那些能看到这两个文件描述符的进程才能够使用管道那么谁能看到进程打开的文件描述符呢?只有该进程及该进程的子孙进程才能看到。这就限制了管道的使用范围

  • 成功调用pipe函数之后,可以对写入端描述符pipefd[1]调用write,向管道里面写入数据,代码如下所示:
  1. write(pipefd[1],wbuf,count);

一旦向管道的写入端写入数据后,就可以对读取端描述符pipefd[0]调用read,读出管道里面的内容。如下所示,管道上的read调用返回的字节数等于请求字节数和管道中当前存在的字节数的最小值。如果当前管道为空,那么read调用会阻塞(如果没有设置O_NONBLOCK标志位的话)。

管道非法read与write内核实现解析

调用pipe函数返回的两个文件描述符中,读取端pipefd[0]支持的文件操作定义在read_pipefifo_fops,写入端pipefd[1]支持的文件操作定义在write_pipefifo_fops,其定义如下:

  1. const struct file_operations read_pipefifo_fops = { //读端相关操作
  2. .llseek = no_llseek,
  3. .read = do_sync_read,
  4. .aio_read = pipe_read,
  5. .write = bad_pipe_w, //一旦写,将调用bad_pipe_w
  6. .poll = pipe_poll,
  7. .unlocked_ioctl = pipe_ioctl,
  8. .open = pipe_read_open,
  9. .release = pipe_read_release,
  10. .fasync = pipe_read_fasync,
  11. };
  12. const struct file_operations write_pipefifo_fops = {//写端相关操作
  13. .llseek = no_llseek,
  14. .read = bad_pipe_r, //一旦读,将调用bad_pipe_r
  15. .write = do_sync_write,
  16. .aio_write = pipe_write,
  17. .poll = pipe_poll,
  18. .unlocked_ioctl = pipe_ioctl,
  19. .open = pipe_write_open,
  20. .release = pipe_write_release,
  21. .fasync = pipe_write_fasync,
  22. };

我们可以看到,对读取端描述符执行write操作,内核就会执行bad_pipe_w函数;对写入端描述符执行read操作,内核就会执行bad_pipe_r函数。这两个函数比较简单,都是直接返回-EBADF。因此对应的read和write调用都会失败,返回-1,并置errno为EBADF

  1. static ssize_t
  2. bad_pipe_r(struct file filp, char __user buf, size_t count, loff_t ppos)
  3. {
  4. return -EBADF; //返回错误
  5. }
  6. static ssize_t
  7. bad_pipe_w(struct file filp, const char __user buf, size_t count,loff_t ppos)
  8. {
  9. return -EBADF;
  10. }

管道通信原理及其亲戚通信解析

父子进程通信解析

我们只介绍了pipe函数接口,至今尚看不出来该如何使用pipe函数进行进程间通信。调用pipe之后,进程发生了什么呢?请看图9-5

可以看到,调用pipe函数之后,系统给进程分配了两个文件描述符,即pipe函数返回的两个描述符。该进程既可以往写入端描述符写入信息,也可以从读取端描述符读出信息。可是一个进程管道,起不到任何通信的作用。这不是通信,而是自言自语。

如果调用pipe函数的进程随后调用fork函数,创建了子进程,情况就不一样了。fork以后,子进程复制了父进程打开的文件描述符(如图9-6所示),两条通信的通道就建立起来了。此时,可以是父进程往管道里写,子进程从管道里面读;也可以是子进程往管道里写,父进程从管道里面读。这两条通路都是可选的,但是不能都选。原因前面介绍过,管道里面是字节流,父子进程都写、都读,就会导致内容混在一起,对于读管道的一方,解析起来就比较困难。常规的使用方法是父子进程一方只能写入,另一方只能读出,管道变成一个单向的通道,以方便使用。如图9-7所示,父进程放弃读,子进程放弃写,变成父进程写入,子进程读出,成为一个通信的通道…

  • 父进程如何放弃读,子进程又如何放弃写?其实很简单,父进程把读端口pipefd[0]这个文件描述符关闭掉,子进程把写端口pipefd[1]这个文件描述符关闭掉就可以了,示例代码如下:
  1. int pipefd[2];
  2. pipe(pipefd);
  3. switch(fork())
  4. {
  5. case -1:
  6. /fork failed, error handler here/
  7. case 0: /子进程/
  8. close(pipefd[1]) ; /关闭掉写入端对应的文件描述符/
  9. /子进程可以对pipefd[0]调用read/
  10. break;
  11. default: /父进程/
  12. close(pipefd[0]); /父进程关闭掉读取端对应的文件描述符/
  13. /父进程可以对pipefd[1]调用write, 写入想告知子进程的内容/
  14. break
  15. }

亲缘关系的进程管道通信解析

  • 图9-8也讲述了如何在兄弟进程之间通过管道通信。如图9-8所示,父进程再次创建一个子进程B,子进程B就持有管道写入端,这时候两个子进程之间就可以通过管道通信了。父进程为了不干扰两个子进程通信,很自觉地关闭了自己的写入端。从此管道成为了两个子进程之间的单向的通信通道。在shell中执行管道命令就是这种情景,只是略有特殊之处,其特殊的地方是管道描述符占用了标准输入和标准输出两个文件描述符

    管道的注意事项及其性质

管道有以下三条性质

  • 只有当所有的写入端描述符都已经关闭了,而且管道中的数据都被读出,对读取描述符调用read函数才返回0(及读到EOF标志)。
  • 如果所有的读取端描述符都已经关闭了,此时进程再次往管道里面写入数据,写操作将会失败,并且内核会像进程发送一个SIGPIPE信号(默认杀死进程)。
  • 当所有的读端与写端都已经关闭时,管道才会关闭.
  • 就因为有这些特性,我们要即使关闭没用的管道文件描述符

shell管道的实现

  • shell编程会大量使用管道,我们经常看到前一个命令的标准输出作为后一个命令的标准输入,来协作完成任务,如图9-9所示。管道是如何做到的呢?

    兄弟进程可以通过管道来传递消息,这并不稀奇,前面已经图示了做法。关键是如何使得一个程序的标准输出被重定向到管道中,而另一个程序的标准输入从管道中读取呢?

    答案就是复制文件描述符。

    对于第一个子进程,执行dup2之后,标准输出对应的文件描述符1,也成为了管道的写入端。这时候,管道就有了两个写入端,按照前面的建议,需要关闭不相干的写入端,使读取端可以顺利地读到EOF,所以应将刚开始分配的管道写入端的文件描述符pipefd[1]关闭掉。

  1. if(pipefd[1] != STDOUT_FILENO)
  2. {
  3. dup2(pipefd[1],STDOUT_FILENO);
  4. close(pipefd[1]);
  5. }

同样的道理,对于第二个子进程,如法炮制:

  1. if(pipefd[0] != STDIN_FILENO)
  2. {
  3. dup2(pipefd[0],STDIN_FILENO);
  4. close(pipefd[0]);
  5. }
  • 简单来说,就是第一个子进程的标准输出被绑定到了管道的写入端,于是第一个命令的输出,写入了管道,而第二个子进程管道将其标准输入绑定到管道的读取端,只要管道里面有了内容,这些内容就成了标准输入。
  • 两个示例代码,为什么要判断管道的文件描述符是否等于标准输入和标准输出呢?原因是,在调用pipe时,进程很可能已经关闭了标准输入和标准输出,调用pipe函数时,内核会分配最小的文件描述符,所以pipe的文件描述符可能等于0或1。在这种情况下,如果没有if判断加以保护,代码就变成了:
  1. dup2(1,1);
  2. close(1);

这样的话,第一行代码什么也没做,第二行代码就把管道的写入端给关闭了,于是便无法传递信息了

与shell命令进行通信

道的一个重要作用是和外部命令进行通信。在日常编程中,经常会需要调用一个外部命令,并且要获取命令的输出。而有些时候,需要给外部命令提供一些内容,让外部命令处理这些输入。Linux提供了popen接口来帮助程序员做这些事情。

就像system函数,即使没有system函数,我们通过fork、exec及wait家族函数一样也可以实现system的功能。但终归是不方便,system函数为我们提供了一些便利。同样的道理,只用pipe函数及dup2等函数,也能完成popen要完成的工作,但popen接口给我们提供了便利。

popen接口定义如下:

  1. #include <stdio.h>
  2. FILE *popen(const char *command, const char *type);
  3. int pclose(FILE *stream);

popen函数会创建一个管道,并且创建一个子进程来执行shell,shell会创建一个子进程来执行command。根据type值的不同,分成以下两种情况。

如果type是r:command执行的标准输出,就会写入管道,从而被调用popen的进程读到。通过对popen返回的FILE类型指针执行read或fgets等操作,就可以读取到command的标准输出,如图9-10所示。

如果type是w:调用popen的进程,可以通过对FILE类型的指针fp执行write、fputs等操作,负责往管道里面写入,写入的内容经过管道传给执行command的进程,作为命令的输入,如图9-11所示

popen函数成功时,会返回stdio库封装的FILE类型的指针,失败时会返回NULL,并且设置errno。常见的失败有fork失败,pipe失败,或者分配内存失败。

I/O结束了以后,可以调用pclose函数来关闭管道,并且等待子进程的退出。尽管popen函数返回的是FILE类型的指针,也不应调用fclose函数来关闭popen函数打开的文件流指针,因为fclose不会等待子进程的退出。pclose函数成功时会返回子进程中shell的终止状态。popen函数和system函数类似,如果command对应的命令无法执行,就如同执行了exit(127)一样。如果发生其他错误,pclose函数则返回-1。可以从errno中获取到失败的原因。

下面给出一个简单的例子,来示范下popen的用法:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<unistd.h>
  4. #include<string.h>
  5. #include<errno.h>
  6. #include<sys/wait.h>
  7. #include<signal.h>
  8. #define MAX_LINE_SIZE 8192
  9. void print_wait_exit(int status)
  10. {
  11. printf("status = %d\n",status);
  12. if(WIFEXITED(status))
  13. {
  14. printf("normal termination,exit status = %d\n",WEXITSTATUS(status));
  15. }
  16. else if(WIFSIGNALED(status))
  17. {
  18. printf("abnormal termination,signal number =%d%s\n",
  19. WTERMSIG(status),
  20. #ifdef WCOREDUMP
  21. WCOREDUMP(status)?"core file generated" : "");
  22. #else
  23. "");
  24. #endif
  25. }
  26. }
  27. int main(int argc ,char* argv[])
  28. {
  29. FILE *fp = NULL ;
  30. char command[MAX_LINE_SIZE],buffer[MAX_LINE_SIZE];
  31. if(argc != 2 )
  32. {
  33. fprintf(stderr,"Usage: %s filename \n",argv[0]);
  34. exit(1);
  35. }
  36. snprintf(command,sizeof(command),"cat %s",argv[1]);
  37. fp = popen(command,"r");
  38. if(fp == NULL)
  39. {
  40. fprintf(stderr,"popen failed (%s)",strerror(errno));
  41. exit(2);
  42. }
  43. while(fgets(buffer,MAX_LINE_SIZE,fp) != NULL)
  44. {
  45. fprintf(stdout,"%s",buffer);
  46. }
  47. int ret = pclose(fp);
  48. if(ret == 127 )
  49. {
  50. fprintf(stderr,"bad command : %s\n",command);
  51. exit(3);
  52. }
  53. else if(ret == -1)
  54. {
  55. fprintf(stderr,"failed to get child status (%s)\n",
  56. strerror(errno));
  57. exit(4);
  58. }
  59. else
  60. {
  61. print_wait_exit(ret);
  62. }
  63. exit(0);
  64. }
  • 将文件名作为参数传递给程序,执行cat filename的命令。popen创建子进程来负责执行cat filename的命令,子进程的标准输出通过管道传给父进程,父进程可以通过fgets来读取command的标准输出。

system函数与popen函数区别

  • popen函数和system有很多相似的地方,但是也有显著的不同。调用system函数时,shell命令的执行被封装在了函数内部,所以若system函数不返回,调用system的进程就不再继续执行。但是popen函数不同,一旦调用popen函数,调用进程和执行command的进程便处于并行状态。然后pclose函数才会关闭管道,等待执行command的进程退出。换句话说,在popen之后,pclose之前,调用popen的进程和执行command的进程是并行的,这种差异带来了两种显著的不同:
  • 在并行期间,调用popen的进程可能会创建其他子进程,所以标准规定popen不能阻塞SIGCHLD信号.这也意味着,popen创建的子进程可能被提前执行的等待操作所捕获。若发生这种情况,调用pclose函数时,已经无法等待command子进程的退出,这种情况下,将返回-1,并且errno为ECHILD。
  • 调用进程和command子进程是并行的,所以标准要求popen不能忽略SIGINT和SIGQUIT信号。如果是从键盘产生的上述信号,那么,调用进程和command子进程都会收到信号。

来自为知笔记(Wiz)

时间: 08-10

Linux内核解析:进程间通信:管道的相关文章

Linux内核解析之标准I/O库

当Linux创建一个进程时,会自动创建3个文件描述符0,1,2,分别对应标准输入,标准输出,错误输出.C库中与文件描述符对应的是文件指针.查看C库头文件stdio.h中的源码 typedef struct _IO_FILE FILE; //文件流类型 extern struct _IO_FILE *stdin; /* 标准输入流 */ extern struct _IO_FILE *stdout; /* 标准输出流 */ extern struct _IO_FILE *stderr; /* 错误

Linux程序设计学习笔记----进程间通信——管道

转载请注明出处: http://blog.csdn.net/suool/article/details/38444149, 谢谢! 进程通信概述 在Linux系统中,进程是一个独立的资源管理单元,但是独立而不孤立,他们需要之间的通信,因此便需要一个进程间数据传递.异步.同步的机制,这个机制显然需要由OS来完成管理和维护.如下: 1.同一主机进程间数据交互机制:无名管道(PIPE),有名管道(FIFO),消息队列(Message Queue)和共享内存(Share Memory).无名管道多用于亲

Linux进程间通信的几种方式总结--linux内核剖析(七)

进程间通信概述 进程通信的目的 传输数据 一个进程须要将它的数据发送给还有一个进程.发送的数据量在一个字节到几M字节之间 共享数据 多个进程想要操作共享数据,一个进程对共享数据 通知事 一个进程须要向还有一个或一组进程发送消息.通知它(它们)发生了某种事件(如进程终止时要通知父进程). 资源共享 多个进程之间共享相同的资源.为了作到这一点,须要内核提供锁和同步机制. 进程控制 有些进程希望全然控制还有一个进程的执行(如Debug进程),此时控制进程希望能够拦截还有一个进程的全部陷入和异常,并能够

深入解析Linux内核I/O剖析(open,write实现)

Linux内核将一切视为文件,那么Linux的文件是什么呢?其既可以是事实上的真正的物理文件,也可以是设备.管道,甚至还可以是一块内存.狭义的文件是指文件系统中的物理文件,而广义的文件则可以是Linux管理的所有对象.这些广义的文件利用VFS机制,以文件系统的形式挂载在Linux内核中,对外提供一致的文件操作接口. 从数值上看,文件描述符是一个非负整数,其本质就是一个句柄,所以也可以认为文件描述符就是一个文件句柄.那么何为句柄呢?一切对于用户透明的返回值,即可视为句柄.用户空间利用文件描述符与内

深入解析Linux内核及其相关架构的依赖关系

Linux kernel 成功的两个原因: 灵活的架构设计使得大量的志愿开发者能够很容易加入到开发过程中:每个子系统(尤其是那些需要改进的)都具备良好的可扩展性.正是这两个原因使得Linux kernel可以不断进化和改进. 一.Linux内核在整个计算机系统中的位置 分层结构的原则: the dependencies between subsystems are from the top down: layers pictured near the top depend on lower la

深入浅出实例解析linux内核container_of宏

做一件事情首先应该知道它的目的是什么. container_of的目的:如何通过结构中的某个变量获取结构本身的指针. 总体思路:假想一下,你的结构体中有好几个成员,你如何通过里面的"任一成员"获取整个结构体的首地址呢.container_of的做法就是通过typeof定义一个与"任一成员"同类型的指针变量pvar_a(假设变量名就是pvar_a),并让指针变量pvar_a指向这个"任一成员",然后用 "pvar_a的地址" 减

linux内核启动参数解析及添加

1.环境: ubuntu16.04 Linux jello 4.4.0-89-generic #112-Ubuntu SMP Mon Jul 31 19:38:41 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 2.查看当前linux内核的启动参数: cat /proc/cmdline 笔者的输出内容如下: BOOT_IMAGE=/boot/vmlinuz-4.4.0-89-generic root=UUID=bef418fa-4202-4513-b39b-cd

Linux内核编程:Linux2.6内核源码解析_进程遍历 &nbsp; &nbsp; &nbsp; &nbsp;

/*     *File    : test.c   *Author  : DavidLin        *Date    : 2014-12-07pm        *Email   : [email protected] or [email protected]        *world   : the city of SZ, in China        *Ver     : 000.000.001        *history :     editor      time    

解析 Linux 内核可装载模块的版本检查机制

转自:http://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/ 为保持 Linux 内核的稳定与可持续发展,内核在发展过程中引进了可装载模块这一特性.内核可装载模块就是可在内核运行时加载到内核的一组代码.通常 , 我们会在两个版本不同的内核上装载同一模块失败,即使是在两个相邻的补丁级(Patch Level)版本上.这是因为内核在引入可装载模块的同时,对模块采取了版本信息校验.这是一个与模块代码无关,却与内核相连的机制.该校验机