• [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
    ");
     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(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
    ")
    16         if ((n = read(fd[0], line, MAXLINE)) < 0) 
    17             err_sys("read error from pipe
    ");
    18 
    19         if (n == 0) 
    20             err_sys("child closed pipe
    ");
    21         line[n] = 0;
    22         if (fputs(line, stdout) == EOF) 
    23             err_sys("fputs error
    ");
    24     }
    25 
    26     if (ferror(stdin))
    27         err_sys("fputs error
    ");
    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
    ");
    36 
    37     if (fd[1] != STDOUT_FILENO) {
    38         if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)
    39             err_sys("dup2 error to stdout
    ");
    40         close(fd[1]);
    41     }
    42 
    43     if (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了:

     1 int fd[2];
     2 if (pipe(fd) < 0)
     3     err_sys("pipe error
    ");
     4 
     5 if (fattach(fd[1], "./pipe") < 0)
     6     err_sys("fattach error
    ");
     7 
     8 printf("attach to file pipe ok
    ");
     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
    ");
    16     if ((n = read(fd[0], line, MAXLINE)) < 0)
    17         err_sys("read error from pipe
    ");
    18 
    19     if (n == 0) 
    20         err_sys("child closed pipe
    ");
    21 
    22     line[n] = 0;
    23     if (fputs(line, stdout) == EOF)
    24         err_sys("fputs error
    ");
    25 }
    26 
    27 if (ferror(stdin))
    28     err_sys("fputs error
    ");
    29 
    30 if (fdetach("./pipe") < 0)
    31     err_sys("fdetach error
    ");
    32 
    33 printf("detach from file pipe ok
    ");

    在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
    ");
     6 
     7 printf("open file pipe ok, fd = %d
    ", 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
    ", int1 + int2);
    12         n = strlen(line);
    13         if (write(fd, line, n) != n)
    14             err_sys("write error
    ");
    15 
    16         printf("i am working on %s
    ", line);
    17     }
    18     else {
    19         if (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与客户进程通讯。为了支持多路连接,我们的代码需要重新整理一下,首先看客户端:

    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
    ");
     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
    ", acceptfd, uid);
    12     while ((n = read(acceptfd, line, MAXLINE)) > 0) {
    13         line[n] = 0;
    14         printf("source: %s
    ", line);
    15         if (sscanf(line, "%d%d", &int1, &int2) == 2) {
    16             sprintf(line, "%d
    ", int1 + int2);
    17             n = strlen(line);
    18             if (write(acceptfd, line, n) != n) {
    19                 printf("write error
    ");
    20                 return 0;
    21             }
    22             printf("i am working on %d + %d = %s
    ", int1, int2, line);
    23         }
    24         else {
    25             if (write(acceptfd, "invalid args
    ", 13) != 13) {
    26                 printf("write msg error
    ");
    27                 return 0;
    28             }
    29         }
    30     }
    31 
    32     close(acceptfd);
    33 }
    34 
    35 if (fdetach("./pipe") < 0) {
    36     printf("fdetach error
    ");
    37     return 0;
    38 }
    39 
    40 printf("detach from file pipe ok
    ");
    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
    ");
     9         return -1;
    10     }
    11 
    12     if (close(tempfd) < 0) {
    13         printf("close temp fd failed
    ");
    14         return -2;
    15     }
    16 
    17     if (pipe(fd) < 0) {
    18         printf("pipe error
    ");
    19         return -3;
    20     }
    21 
    22     if (ioctl(fd[1], I_PUSH, "connld") < 0) {
    23         printf("I_PUSH connld failed
    ");
    24         close(fd[0]);
    25         close(fd[1]);
    26         return -4;
    27     }
    28 
    29     printf("push connld ok
    ");
    30     if (fattach(fd[1], name) < 0) {
    31         printf("fattach error
    ");
    32         close(fd[0]);
    33         close(fd[1]);
    34         return -5;
    35     }
    36 
    37     printf("attach to file pipe ok
    ");
    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
    ");
     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。

  • 相关阅读:
    Kafka 入门(四)-- Python Kafka Client 性能测试
    XShell连接阿里云服务器出现”用户密钥加载失败:请确定输入的密码“处理办法
    优化自动化测试流程,使用 flask 开发一个 toy jenkins工具
    我做了回视频,告诉你需要用到哪些工具
    提问的基本原则
    12 月31 日返利系统问题复盘
    外部prometheus监控k8s(k3s)集群
    一个Java类在运行时候,变量是怎么在JVM中分布的呢?
    JVM学习第二篇思考:一个Java代码是怎么运行起来的-下篇
    JVM学习第一篇思考:一个Java代码是怎么运行起来的-上篇
  • 原文地址:https://www.cnblogs.com/goodcitizen/p/11937181.html
Copyright © 2020-2023  润新知