【APUE】Chapter15 Interprocess Communication

15.1 Introduction

  这部分太多概念我不了解。只看懂了最后一段,进程间通信(IPC)内容被组织成了三个部分:

  (1)classical IPC : pipes, FIFOs, message queues, semaphores, and shared memory

  (2)network IPC : sockets mechanism

  (3)advanced features of IPC

 

15.2 Pipes

  Pipes是“the oldest form of UNIX System IPC”,因此有两个方面的limitations:

  (1)一般来说pipes都是半双工(half-duplex)的工作模式

  (2)pipes能通信的process是有要求的:必须是同一个ancestor的(比如一个process不断fork这种的)

  1. pipe函数

  int pipe(int fd[2])

    fd[0]:执行pipe后,fd[0]代表reading file descriptor

    fd[1]: 执行pipe后,fd[1]代表writing file descriptor

    返回0代表成功,-1表示失败

  这个函数的原理可以用一张图来理解:

  【APUE】Chapter15 Interprocess Communication_第1张图片

  上左图:two ends connected in a single process

  上右图:emphasizes that the data in the pipe flows through the kernel

  这还是在single process里面玩儿,跟我们说的interprocess communication不一样。这时候要借助Chapter8中学到的内容,fork函数,化腐朽为神奇的一步:

    “the process that calls pipe then calls fork, creating an IPC channel from the parent to the child, or vice versa

  翻译过来就是:pipe + fork 完成了一个最原始的interprocess communication。上面的IPC思路,可以用下图解释:

  【APUE】Chapter15 Interprocess Communication_第2张图片

  理解上面的原理图,需要有fork的背景知识:parent process经过fork出child process后,parent process的file descriptor是直接copy到child process的

  这样,经过fork之后,就在parent和child中各有一对fd[0] fd[1]:

  (1)parent中的fd[1]由pipe输出到parent和child的两个fd[0]

  (2)child中的fd[1]同理也可以由pipe输出到parent和child中的两个fd[0]

  2. Pipe函数实现基础IPC功能

  下面,搞一个程序验证下是不是pipe是不是实现了这样的功能:

 1 #include "apue.h"
 2 
 3 int main(int argc, char *argv[])
 4 {
 5     int n;
 6     int fd[2];
 7     pid_t pid;
 8     char line[MAXLINE];
 9 
10     if (pipe(fd)<0) { 
11         err_sys("pipe error"); 
12     }
13     if ((pid=fork())<0) { 
14         err_sys("fork error"); 
15     }
16     else if (pid>0) { /*parent process*/
17         //close(fd[0]);
18         write(fd[1], "12345678", 8);
19         sleep(2);
20         n = read(fd[0], line, 4);
21         write(STDOUT_FILENO, line, n);
22         printf("parent\n");
23     }
24     else { 
25         //close(fd[1]);
26         n = read(fd[0], line, 4);
27         write(STDOUT_FILENO, line, n);
28         printf("child\n");
29     }
30     exit(0);
31 }

  ( 在pipe+fork之后,常规的套路应该是关闭parent和child端不用的file descriptor(parent关闭fd[0], child关闭fd[1]),但是为了解决上面的疑惑故意没有关上)

  这样,效果就是parent和child都保持了read和write的入口。

  程序做的事情就是在parent process中往pipe中写“12345678”总共8个字符,先让child process从pipe的出口读4个字符,然后再让parent process从pipe的出口读4个字符。代码运行结果如下:

  

  从结果中看到,parent和child都从管道中fetch了4个字符。解决了之前的疑惑。

  更进步一步,查阅了如下的blog(http://blog.csdn.net/tennysonsky/article/details/46315517),下面的几点总结的挺好:

  【APUE】Chapter15 Interprocess Communication_第3张图片

  还有一点要注意,read方法是阻塞的:如果parent和child都用read方法从pipe中读内容,如果pipe中的内容被读光了,read方法就阻塞住了。

  把上面代码中line18改为“write(fd[1], "1234", 4)”,运行结果如下:

  

  结果就是parent process在read那里阻塞住了。原因是:

  (1)pipe中的数据一旦被read走就消失了

  (2)read方法是阻塞方法,读不到数据就挂住了

  上面解释了“read读不到数据被阻塞的情况”,如果是write写数据的时候,read端已经关闭了呢?这个时候会产生一个SIGPIPE信号。

  有如下代码:

 1 #include "apue.h"
 2 #include <signal.h>
 3 #include <unistd.h> 
 4 
 5 void sig_pipe(int signo)
 6 {
 7     printf("pid:%d\n",getpid());
 8     if ( signo == SIGPIPE)
 9     {
10         printf("fetch SIGPIPE\n");
11         fflush(stdout);
12     }
13 }
14 
15 int main(int argc, char *argv[])
16 {
17     int n;
18     int fd[2];
19     pid_t pid;
20     char line[MAXLINE];
21     signal(SIGPIPE, sig_pipe);
22 
23     if (pipe(fd)<0) { 
24         err_sys("pipe error"); 
25     }
26     if ((pid=fork())<0) { 
27         err_sys("fork error"); 
28     }
29     else if (pid>0) { /*parent process*/
30         close(fd[0]);
31         sleep(10);
32         write(fd[1], "1234", 4);
33         printf("parent\n");
34     }
35     else { 
36         //close(fd[1]);
37         printf("child\n");
38         close(fd[0]);
39     }
40     exit(0);
41 }

    程序运行结果如下:

    【APUE】Chapter15 Interprocess Communication_第4张图片

    【APUE】Chapter15 Interprocess Communication_第5张图片

    可以看到,是调用write的parent process捕获了这个SIGPIPE信号(parent pid是6867)。(child pid为6868,由于parent没有回收child process,所以在child process执行完之后,就变成了野进程了<defunct>)

   3. 利用pipe函数实现一个 cat XXX | more的例子

   如题,直接上例子代码:

 1 #include "apue.h"
 2 #include <sys/wait.h>
 3 
 4 #define DEF_PAGER "/bin/more" /*default pager program*/
 5 
 6 int main(int argc, char *argv[])
 7 {
 8     int n;
 9     int fd[2];
10     pid_t pid;
11     char *pager, *argv0;
12     char line[MAXLINE];
13     FILE *fp;
14 
15     fp = fopen(argv[1], "r"); /*把fp关联到disk file*/
16     pipe(fd); /*创建pipe*/
17 
18     if ((pid = fork())<0) { 
19         err_sys("fork error");
20     }
21     else if (pid>0) { /*parent process*/
22         close(fd[0]);
23         while (fgets(line, MAXLINE, fp)!=NULL) { /*从disk file中读一行*/
24             n = strlen(line);
25             if (write(fd[1], line, n)!=n) { /*往child process的fd[0]中写*/
26                 err_sys("write error to pipe");
27             }
28         }
29         ferror(fp); /*检查写的过程中是否出问题了*/
30         close(fd[1]); /*关闭写的fd*/
31         if (waitpid(pid, NULL, 0)<0) { /*等child process执行完毕*/
32             err_sys("waitpid error");
33         }
34         exit(0);
35     }
36     else { 
37         close(fd[1]);
38         if (fd[0] != STDIN_FILENO) { /*将fd[0]关联到stdin中*/
39             if (dup2(fd[0], STDIN_FILENO)!=STDIN_FILENO) {
40                 err_sys("dup2 error to stdin");
41             }
42             close(fd[0]); /*fd[0]的使命到此结束了 因为read end已经被关联到stdout了*/
43         }
44         if ((pager = getenv("PAGER"))==NULL) { /*从环境变量中读取PAGER*/
45             pager = DEF_PAGER;
46         }
47         if ((argv0 = strrchr(pager, '/'))!=NULL) { /*找到'/'在字符串pager中最后一次出现的位置*/
48             argv0++; 
49         }
50         else { 
51             argv0 = pager;
52         }
53         if (execl(pager, argv0, (char *)0)<0) { /*pager是命令path argv0是具体路径下的命令*/
54             err_sys("execl error for %s", pager);
55         }
56     }
57     exit(0);
58 }

    编译后,执行如下命令:

    

    获得结果如下:

    【APUE】Chapter15 Interprocess Communication_第6张图片

    从结果来看 就跟 cat XXX | more实现的功能是一样的。

    从代码上看,为了实现上面的功能,主要在line39~line41中用到了dup2这个函数。回顾一下dup2这个函数:

    int dup2(A, B)

    (1)A是已经打开的file descriptor

    (2)让B与A关联到同一个file

    (3)如果A是not valid则fail

    (4)如果A与B相等,则什么都不做返回

    有了上面的背景知识,理解以上的代码主要有以下几点:

    (1)dup2是把parent process通过fd[1]往pipe里面写数据,本来正常的出口是fd[0],但是通过dup2把出口改到了child process的STDIN_FILENO上面。这样就彻底实现了cat XXX | more一样的功能。“cat XXX”就是parent process做的事情,“|”就是起到了dup2的作用,“more”是child process实际做的事情。

    (2)execl之后child process执行的内容就是/bin/more命令;而执行more命令的process中file descriptor是从之前child process中继承下来的,而STDIN_FILENO就是编号为0的file descriptor;所以对于“more”命令来说,它的STDIN_FILENO就是pipe的出口端fd[0]。具体excel的细节,还得去看APUE的chapter 8。

   4. popen & pclose函数 及其实现细节

   "pipe + fork"编程模型是一个最基础的IPC模型,<stdio.h>中提供了两个函数来集成化实现"pipe+fork"的编程模型。

   FILE *popen(const char *cmdstring, const char *type)

   int pclose(FILE *fp)

   参数cmdstring就是要执行的命令, type可以取值{“r”, “w”}两个值。这里的r和w跟fopen中的r和w一个意思。

   当type对应的参数为"r"的时候,即fp = popen(cmdstring, "r")的情况下,是如下的编程模型:

   【APUE】Chapter15 Interprocess Communication_第7张图片

   当type对应的参数为"w"的时候,即fp = popen(cmdstring, "w")的情况下,是如下的编程模型:

   【APUE】Chapter15 Interprocess Communication_第8张图片

   这两个函数本身stdio是都提供具体实现的,但是为了串起来一些细节,用一个综合的例子来说明。

   这个例子还是实现cat XXX | more的功能,与之前的区别是把"pipe+fork"的编程模型继承到了popen和pclose中。具体代码如下:

  1 #include <stdio.h>
  2 #include <stdlib.h> 
  3 #include <unistd.h> 
  4 #include <errno.h>
  5 #include <fcntl.h>
  6 #include <sys/wait.h>
  7 #include <limits.h>
  8 
  9 #ifdef OPEN_MAX
 10 static long openmax = OPEN_MAX;
 11 #else
 12 static long openmax = 0;
 13 #endif
 14 
 15 #define OPEN_MAX_GUESS 256
 16 #define PAGER "${PAGER:-more}" /*environment variable, or default*/
 17 #define MAXLINE 4096
 18 
 19 static pid_t *childpid = NULL; /*pointer to array allocated at run-time*/
 20 static int maxfd;
 21 
 22 /*系统允许获得的最大的file descriptor数量*/
 23 long open_max(void)
 24 {
 25     if (openmax==0) { 
 26         errno = 0;
 27         if ((openmax = sysconf(_SC_OPEN_MAX))<0) { 
 28             if (errno == 0) { 
 29                 openmax = OPEN_MAX_GUESS;
 30             }
 31             else { 
 32                 err_sys("sysconf error for _SC_OPEN_MAX");
 33             }
 34         }
 35     }
 36     return openmax;
 37 }
 38 
 39 
 40 FILE * popen(const char *cmdstring, const char *type)
 41 {
 42     int i;
 43     int pfd[2];
 44     pid_t pid;
 45     FILE *fp;
 46     /*这段代码保证输入参数*/
 47     if ((type[0] != 'r' && type[0] != 'w')||(type[1] !=0)) { 
 48         errno = EINVAL;
 49         return NULL;
 50     }
 51     if (childpid==NULL) { /*如果是首次处理childpid 动态给childpid分配内存*/
 52         maxfd = open_max();
 53         if ((childpid = calloc(maxfd, sizeof(pid_t)))==NULL) { 
 54             return NULL;
 55         }
 56     }
 57     if (pipe(pfd)<0) { /*pipe搞一下pfd, pfd[0]代表input pfd[1]代表output*/ 
 58         return NULL;
 59     }
 60     if ( pfd[0]>=maxfd || pfd[1]>=maxfd ) { /*如果超出fd最大量程 就把fd都关了返回*/
 61         close(pfd[0]);
 62         close(pfd[1]);
 63         errno = EMFILE; /*这个ERROR的意思就是file descriptor超出最大量程了*/
 64         return NULL;
 65     }
 66     /*在这之前都是做各种准备工作*/
 67     if ((pid=fork())<0) { 
 68         return NULL;
 69     }
 70     else if (pid==0) { /*child process*/ 
 71         if (*type=='r') { /*parent要从child中读 所以关闭child中pfd[0] input的fd*/
 72             close(pfd[0]);
 73             if (pfd[1]!=STDOUT_FILENO) { /*常规套路 把pdf[1]引到child的标准输出中*/
 74                 dup2(pfd[1], STDOUT_FILENO);
 75                 close(pfd[1]);
 76             }
 77         }
 78         else { /*parent要向child中写 所以关闭child中pfd[1] output的fd*/
 79             close(pfd[1]);
 80             if (pfd[0]!=STDIN_FILENO) { 
 81                 dup2(pfd[0], STDIN_FILENO);
 82                 close(pfd[0]);
 83             }
 84         }
 85         /*close all descriptors in childpid[]*/
 86         for( i=0; i<maxfd; i++ ) /*为什么要这种操作 因为child process可以继承parent process的file descriptor 但是这些file descriptor其实并不需要 所以都需要关了*/ 
 87         {
 88             if (childpid[i]>0) { /*每次fork之后 child process中都会copy过来一套childpid 而这些pid在child porocess中其实都不需要 所以 需要一一给删除*/
 89                 close(i);
 90             }
 91         }
 92         /*至此 child process的准备工作都做好了: 1.管道铺好了 2.该关的fd也都关了*/
 93         execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
 94         _exit(127);
 95     }
 96     /*parent process继续往下进行*/
 97     if (*type=='r') { 
 98         close(pfd[1]);
 99         if ((fp=fdopen(pfd[0], type))==NULL) { /*因为pfd已经被pipe打开了 所以对于打开的fd就要用fdopen来处理 下面'w'的情况同理*/ 
100             return NULL;
101         }
102     }
103     else { 
104         close(pfd[0]);
105         if ((fp=fdopen(pfd[1], type))==NULL) { 
106             return NULL;
107         }
108     }
109     childpid[fileno(fp)] = pid; /*这里的方式是用fd作为index child pid号作为value 构建这个childpid数组*/
110     return fp;
111 }
112 
113 int pclose(FILE *fp)
114 {
115     int fd, stat;
116     pid_t pid;
117 
118     if (childpid==NULL) { /*没有调用过popen函数  报错*/
119         errno = EINVAL;
120         return -1;
121     }
122     fd = fileno(fp); /*file descriptor超过最大量程 报错*/
123     if (fd>=maxfd) {
124         errno = EINVAL;
125         return -1;
126     }
127     if ((pid=childpid[fd])==0) { /*正用着的进程的pid是不可能为0的 因为没用用的childpid[]都是0  报错*/
128         errno = EINVAL;
129         return -1;
130     }
131     childpid[fd] = 0; /*把进程pid设为0了 告诉父进程 这个曾经起作用的child process已经无效了*/
132     if (fclose(fp)==EOF) { /*fclose执行不成功返回的是-1 且errno中标示了是什么错误*/
133         return -1;
134     }
135     /*这里为什么要用while循环?*/
136     while (waitpid(pid, &stat, 0)<0) { /*这个pid是子进程的pid 由于调用pclose的一定是parent process 所以必须等着child process进程执行完毕了再往下进行*/
137         if (errno!=EINTR) {
138             return -1;
139         }
140     }
141     return stat; /*通过stat返回 与fp挂钩的child process的执行状态*/
142 }
143 
144 
145 int main(int argc, char *argv[])
146 {
147     char line[MAXLINE];
148     FILE *fpin, *fpout;
149 
150     fpin = fopen(argv[1], "r");
151     if ((fpout = popen(PAGER, "w"))==NULL) { 
152         err_sys("popen error");
153     }
154     while (fgets(line, MAXLINE, fpin)!=NULL) { 
155         if (fputs(line, fpout)==EOF) { 
156             err_sys("fputs error to pipe");
157         }
158     }
159     if (ferror(fpin)) {
160         err_sys("fgets error");
161     }
162     if (pclose(fpout)==-1) {
163         err_sys("pclose error");
164     }
165     exit(0);
166 }

    代码执行结果与之前相同,不再赘述。主要通过这个例子来学习一些细节。

    popen函数:

    由于这种通用的函数要defensive programming的思路来设计,所以在实现functionality之前要处理各种corner case

    (1)如果type的值不是r和w 则出错返回。

    (2)如果childpid(childpid中存放着已经开着的各种child process的pid)为空,证明是parent process首次调用popen函数,需要给childpid分配memory。

    (3)具体要给childpid分配多大的memory,这个由maxfd参数决定。maxfd这个由于具体系统实现决定(通过open_max函数获得,apue耍书上P52 figure2.17实现),含义是同一个process最多能开多少个fd(在我的用centos系统上是1024)。给childpid分配memory的方式一开始没理解好,问题主要纠结在为什么pid的array要根据file descriptor max来决定。其实,就应该是这样的,因为"pipe+fork"编程模型的核心在于一个fd通向一个child process;所以,最多能有多少个chidpid就是取决于最多能弄出来多少个file descriptor

    接下来就是之前提到过的“pipe+fork”编程模型,看似只返回了一个FILE *fp,实际上做了如下几件事情:

    (4)私下搞出来一个child process,执行传入的cmdstring命令

    (5)返回给parent process一个fp,这个fp就相当于child process的STDIN

    (6)为了让parent process能定位到popen函数创建的child process。需要在childpid中存着这个pid,这里有个技巧,就是用parent process与child process关联的fp的fd,最终是fd作为这个child process pid在childpid数组中的index。我猜这样设计的原因是,pid和fd都同时知道了,想做什么操作也方便了。

    (7)在child process中,用for循环把childpid中所有的fd都关闭了。因为fork之后,parent process中的childpid也继承到了chid process中,这对child process是无用的,而且不关掉还容易留隐患。通过检验childpid[i]是否为0,可以判断这个child process是否被fork出来过;如果被fork出来过,则证明其fd(就是i)是有效的,统统关闭之。注意,这里关闭的是后面fork出来的child process指向之前fork出来的child process的fd,有点儿拗口,主要是下面的图:

    【APUE】Chapter15 Interprocess Communication_第9张图片

    大概就是这个意思了。因此才有了for循环,删除child与child之间fd的关联。没有关联就不能乱删,判断有关联的标准就是pid!=0。这样就保证了只有parent与child之间的pipe连接,不会产生child与child之间的pipe通道。

    pclose函数

    这个函的功能就是断开parent与某个child之间的pipe通道。这个功能分为三个实现环节:

    (1)关闭之前获得的fp(传入的参数fp发挥了作用

    (2)防止child成为孤儿进程,必须回收child process的执行状态:用waitpid来完成这个事情,又由于waitpid允许被signal打断,而signal产生的EINTR信号是可以容忍出现的,因此用了while循环来做。(之前保存在childpid中的pid发挥了作用

    (3)还必须保证后续fork的child知道,这个child已经被回收了(childpid[fd]=0发挥了作用

 

15.3 Coprocesses

  自己写出去,经过一个filter处理后,再读回来。

  【APUE】Chapter15 Interprocess Communication_第10张图片

  先构造一个2整数加法器如下:  

 1 #include "apue.h"
 2 
 3 int main(int argc, char *argv[])
 4 {
 5     int n, int1, int2;
 6     char line[MAXLINE];
 7 
 8     while ((n=read(STDIN_FILENO, line, MAXLINE))>0) { 
 9         line[n] = 0; /*null terminate*/
10         if (sscanf(line, "%d%d", &int1, &int2)==2) { 
11             sprintf(line, "%d\n", int1+int2);
12             n = strlen(line);
13             if (write(STDOUT_FILENO, line, n) != n) { 
14                 err_sys("write error");
15             }
16         }
17         else { 
18             if (write(STDOUT_FILENO, "invalid args\n", 13)!=13) { 
19                 err_sys("write error");
20             }
21         }
22     }
23     exit(0);
24 }

  这个加法器的作用就是从stdin中读两个整数,做容错处理,加法运算,再写出到stdout中。编译成可执行文件add2。

  构造一个进程,利用coprocess的方式,来调用add2处理加法运算。代码如下(除去了书上一些defensive programming的段落,目的是便于把思路捋清楚):

 1 #include "apue.h"
 2 
 3 static void sig_pipe(int signo)
 4 {
 5     if ( signo == SIGPIPE )
 6     {
 7         printf("fetch SIGPIPE\n");
 8     }
 9     exit(1);
10 }
11 
12 int main(int argc, char *argv[])
13 {
14     int n, fd1[2], fd2[2];
15     char line[MAXLINE];
16     pid_t pid;
17 
18     signal(SIGPIPE, sig_pipe);
19     pipe(fd1);
20     pipe(fd2);
21 
22     if ((pid = fork())<0) { 
23         err_sys("fork error");
24     }
25     else if (pid>0) { /*parent process*/
26         close(fd1[0]);
27         close(fd2[1]);
28         while (fgets(line,MAXLINE,stdin)!=NULL) { 
29             n = strlen(line);
30             if (write(fd1[1],line,n)!=n) { 
31                 err_sys("write error to pipe");
32             }
33             if ((n=read(fd2[0], line, MAXLINE))<0) { 
34                 err_sys("read error from pipe");
35             }
36             if (n==0) { 
37                 err_msg("child closed pipe");
38                 break;
39             }
40             line[n] = 0; /*null terminate*/
41             if (fputs(line ,stdout)==EOF) { 
42                 err_sys("fputs error");
43             }
44         }
45         ferror(stdin);
46         exit(0);
47     }
48     else { 
49         close(fd1[1]);
50         close(fd2[0]);
51         if (fd1[0]!=STDIN_FILENO) {
52             dup2(fd1[0], STDIN_FILENO);
53             close(fd1[0]);
54         }
55         if (fd2[1]!=STDOUT_FILENO) { 
56             dup2(fd2[1], STDOUT_FILENO);
57             close(fd2[1]);
58         }
59         execl("./add2", "add2", (char *)0);
60     }
61     exit(0);
62 }

  执行结果如下:

  【APUE】Chapter15 Interprocess Communication_第11张图片

  (1)一开始按照coprocess的模型执行两个整数的加法,输入“1 2”返回“3”

  (2)然后手工结束add2进程如下:

  【APUE】Chapter15 Interprocess Communication_第12张图片

  手动结束进程后,pipe的一端已经没有了;因此,a.out的write就触发了SIGPIPE的信号。

  这里还有个内容需要注意,add2在实现上用的都是low-level的read和write两个读写函数。

  如果把level往上拔一下,换到standard I/O,代码如下:  

 1 #include "apue.h"
 2 
 3 int main(int argc, char *argv[])
 4 {
 5     int int1, int2;
 6     char line[MAXLINE];
 7 
 8     setvbuf(stdin, NULL, _IOLBF, 0);
 9     setvbuf(stdout, NULL, _IOLBF, 0);
10 
11     while (fgets(line, MAXLINE, stdin)!=NULL) { 
12         if (sscanf(line, "%d%d", &int1, &int2)==2) { 
13             printf("%d\n", int1+int2);
14         }
15         else { 
16             printf("invalid args\n");
17         }
18     }
19     exit(0);
20 }

  则再执行上述的完整流程,完全进行不下去了。

  原因可以用一句话来概括:由于standard I/O的buffer机制,使得这种coprocess编程模型出现了deadlock

  由于这种编程模型下,add2自动识别standard input来自于pipe(同理,standard output也被识别为输出到pipe),因此standard I/O默认设置为fully buffered。在这样的背景下,整个program执行的流程如下:

  (1)a.out调用write向pipe中写数据

  (2)add2调用fgets从pipe中读数据,但是由于是fully buffered,所以不刷新一直读不进来;由于没有读到数据,因此也不会往出写数据

  (3)同时,a.out调用完write之后,又马上调用了read;由于read是阻塞函数,所以一直等着pipe的输出

  (4)但是(2)中已经说了,add2等着a.out给buffer写满再刷新,同时a.out也等着add2往pipe中写数据。a.out和add2都互相等着对方,最终就造成了deadlock。

  如何解决coprocess这种编程模型遇到的问题?

  一种方式就是修改源代码,强制为line buffered;但是并不是所有程序都拿的到源代码的,于是就有了另外一种方法,叫pesudo terminal,后面Chapter 19中提到。

  

15.5 FIFOs

  传统的pipes是在ancestor relation processes之间完成IPC,FIFOs则可以在unrelated processes之间完成IPC。

  1. FIFOs是一种file,它的创建可以用如下函数:

    int mkfifo(const char *path, mode_t mode)

    int mkfifoat(int fd, const char *path, mode_t mode)

    其用法可以类比open和openat(书上P62)

  2. 与pipe类似,如果向FIFOs执行write操作,但是read端已经关闭了,也会发出SIGPIPE信号

  3. 同一个FIFOs可以有多个writer向其中写,因此有多个写的atomic问题:与pipe类似,如果每个writer写的数据不大于PIPE_BUF,则FIFOs的实现机制可以保证其同步,不会数据在FIFOs中data interleaved

  4. FIFOs的实际用处主要包括两个方面:

    (1)shell command用FiFOs技术完成一个pipeline向另一个pipeline传输数据(好处是不用创建临时文件)

    (2)CS架构中,client与server数据的交换汇合点

  三个例子。

  第一例子说明,FIFOs可以在unrelated process之间exchange data,代码:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 
 6 #include <sys/types.h>
 7 #include <sys/stat.h>
 8 #include <signal.h> 
 9 #include <errno.h> 
10 
11 #define PATHNAME "/tmp/myfifo"
12 
13 int main(int argc, char *argv[])
14 {
15     pid_t pid;
16     int fd = -1;
17     char buf[BUFSIZ] = "";
18 
19     mkfifo(PATHNAME, 0644); /*创建一个管道文件*/
20     fflush(NULL);
21     if ((pid = fork())<0) { 
22         exit(1);
23     }
24     else if (pid>0) { /*parent 1*/
25         pid_t child1 = pid;
26         pid = fork();
27         if (pid>0) { /*parent 2*/
28             waitpid(pid, NULL, 0); /*先等着child2执行完*/
29         }
30         else { /*child 这样就保证两个child没有共同的parent process*/
31             fd = open(PATHNAME, O_RDWR); /*打开fifo文件, 既能读又能写*/
32             read(fd, buf, BUFSIZ); /*阻塞读*/
33             printf("%s",buf); /*读到什么展示一下*/
34             write(fd, " World!", 8); /*读完了再写点儿东西进去*/
35             close(fd);
36             exit(0);
37         }
38         waitpid(child1, NULL, 0); /*再等着child1执行完*/
39     }
40     else { /*child 1*/
41         fd = open(PATHNAME, O_RDWR); /*打开fifo文件*/
42         write(fd, "Hello", 6); /*写点儿东西*/
43         sleep(1); /*停一会儿*/
44         read(fd, buf, BUFSIZ); /*阻塞读*/
45         close(fd);
46         puts(buf); /*展示读到了什么*/
47         remove(PATHNAME); /*把创建的fifo文件删了*/
48         exit(0);
49     }
50     return 0;
51 }

    代码执行结果如下:

    【APUE】Chapter15 Interprocess Communication_第13张图片

    上述的代码构造了两个unrelated process,并且通过FIFOs来回写了一次。

  第二个例子,pipeline向pipeline之间传数据,shell调用tee命令

    mkfifo fifo1

    prog3 < fifo1 &

    prog1 < infile | tee fifo1 | prog2

    上述shell command的实际流程就是下面的图:

    【APUE】Chapter15 Interprocess Communication_第14张图片

    如果没有FIFO这个环节,就只能用临时文件:因为prog3是一个进程,而tee的stdout只能输出prog2,因此tee输出到FIFO就相当于输出到了prog3中。

  第三个例子,CS架构的数据汇合节点,如下图所示:

    【APUE】Chapter15 Interprocess Communication_第15张图片

    (1)从C向S,通过well-known FIFO

    (2)从S向C,server给每个client都分配一个unique FIFO

    (3)如果client crash了,server向client执行write的时候,会出发SIGPIPE信号。

 

15.6 XSI IPC

  书上这部分内容比较抽象,讲述的就是XSI IPC三种类型(message queues, semaphores, and shared memory)的共性:

  1. Identifiers and Keys:

    Keys用于全局唯一标识某种IPC,常用的生成Keys的方式是利用pathname+project ID的方式来生成,具体的函数是ftok函数。

    Identifiers依赖于Keys,向msgget函数传入参数Keys来生成Identifier;这个Identifier在进程内部唯一标识某个IPC,具体的函数是msgget函数。

    为什么要搞这种内外分开的设计模式?

    我猜这是因为方便管理进程对于某个IPC structure的权限管理:Keys的意思是,IPC structure客观上就在那里摆着(由pathname+project ID决定);但是,每个进程中再由Keys生成Identifiers的时候,必须设定相应的权限。通过这样的方式,管理了不同进程对相同IPC structure的不同权限。

    /* 2015.12.24日补充*/

    看到chapter17的时候,以17.3 17.4的messsage queue为例:同一台机器上,一个process中规定了key,创建了message queue以及其poll接收方;另一个process利用这个key,并且创建了message queue的消息发送方。

    (1)在两个process之间,这个key就是两个不同process之间通信的信物

    (2)在每个process内部,分别由相同的key产生各自process内部的identifier类似file descriptor这个id就相当于这个key在进程内部的别名

    这才是稍微正确的打开方式,理解为什么要有key以及identifier。从这个角度理解,message queue的key也就是一个文件名了,不同的process里面通过各自的identifier关联到这个message queue上。

    /****************/

   2. Permission Structure

    通过一个struct ipc_perm中的各项属性来决定。

   3. Configuration Limits

    这是由kernel管的。 

  4. Advantages and Disadvantages

    没太看懂。

 

15.7 Message Queues

  书上缺少具体的例子,参考下面的blog(http://www.cnblogs.com/chuyuhuashi/p/4475904.html)中的代码例子,模拟一个单机版的Message Queues。

  代码分为三个部分:

  第一个部分是记录消息队列接口参数以及消息主体结构体的头文件proto.h,如下:

 1 #ifndef PROTO_H_
 2 #define PROTO_H_
 3 
 4 #define NAMESIZE 32
 5 
 6 #define KEYPATH "/tmp/out"
 7 
 8 #define KEYPROJ 'a'
 9 
10 #define MSGTYPE1 11
11 #define MSGTYPE2 12
12 
13 struct msg_st{
14     long mtype;
15     char name[NAMESIZE];
16     int chinese;
17     int math;
18 };
19 
20 #endif

  第二个部分是消息队列接收部分,reciver.c如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/types.h>
 4 #include <sys/ipc.h>
 5 #include <sys/msg.h>
 6 
 7 #include "proto.h"
 8 
 9 int main(int argc, char *argv[])
10 {
11     key_t key;
12     int msgid;
13     struct msg_st rbuf;
14 
15     /*创建key 标识从哪个消息队列接收*/
16     key = ftok(KEYPATH, KEYPROJ);
17     if (key<0) { 
18         perror("ftok()");
19         exit(1);
20     }
21 
22     /*reciver创建消息队列*/
23     msgid = msgget(key, IPC_CREAT|0600); /*0400和0200的合体 具有user-read和user-write的权限*/
24     if (msgid<0) { 
25         perror("msgget()");
26         exit(1);
27     }
28 
29     /*不断从消息队列中接收消息*/
30     while (1) { 
31         /*接收消息*/
32         if (msgrcv(msgid, &rbuf, sizeof(rbuf)-sizeof(long),0,0)<0) { 
33             perror("msgrcv");
34             exit(1);
35         }
36         /*校验消息类型*/
37         if (rbuf.mtype == MSGTYPE1) {
38             printf("MSGTYPE1:\n"); 
39             printf("name = %s\n", rbuf.name);
40             printf("math = %d\n", rbuf.math);
41             printf("chinese = %d\n", rbuf.chinese);
42         }
43         else if (rbuf.mtype == MSGTYPE2) { 
44             printf("MSGTYPE2:\n");
45             printf("name = %s\n", rbuf.name);
46             printf("math = %d\n", rbuf.math);
47             printf("chinese = %d\n", rbuf.chinese);
48         }
49         else { 
50             exit(1);
51         }
52     }
53 
54     /*移除消息队列信息*/
55     msgctl(msgid, IPC_RMID, NULL);
56     exit(0);
57 }

  第三部分是向消息队列发送消息的部分,sender.c如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

#include "proto.h"

int main(int argc, char *argv[])
{
    key_t key;
    int msgid;
    struct msg_st sbuf;

    srand(time(NULL));
    
    /*创建key 用于标识发送到哪个消息队列*/
    key = ftok(KEYPATH, KEYPROJ);
    if (key<0) { 
        perror("ftok()");
        exit(1);
    }

    /*由于reciver已经创建了以key为标识的消息队列 所以只需要得到msgid即可*/
    msgid = msgget(key, 0);
    if (msgid<0) { 
        perror("msgget()");
        exit(1);
    }

    /*构建消息主体*/
    sbuf.mtype = MSGTYPE1;
    strcpy(sbuf.name, "type1 sender");
    sbuf.math = rand()%100;
    sbuf.chinese = rand()%100;
    /*发送消息*/
    if (msgsnd(msgid, &sbuf, sizeof(sbuf)-sizeof(long),0)<0) { 
        perror("msgsnd()");
        exit(1);
    }

    sbuf.mtype = MSGTYPE2;
    strcpy(sbuf.name, "type2 sender");
    sbuf.math = rand()%100;
    sbuf.chinese = rand()%100;
    if (msgsnd(msgid, &sbuf, sizeof(sbuf)-sizeof(long),0)<0) { 
        perror("msgsnd()");
        exit(1);
    }


    /*发送完毕*/
    puts("ok!");
    exit(0);
}

  执行结果如下:

  【APUE】Chapter15 Interprocess Communication_第16张图片

  操作流程如下:

  (1)先启动./reciver(ps -a查看,确实有reciver再运行),建立消息队列并不断做好准备从消息队列中循环接收消息。

  (2)启动./sender ,向reciver建立的消息队列中发送消息。

  (3)reciver通过消息队列接收到了sender发来的消息,做出响应并输出结果

  上面的流程,就是典型的消息队列通信流程,其中涉及到如下几个关键函数:

  1. ftok函数

  key_t ftok(const char *path, int id)

  函数的作用是根据输入参数获得唯一的标示某个消息队列的key_t型变量。

  一个消息队列必须对应唯一的key_t;如果reciver和sender想要对应同一个消息队列,reciver和sender就必须用同样的path和id作为参数,生成key_t。path和id就是定义在proto.h中的两个宏:

  

  注意。这里的KEYPATH所指的文件必须是存在的,如果不存在,则会报错。

  2. msgget函数

  int msgget(key_t key, int flag)

  打开或者创建一个消息队列。如果消息队列不存在,就创建之;如果存在了,就打开之。

  其中key就是就ftok生成的唯一标识消息队列的key_t类型变量;flag其中一个作用是控制权限,以及消息队列的打开模式。

  函数的返回值是int型,就是前面提到的消息队列identifier。在process内部的所有对消息队列的进一步操作,都用msgid来唯一标识这个消息队列。

  3. msgrcv函数

  ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag)

  用于消息队列接收端,接受消息。

  msqid:进程中由msgget产生的消息队列进程内唯一id,即告诉从哪个进程接受消息

  ptr:指向一个long类型的地址,即告诉消息从消息队列中读完之后存到哪里。上述代码中是一个结构体,即消息队列中每个消息的定义结构,reciver和sender都要知道这个结构才能使IPC通信完成。

  nbytes:ptr所指的long类型再往后读多少bytes的消息数据内容;默认每个消息的开头都是long类型,这个long类型只表征消息的类型,不涉及到消息的内容;跳过这个long的大小之后,紧接着的都是消息内容,这个nbytes表征了读多少消息内容。

  type:等于0标示读消息队列中最近一个消息;大于0表示,只读最近一个type=ptr所指向的long变量类型的值(在这里就是mtype的值);小于0也有其对应的意义。

  flag:有一个重要属性是IPC_NOWAIT;正常msgrcv这个函数是阻塞的,即干等着收到消息才返回;如果在flag中设置了IPC_NOWAIT这个标志位,则不会干等着,一旦没有马上获取消息队列中的消息,就马上返回了,并且将errno设置为EIDRM。即,默认不改动flag情况下,msgrcv是一个阻塞函数:每次就从消息队列中读那么多,读到了才算完事儿,否则就干等着。

  函数的返回值是从消息的数据部分(不包括long类型的mtype

  这里有关ptr结构体成员的定义是有严格要求的:第一个成员必须是long类型的mtype,并且必须是大于0的值。为什么这样?这几个函数就是这么协定的。即完整的消息结构必须是:“long表示的消息类型+紧跟着的消息数据内容

  4. msgsnd函数

  int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag)

  与3相同的几个参数不再赘述。

  flag的设置也可以设为IPC_NOWAIT;不同的在于如果没有立刻向队列中发出去消息,则将errno设为EAGAIN,表示资源繁忙。

  5. msgctl函数

  int msgctl(int msqid, int cmd, struct msqid_ds *buf)

  这是个消息队列的管家函数。

  cmd可以取值为 {IPC_STAT,IPC_SET,IPC_RMID}

  根据cmd不同取值,执行不同的功能。上述代码中执行的是删除功能。

  

15.8 Semaphores

  信号量是一个“counter used to provide access to a shared data object for multiple processes”。即保证多进程对于shared data object进行同步操作。APUE的作者对XSI IPC的semaphore的评价并不是很高,使用上比较复杂,并且有时候容易遇到各种坑。

  通过一个例子,来说明semaphore的一般用法,代码如下;

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <sys/wait.h>
 6 #include <sys/types.h>
 7 #include <sys/ipc.h>
 8 #include <sys/sem.h>
 9 #include <errno.h>
10 
11 #define PROCNUM 10
12 #define FNAME "/tmp/out"
13 #define BUFSIZE 1024
14 
15 static int semid; /*所有的进程都要访问的资源 且不会被修改*/
16 
17 static void P(int sem_index){
18     struct sembuf op;
19     op.sem_num = sem_index; /*semid标定的semaphore set中具体第sem_index下标的那个semaphore*/
20     op.sem_op = -1; /*申请占用1个资源*/
21     op.sem_flg = 0; /*没有特殊要求*/
22     while (semop(semid,&op,1)<0) { /*这里的'1'指的是从&op这个地址往后取几个struct sembuf*/
23         if (errno!=EINTR && errno!=EAGAIN) { 
24             perror("semop()");
25             exit(1);
26         }
27     }
28 }
29 
30 static  void V(int sem_index){
31     struct sembuf op;
32     op.sem_num = sem_index;
33     op.sem_op = 1; /*释放1个资源*/
34     op.sem_flg = 0;
35     while (semop(semid,&op,1)<0) { 
36         if (errno!=EINTR && errno!=EAGAIN) { 
37             perror("semop()");
38             exit(1);
39         }
40     }
41 }
42 
43 static void func_add(){
44     FILE *fp;
45     char buf[BUFSIZE];
46     fp = fopen(FNAME, "r+"); /*所有child process都可以打开FNAME*/
47     /*并且所有的child process都必须按照套路出牌 先敲门再访问*/
48     P(0); /*占用资源*/
49     P(1);
50     fgets(buf, BUFSIZE, fp); 
51     rewind(fp);
52     sleep(1);
53     fprintf(fp,"%d\n",atoi(buf)+1);
54     fflush(fp);
55     V(1);
56     V(0); /*释放资源*/
57     fclose(fp);
58 }
59 
60 int main(int argc, char *argv[]){
61     int i;
62     pid_t pid;
63     /*由于都是在亲缘关系的process之间  因此可以用IPC_PRIVATE宏生成semid*/
64     semid = semget(IPC_PRIVATE, 2, 0600); /*创建两个semaphore*/
65     if (semid<0) { 
66         perror("segment()");
67         exit(1);
68     }
69     if (semctl(semid,0,SETVAL,1)<0) { /*设置semaphore[0]资源数量是1*/
70         perror("semctl()");
71         exit(1);
72     }
73     if (semctl(semid,1,SETVAL,1)<0) { /*设置semaphore[1]资源的数量是1*/
74         perror("semctl()");
75         exit(1);
76     }
77     /*fork出20个子进程*/
78     for (i=0; i<PROCNUM; i++){
79         pid = fork();
80         if (pid==0) { /*每次在child process中执行对semaphore的操作*/ 
81             func_add();
82             exit(0);
83         }
84     }
85     for(i=0; i<PROCNUM; i++) /*等着fork出来的所有child都返回完成了*/
86         wait(NULL);
87     semctl(semid, 0, IPC_RMID); /*移除这个semaphore*/
88     exit(0);
89 }

    代码执行到功能,是10个进程向同一个文件(/tmp/out)中累加数据,用信号量的方式对多进程操作同步。

    有了前面消息队列的基础,对semaphore的几个函数梳理如下:

    1. semget函数

    int segmet(key_t key, int nsems, int flag)

    创建信号量

    key : 喂给这个函数key(这个key是不同进程中唯一标示semaphore的,上面的例子中,由于各个process都是由一个parent fork出来的,属于亲属关系,因此用IPC_PRIVATE作为key即可

    nsems : 创建信号量的数量(上述代码是2个

    flag:一些特殊要求设置flag位

    函数的返回值在是semaphore在进程内部的唯一标示

    2. semctl函数

    信号量的管理函数

    int semctl(int semid, int semnum, int cmd, ... /* union semun arg */)

    semid : 进程内部唯一标示semaphore的identifier

    semnum : 这个变量有点别扭,即semid标示的是一个semaphore数组,semnum指的是这个数组中的semaphore下标(在上面的代码中由于semid标示的数组总共有两个semaphore资源,所以这里semnum可以取值为{0,1})。

    cmd : 要对某个semaphore执行的具体操作(上述代码中是SETVAL,即只对一个semaphore资源操作

    3. semop函数

    int semop(int semid, struct sembuf semop_array[], size_t nops)

    对信号量操作

    semid:同上

    semop_array:struct sembuf类型的结构体数组指针,每个结构体中存放着要对semid所指向的某个semaphore的操作信息。

struct sembuf{
    unsigned short sem_num; /*semid标定的semaphore数组中的元素下标*/
    short sem_op; /*正几就是占用几个资源 负几就是释放几个资源 0代表等着资源是0*/
    short sem_flg; /*特殊要求 设置flag位*/ 
}   

    nops:代表semop_array的数量,到底有多少个op需要加在semaphore上面

    

15.9 Shared Memory

    优点就是速度快,缺点是同步起来麻烦,得借助信号量之类的。

    这种shared memory不像之前的消息队列或信号量,没有显式的文件名。

    通过shared memory这种方式可以方便的在parent process和child process之间通信。

    直接看一个列子:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/mman.h>
 4 #include <sys/types.h>
 5 #include <sys/stat.h>
 6 #include <unistd.h>
 7 #include <fcntl.h>
 8 #include <string.h>
 9 #include <sys/ipc.h>
10 #include <sys/shm.h>
11 #include <sys/wait.h>
12 
13 #define MEMSIZE 1024
14 
15 int main(int argc, char *argv[])
16 {
17     char *ptr;
18     pid_t pid;
19     int shmid;
20 
21     shmid = shmget(IPC_PRIVATE, MEMSIZE, 0600);
22 
23     if ((pid=fork())<0) { 
24         perror("fork()");
25     }
26     else if (pid==0) { /*child process*/
27         ptr = shmat(shmid, NULL, 0);
28         printf("CHILD : shared memory from %p to %p\n", (void*)ptr, (void*)ptr+MEMSIZE);
29         fflush(stdout);
30         strcpy(ptr, "hello");
31         shmdt(ptr);
32         exit(0);
33     }
34     else { /*parent process*/
35         wait(NULL);
36         ptr = shmat(shmid, NULL, 0);
37         printf("PARENT : shared memory from %p to %p\n", (void*)ptr, (void*)ptr+MEMSIZE);
38         puts(ptr);
39         shmdt(ptr);
40         shmctl(shmid, IPC_RMID, NULL);
41         exit(0);
42     }
43 }

    上述代码开辟了一段shared memory(line21);child process向这段shared memory写入“hello"字符串;parent process从这段shared memory读入内容再输出到terminal上。运行结果如下:

    

    shared memory的关键在于:

    (1)shmget函数:负责理出来一块memroy专门用于shared memory,仅仅是理出来这么一块memory,并且生成了shmid这个identifier,唯一标识这块shared memory。

    (2)shmat函数:shmget函数整理出来的shared memory还不能够被其他process访问,而shmat做的事情就是启动当前process对这段shared memory的访问,把这段shared memory对接到当前process的memory space上,并返回这段共享内存在当前process中的起始地址,即上述代码中的ptr。child process和parent process中的ptr相当于这段shared memory的代言人。

    乍看上述代码的输出,为什么child和parent中的ptr的值都相同呢?分析一下不难得出结论:

    这个地址绝对不是shared memory的实际地址,而是这段shared memory在child和process内部各自的地址代言人。既然各自process中的代言人,为什么还相同呢?原因有两个:

    (1)ptr的产生方式都是 ptr = shmat(shmid ,0 ,0) 即让系统的算法自动给这段shared memory在当前process的memory layout中找到first avalialbe的地址。

    (2)parent process通过fork的方式产生child process,则child与process在memory layout上完全copy过去的。因此,按照(1)中提到的方法,在child和parent的内部获得的地址一定是相同的。

    

你可能感兴趣的:(【APUE】Chapter15 Interprocess Communication)