[apue]神奇的Solarispipe

编程

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

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

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

 

1.solaris pipe 是全双工的

一般系统上的pipe调用是半双工的,只能单向传递数据,如果需要双向通讯,我们一般是建两个pipe分别读写。像下面这样:

 1int n, fd1[2], fd2[2]; 

2if (pipe (fd1) < 0 || pipe(fd2) < 0)

3 err_sys ("pipe error");

4

5char line[MAXLINE];

6 pid_t pid = fork ();

7if (pid < 0)

8 err_sys ("fork error");

9elseif (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

13while (fgets (line, MAXLINE, stdin) != NULL) {

14 n = strlen (line);

15if (write (fd1[1], line, n) != n)

16 err_sys ("write error to pipe");

17if ((n = read (fd2[0], line, MAXLINE)) < 0)

18 err_sys ("read error from pipe");

19

20if (n == 0) {

21 err_msg ("child closed pipe");

22break;

23 }

24 line[n] = 0;

25if (fputs (line, stdout) == EOF)

26 err_sys ("fputs error");

27 }

28

29if (ferror (stdin))

30 err_sys ("fputs error");

31

32return0;

33 }

34else {

35 close (fd1[1]);

36 close (fd2[0]);

37if (fd1[0] != STDIN_FILENO) {

38if (dup2 (fd1[0], STDIN_FILENO) != STDIN_FILENO)

39 err_sys ("dup2 error to stdin");

40 close (fd1[0]);

41 }

42

43if (fd2[1] != STDOUT_FILENO) {

44if (dup2 (fd2[1], STDOUT_FILENO) != STDOUT_FILENO)

45 err_sys ("dup2 error to stdout");

46 close (fd2[1]);

47 }

48

49if (execl (argv[1], "add2", (char *)0) < 0)

50 err_sys ("execl error");

51 }

这个程序创建两个管道,fd1用来写请求,fd2用来读应答;对子进程而言,fd1重定向到标准输入,fd2重定向到标准输出,读取stdin中的数据相加然后写入stdout完成工作。父进程在取得应答后向标准输出写入结果。

如果在Solaris上,可以直接用一个pipe同时读写,代码可以重写成这样:

 1int fd[2];

2if (pipe(fd) < 0)

