[apue] 神奇的 Solaris pipe

说到 pipe 你们可能都不陌生,经典的pipe调用配合fork进行父子进程通信,简直就是Unix程序的标配。html

然而Solaris上的pipe却和Solaris同样是个奇葩(虽然Solaris前途黯淡,可是不妨碍咱们从它里面挖掘一些有价值的东西),git

有着和通常pipe诸多的不一样之处,本文就来讲说Solaris上神奇的pipe和通常pipe之间的异同。github

 

1.solaris pipe 是全双工的bash

通常系统上的pipe调用是半双工的,只能单向传递数据,若是须要双向通信,咱们通常是建两个pipe分别读写。像下面这样:tcp

 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]是啥意思是一件很养脑的事。ui

不过这样的代码只能在Solaris上运行(据说BSD也支持?),若是考虑到可移植性,仍是写上面的比较稳妥。spa

 

测试程序命令行

padd2.c 

add2.c

 

 

2. solaris pipe 能够脱离父子关系创建

pipe 好用可是无法脱离fork使用,通常的pipe若是想让任意两个进程通信,得借助它的变身fifo来实现。

关于FIFO,详情可参考我以前写的一篇文章:

[apue] 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么?

而以前的例子能够,是由于请求是顺序发送的,上个请求获得应答后才发送下个请求,因此不存在这个例子的问题(可是实用性也不高)。

 

测试程序

padd3.c

add3.c

 

 

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

 也是没问题的,既没有出现多个请求粘连的状况,也没有出现请求与应答错位的状况。

 

测试程序

padd4.c

add4.c

 

 

4.结论

Solaris 上的pipe不只能够全双工通信、不依赖父子进程关系,还能够实现相似tcp那样分离多个客户端通信链接的能力。

虽然Solaris前途未卜,可是但愿一些好的东西仍是能流传下来,就好比这个神奇的pipe。

 

看完今天的文章,你是否是对特立独行的Solaris又加深了一层了解?欢迎留言区说说你认识的Solaris。

 

 

 

 

原文出处:https://www.cnblogs.com/goodcitizen/p/11937181.html

相关文章
相关标签/搜索