[apue] 神奇的 Solaris pipe
说到 pipe 大家可能都不陌生,经典的pipe调用配合fork进行父子进程通讯,简直就是unix程序的标配。
然而solaris上的pipe却和solaris一样是个奇葩(虽然solaris前途黯淡,但是不妨碍我们从它里面挖掘一些有价值的东西),
有着和一般pipe诸多的不同之处,本文就来说说solaris上神奇的pipe和一般pipe之间的异同。
1.solaris pipe 是全双工的
一般系统上的pipe调用是半双工的,只能单向传递数据,如果需要双向通讯,我们一般是建两个pipe分别读写。像下面这样:
1 int n, fd1[2], fd2[2]; 2 if (pipe (fd1) < 0 || pipe(fd2) < 0) 3 err_sys ("pipe error"); 4 5 char line[maxline]; 6 pid_t pid = fork (); 7 if (pid < 0) 8 err_sys ("fork error"); 9 else if (pid > 0) 10 { 11 close (fd1[0]); // write on pipe1 as stdin for co-process 12 close (fd2[1]); // read on pipe2 as stdout for co-process 13 while (fgets (line, maxline, stdin) != null) { 14 n = strlen (line); 15 if (write (fd1[1], line, n) != n) 16 err_sys ("write error to pipe"); 17 if ((n = read (fd2[0], line, maxline)) < 0) 18 err_sys ("read error from pipe"); 19 20 if (n == 0) { 21 err_msg ("child closed pipe"); 22 break; 23 } 24 line[n] = 0; 25 if (fputs (line, stdout) == eof) 26 err_sys ("fputs error"); 27 } 28 29 if (ferror (stdin)) 30 err_sys ("fputs error"); 31 32 return 0; 33 } 34 else { 35 close (fd1[1]); 36 close (fd2[0]); 37 if (fd1[0] != stdin_fileno) { 38 if (dup2 (fd1[0], stdin_fileno) != stdin_fileno) 39 err_sys ("dup2 error to stdin"); 40 close (fd1[0]); 41 } 42 43 if (fd2[1] != stdout_fileno) { 44 if (dup2 (fd2[1], stdout_fileno) != stdout_fileno) 45 err_sys ("dup2 error to stdout"); 46 close (fd2[1]); 47 } 48 49 if (execl (argv[1], "add2", (char *)0) < 0) 50 err_sys ("execl error"); 51 }
这个程序创建两个管道,fd1用来写请求,fd2用来读应答;对子进程而言,fd1重定向到标准输入,fd2重定向到标准输出,读取stdin中的数据相加然后写入stdout完成工作。父进程在取得应答后向标准输出写入结果。
如果在solaris上,可以直接用一个pipe同时读写,代码可以重写成这样:
1 int fd[2]; 2 if (pipe(fd) < 0) 3 err_sys("pipe error\n"); 4 5 char line[maxline]; 6 pid_t pid = fork(); 7 if (pid < 0) 8 err_sys("fork error\n"); 9 else if (pid > 0) 10 { 11 close(fd[1]); 12 while (fgets(line, maxline, stdin) != null) { 13 n = strlen(line); 14 if (write(fd[0], line, n) != n) 15 err_sys("write error to pipe\n") 16 if ((n = read(fd[0], line, maxline)) < 0) 17 err_sys("read error from pipe\n"); 18 19 if (n == 0) 20 err_sys("child closed pipe\n"); 21 line[n] = 0; 22 if (fputs(line, stdout) == eof) 23 err_sys("fputs error\n"); 24 } 25 26 if (ferror(stdin)) 27 err_sys("fputs error\n"); 28 29 return 0; 30 } 31 else { 32 close(fd[0]); 33 if (fd[1] != stdin_fileno) 34 if (dup2(fd[1], stdin_fileno) != stdin_fileno) 35 err_sys("dup2 error to stdin\n"); 36 37 if (fd[1] != stdout_fileno) { 38 if (dup2(fd[1], stdout_fileno) != stdout_fileno) 39 err_sys("dup2 error to stdout\n"); 40 close(fd[1]); 41 } 42 43 if (execl(argv[1], argv[2], (char *)0) < 0) 44 err_sys("execl error\n"); 45 46 }
代码清爽多了,不用去考虑fd1[0]和fd2[1]是啥意思是一件很养脑的事。
不过这样的代码只能在solaris上运行(听说bsd也支持?),如果考虑到可移植性,还是写上面的比较稳妥。
测试程序
2. solaris pipe 可以脱离父子关系建立
pipe 好用但是没法脱离fork使用,一般的pipe如果想让任意两个进程通讯,得借助它的变身fifo来实现。
关于fifo,详情可参考我之前写的一篇文章:
而solaris上的pipe没这么多事,加入两个调用:fattach / fdetach,你就可以像使用fifo一样使用pipe了:
1 int fd[2]; 2 if (pipe(fd) < 0) 3 err_sys("pipe error\n"); 4 5 if (fattach(fd[1], "./pipe") < 0) 6 err_sys("fattach error\n"); 7 8 printf("attach to file pipe ok\n"); 9 10 close(fd[1]); 11 char line[maxline]; 12 while (fgets(line, maxline, stdin) != null) { 13 n = strlen(line); 14 if (write(fd[0], line, n) != n) 15 err_sys("write error to pipe\n"); 16 if ((n = read(fd[0], line, maxline)) < 0) 17 err_sys("read error from pipe\n"); 18 19 if (n == 0) 20 err_sys("child closed pipe\n"); 21 22 line[n] = 0; 23 if (fputs(line, stdout) == eof) 24 err_sys("fputs error\n"); 25 } 26 27 if (ferror(stdin)) 28 err_sys("fputs error\n"); 29 30 if (fdetach("./pipe") < 0) 31 err_sys("fdetach error\n"); 32 33 printf("detach from file pipe ok\n");
在pipe调用之后立即加入fattach调用,可以将管道关联到文件系统的一个文件名上,该文件必需事先存在,且可读可写。
在fattach调用之前这个文件(./pipe)是个普通文件,打开读写都是磁盘io;
在fattach调用之后,这个文件就变身成为一个管道了,打开读写都是内存流操作,且管道的另一端就是attach的那个进程。
子进程也需要改造一下,以便使用pipe通讯:
1 int fd, n, int1, int2; 2 char line[maxline]; 3 fd = open("./pipe", o_rdwr); 4 if (fd < 0) 5 err_sys("open file pipe failed\n"); 6 7 printf("open file pipe ok, fd = %d\n", fd); 8 while ((n = read(fd, line, maxline)) > 0) { 9 line[n] = 0; 10 if (sscanf(line, "%d%d", &int1, &int2) == 2) { 11 sprintf(line, "%d\n", int1 + int2); 12 n = strlen(line); 13 if (write(fd, line, n) != n) 14 err_sys("write error\n"); 15 16 printf("i am working on %s\n", line); 17 } 18 else { 19 if (write(fd, "invalid args\n", 13) != 13) 20 err_sys("write msg error\n"); 21 } 22 } 23 24 close(fd);
打开pipe就如同打开普通文件一样,open直接搞定。当然前提是attach进程必需已经在运行。
当attach进程detach后,管道文件又将恢复它的本来面目。
脱离了父子关系的pipe其实可以建立多对一关系(多对多关系不可以,因为只能有一个进程attach)。
例如开4个cmd窗口,分别执行以下命令:
./padd2 abc
./add2
./add2
./add2
向attach进程(padd2)发送9个计算请求后,可以看到输出结果如下:
-bash-3.2$ ./padd2 abc
attach to file pipe ok
1 1
2
2 2
4
3 3
6
4 4
8
5 5
10
6 6
12
7 7
14
8 8
16
9 9
18
再回来看各个open管道的进程,输出分别如下:
-bash-3.2$ ./add2
open file pipe ok, fd = 3
source: 1 1
i am working on 2
source: 4 4
i am working on 8
source: 7 7
i am working on 14
-bash-3.2$ ./add2
open file pipe ok, fd = 3
source: 2 2
i am working on 4
source: 5 5
i am working on 10
source: 9 9
i am working on 18
-bash-3.2$ ./add2
open file pipe ok, fd = 3
source: 2 2
i am working on 4
source: 5 5
i am working on 10
source: 9 9
i am working on 18
-bash-3.2$ ./add2
open file pipe ok, fd = 3
source: 3 3
i am working on 6
source: 6 6
i am working on 12
source: 8 8
i am working on 16
可以发现一个很有趣的现象,就是各个add2进程基本是轮着来获取请求的,可以猜想底层的pipe可能有一个进程排队机制。
但是反过来使用pipe就不行了。就是说当启动一个add3(区别于上例的add2与padd2)作为fattach端打开pipe,启动多个padd3作为open端使用pipe,
然后通过命令行给padd3传递要相加的值,可以写一个脚本同时启动多个padd3,来查看效果:
#! /bin/sh
./padd3 1 1 &
./padd3 2 2 &
./padd3 3 3 &
./padd3 4 4 &
这个脚本中启动了4个加法进程,同时向add3发送4个加法请求,脚本中四个进程输出如下:
-bash-3.2$ ./padd3.sh
-bash-3.2$ open file pipe ok, fd = 3
1 1 = 2
open file pipe ok, fd = 3
2 2 = 4
open file pipe ok, fd = 3
open file pipe ok, fd = 3
4 4 = 37
可以看到3+3的请求被忽略了,转到add3查看输出:
-bash-3.2$ ./add3
attach to file pipe ok
source: 1 1
i am working on 1 + 1 = 2
source: 2 2
i am working on 2 + 2 = 4
source: 3 34 4
i am working on 3 + 34 = 37
原来是3+3与4+4两个请求粘连了,导致add3识别成一个3+34的请求,所以出错了。
多运行几遍脚本后,发现还有这样的输出:
-bash-3.2$ ./padd3.sh
-bash-3.2$ open file pipe ok, fd = 3
4 4 = 2
open file pipe ok, fd = 3
2 2 = 4
open file pipe ok, fd = 3
3 3 = 6
open file pipe ok, fd = 3
1 1 = 8
4+4=2?1+1=8?再看add3这头的输出:
-bash-3.2$ ./add3
attach to file pipe ok
source: 1 1
i am working on 1 + 1 = 2
source: 2 2
i am working on 2 + 2 = 4
source: 3 3
i am working on 3 + 3 = 6
source: 4 4
i am working on 4 + 4 = 8
完全正常呢。
经过一番推理,发现是4+4的请求取得了1+1请求的应答;1+1的请求取得了4+4的应答。
可见这样的结构还有一个弊端,同时请求的进程可能无法得到自己的应答,应答与请求之间相互错位。
所以想用fattach来实现多路请求的人还是洗洗睡吧,毕竟它就是一个pipe不是,还能给它整成tcp么?
而之前的例子可以,是因为请求是顺序发送的,上个请求得到应答后才发送下个请求,所以不存在这个例子的问题(但是实用性也不高)。
测试程序
3. solaris pipe 可以通过connld模块实现类似tcp的多路连接
第2条刚说不能实现多路连接,第3条就接着来打脸了,这是由于solaris上的pipe都是基于streams技术构建,
而streams是支持灵活的push、pop流处理模块的,再加上streams传递文件fd的能力,就可以支持类似tcp中accept的能力。
即每个open pipe文件的进程,得到的不是原来管道的fd,而是新创建管道的fd,而管道的另一侧fd则通过已有的管道发送到attach进程,
后者使用这个新的fd与客户进程通讯。为了支持多路连接,我们的代码需要重新整理一下,首先看客户端:
1 int fd; 2 char line[maxline]; 3 fd = cli_conn("./pipe"); 4 if (fd < 0) 5 return 0;
这里将open相关逻辑封装到了cli_conn函数中,以便之后复用:
1 int cli_conn(const char *name) 2 { 3 int fd; 4 if ((fd = open(name, o_rdwr)) < 0) { 5 printf("open pipe file failed\n"); 6 return -1; 7 } 8 9 if (isastream(fd) == 0) { 10 close(fd); 11 return -2; 12 } 13 14 return fd; 15 }
可以看到与之前几乎没有变化,只是增加了isastream调用防止attach进程没有启动。
再来看下服务端:
1 int listenfd = serv_listen("./pipe"); 2 if (listenfd < 0) 3 return 0; 4 5 int acceptfd = 0; 6 int n = 0, int1 = 0, int2 = 0; 7 char line[maxline]; 8 uid_t uid = 0; 9 while ((acceptfd = serv_accept(listenfd, &uid)) >= 0) 10 { 11 printf("accept a client, fd = %d, uid = %ld\n", acceptfd, uid); 12 while ((n = read(acceptfd, line, maxline)) > 0) { 13 line[n] = 0; 14 printf("source: %s\n", line); 15 if (sscanf(line, "%d%d", &int1, &int2) == 2) { 16 sprintf(line, "%d\n", int1 + int2); 17 n = strlen(line); 18 if (write(acceptfd, line, n) != n) { 19 printf("write error\n"); 20 return 0; 21 } 22 printf("i am working on %d + %d = %s\n", int1, int2, line); 23 } 24 else { 25 if (write(acceptfd, "invalid args\n", 13) != 13) { 26 printf("write msg error\n"); 27 return 0; 28 } 29 } 30 } 31 32 close(acceptfd); 33 } 34 35 if (fdetach("./pipe") < 0) { 36 printf("fdetach error\n"); 37 return 0; 38 } 39 40 printf("detach from file pipe ok\n"); 41 close(listenfd);
首先调用serv_listen建立基本pipe,然后不断在该pipe上调用serv_accept来获取独立的客户端连接。之后的逻辑与以前一样。
现在重点看下封装的这两个方法:
1 int serv_listen(const char *name) 2 { 3 int tempfd; 4 int fd[2]; 5 unlink(name); 6 tempfd = creat(name, fifo_mode); 7 if (tempfd < 0) { 8 printf("creat failed\n"); 9 return -1; 10 } 11 12 if (close(tempfd) < 0) { 13 printf("close temp fd failed\n"); 14 return -2; 15 } 16 17 if (pipe(fd) < 0) { 18 printf("pipe error\n"); 19 return -3; 20 } 21 22 if (ioctl(fd[1], i_push, "connld") < 0) { 23 printf("i_push connld failed\n"); 24 close(fd[0]); 25 close(fd[1]); 26 return -4; 27 } 28 29 printf("push connld ok\n"); 30 if (fattach(fd[1], name) < 0) { 31 printf("fattach error\n"); 32 close(fd[0]); 33 close(fd[1]); 34 return -5; 35 } 36 37 printf("attach to file pipe ok\n"); 38 close(fd[1]); 39 return fd[0]; 40 }
serv_listen封装了与建立基本pipe相关的代码,首先确保pipe文件存在且可读写,然后创建普通的pipe,在fattach调用之前必需先push一个connld模块到该pipe stream中。这样就大功告成!
1 int serv_accept(int listenfd, uid_t *uidptr) 2 { 3 struct strrecvfd recvfd; 4 if (ioctl(listenfd, i_recvfd, &recvfd) < 0) { 5 printf("i_recvfd from listen fd failed\n"); 6 return -1; 7 } 8 9 if (uidptr) 10 *uidptr = recvfd.uid; 11 12 return recvfd.fd; 13 }
当有客户端连接上来的时候,使用i_recvfd接收connld返回的另一个pipe的fd。之后的数据将在该pipe进行。
看了看,感觉和tcp的listen与accept别无二致,看来天下武功,至精深处都是英雄所见略同。
之前的多个客户端同时运行的例子再跑一遍,观察attach端输出:
-bash-3.2$ ./add4
push connld ok
attach to file pipe ok
accept a client, fd = 4, uid = 101
source: 1 1
i am working on 1 + 1 = 2
accept a client, fd = 4, uid = 101
source: 2 2
i am working on 2 + 2 = 4
accept a client, fd = 4, uid = 101
source: 3 3
i am working on 3 + 3 = 6
accept a client, fd = 4, uid = 101
source: 4 4
i am working on 4 + 4 = 8
一切正常。再看下脚本中四个进程的输出:
-bash-3.2$ ./padd4.sh
-bash-3.2$ open file pipe ok, fd = 3
1 1 = 2
open file pipe ok, fd = 3
2 2 = 4
open file pipe ok, fd = 3
3 3 = 6
open file pipe ok, fd = 3
4 4 = 8
也是没问题的,既没有出现多个请求粘连的情况,也没有出现请求与应答错位的情况。
测试程序
4.结论
solaris 上的pipe不仅可以全双工通讯、不依赖父子进程关系,还可以实现类似tcp那样分离多个客户端通讯连接的能力。
虽然solaris前途未卜,但是希望一些好的东西还是能流传下来,就比如这个神奇的pipe。
看完今天的文章,你是不是对特立独行的solaris又加深了一层了解?欢迎留言区说说你认识的solaris。
下一篇: 时间复杂度