3 err_sys("pipe error

");

4

5char line[MAXLINE];

6 pid_t pid = fork();

7if (pid < 0)

8 err_sys("fork error

");

9elseif (pid > 0)

10{

11 close(fd[1]);

12while (fgets(line, MAXLINE, stdin) != NULL) {

13 n = strlen(line);

14if (write(fd[0], line, n) != n)

15 err_sys("write error to pipe

")

16if ((n = read(fd[0], line, MAXLINE)) < 0)

17 err_sys("read error from pipe

");

18

19if (n == 0)

20 err_sys("child closed pipe

");

21 line[n] = 0;

22if (fputs(line, stdout) == EOF)

23 err_sys("fputs error

");

24 }

25

26if (ferror(stdin))

27 err_sys("fputs error

");

28

29return0;

30}

31else {

32 close(fd[0]);

33if (fd[1] != STDIN_FILENO)

34if (dup2(fd[1], STDIN_FILENO) != STDIN_FILENO)

35 err_sys("dup2 error to stdin

");

36

37if (fd[1] != STDOUT_FILENO) {

38if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)

39 err_sys("dup2 error to stdout

");

40 close(fd[1]);

41 }

42

43if (execl(argv[1], argv[2], (char *)0) < 0)

44 err_sys("execl error

");

45

46 }

代码清爽多了,不用去考虑fd1[0]和fd2[1]是啥意思是一件很养脑的事。

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

 

测试程序

padd2.c 

add2.c

 

 

2. solaris pipe 可以脱离父子关系建立

pipe 好用但是没法脱离fork使用,一般的pipe如果想让任意两个进程通讯,得借助它的变身fifo来实现。

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

[apue] FIFO:不是文件的文件

 

而Solaris上的pipe没这么多事,加入两个调用:fattach / fdetach,你就可以像使用FIFO一样使用pipe了:

 1int fd[2];

2if (pipe(fd) < 0)

3 err_sys("pipe error

");

4

5if (fattach(fd[1], "./pipe") < 0)

6 err_sys("fattach error

");

7

8 printf("attach to file pipe ok

");

9

10 close(fd[1]);

11char line[MAXLINE];

12while (fgets(line, MAXLINE, stdin) != NULL) {

13 n = strlen(line);

14if (write(fd[0], line, n) != n)

15 err_sys("write error to pipe

");

16if ((n = read(fd[0], line, MAXLINE)) < 0)

17 err_sys("read error from pipe

");

18

19if (n == 0)

20 err_sys("child closed pipe

");

21

22 line[n] = 0;

23if (fputs(line, stdout) == EOF)

24 err_sys("fputs error

");

25}

26

27if (ferror(stdin))

28 err_sys("fputs error

");

29

30if (fdetach("./pipe") < 0)

31 err_sys("fdetach error

");

32

33 printf("detach from file pipe ok

");

在pipe调用之后立即加入fattach调用,可以将管道关联到文件系统的一个文件名上,该文件必需事先存在,且可读可写。

在fattach调用之前这个文件(./pipe)是个普通文件,打开读写都是磁盘IO;

在fattach调用之后,这个文件就变身成为一个管道了,打开读写都是内存流操作,且管道的另一端就是attach的那个进程。

子进程也需要改造一下,以便使用pipe通讯:

 1int fd, n, int1, int2;

2char line[MAXLINE];

3 fd = open("./pipe", O_RDWR);

4if (fd < 0)

5 err_sys("open file pipe failed

");

6

7 printf("open file pipe ok, fd = %d

", fd);

8while ((n = read(fd, line, MAXLINE)) > 0) {

9 line[n] = 0;

10if (sscanf(line, "%d%d", &int1, &int2) == 2) {

11 sprintf(line, "%d

", int1 + int2);

12 n = strlen(line);

13if (write(fd, line, n) != n)

14 err_sys("write error

");

15

16 printf("i am working on %s

", line);

17 }

18else {

19if (write(fd, "invalid args

", 13) != 13)

20 err_sys("write msg error

");

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与客户进程通讯。为了支持多路连接,我们的代码需要重新整理一下,首先看客户端:

1int fd;

2char line[MAXLINE];

3 fd = cli_conn("./pipe");

4if (fd < 0)

5return0;

这里将open相关逻辑封装到了cli_conn函数中,以便之后复用:

 1int cli_conn(constchar *name)

2{

3int fd;

4if ((fd = open(name, O_RDWR)) < 0) {

5 printf("open pipe file failed

");

6return -1;

7 }

8

9if (isastream(fd) == 0) {

10 close(fd);

11return -2;

12 }

13

14return fd;

15 }

可以看到与之前几乎没有变化,只是增加了isastream调用防止attach进程没有启动。

再来看下服务端:

 1int listenfd = serv_listen("./pipe");

2if (listenfd < 0)

3return0;

4

5int acceptfd = 0;

6int n = 0, int1 = 0, int2 = 0;

7char line[MAXLINE];

8 uid_t uid = 0;

9while ((acceptfd = serv_accept(listenfd, &uid)) >= 0)

10{

11 printf("accept a client, fd = %d, uid = %ld

", acceptfd, uid);

12while ((n = read(acceptfd, line, MAXLINE)) > 0) {

13 line[n] = 0;

14 printf("source: %s

", line);

15if (sscanf(line, "%d%d", &int1, &int2) == 2) {

16 sprintf(line, "%d

", int1 + int2);

17 n = strlen(line);

18if (write(acceptfd, line, n) != n) {

19 printf("write error

");

20return0;

21 }

22 printf("i am working on %d + %d = %s

", int1, int2, line);

23 }

24else {

25if (write(acceptfd, "invalid args

", 13) != 13) {

26 printf("write msg error

");

27return0;

28 }

29 }

30 }

31

32 close(acceptfd);

33}

34

35if (fdetach("./pipe") < 0) {

36 printf("fdetach error

");

37return0;

38}

39

40 printf("detach from file pipe ok

");

41 close(listenfd);

首先调用serv_listen建立基本pipe,然后不断在该pipe上调用serv_accept来获取独立的客户端连接。之后的逻辑与以前一样。

现在重点看下封装的这两个方法:

 1int serv_listen(constchar *name)

2{

3int tempfd;

4int fd[2];

5 unlink(name);

6 tempfd = creat(name, FIFO_MODE);

7if (tempfd < 0) {

8 printf("creat failed

");

9return -1;

10 }

11

12if (close(tempfd) < 0) {

13 printf("close temp fd failed

");

14return -2;

15 }

16

17if (pipe(fd) < 0) {

18 printf("pipe error

");

19return -3;

20 }

21

22if (ioctl(fd[1], I_PUSH, "connld") < 0) {

23 printf("I_PUSH connld failed

");

24 close(fd[0]);

25 close(fd[1]);

26return -4;

27 }

28

29 printf("push connld ok

");

30if (fattach(fd[1], name) < 0) {

31 printf("fattach error

");

32 close(fd[0]);

33 close(fd[1]);

34return -5;

35 }

36

37 printf("attach to file pipe ok

");

38 close(fd[1]);

39return fd[0];

40 }

serv_listen封装了与建立基本pipe相关的代码,首先确保pipe文件存在且可读写,然后创建普通的pipe,在fattach调用之前必需先PUSH一个connld模块到该pipe STREAM中。这样就大功告成!

 1int serv_accept(int listenfd, uid_t *uidptr)

2{

3struct strrecvfd recvfd;

4if (ioctl(listenfd, I_RECVFD, &recvfd) < 0) {

5 printf("I_RECVFD from listen fd failed

");

6return -1;

7 }

8

9if (uidptr)

10 *uidptr = recvfd.uid;

11

12return 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。

 

 

 

 

以上是 [apue]神奇的Solarispipe 的全部内容, 来源链接: utcz.com/z/511215.html

回到顶部