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

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

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

1.solaris pipe 是全双工的

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

     int n, fd1[], fd2[];
if (pipe (fd1) < || pipe(fd2) < )
err_sys ("pipe error"); char line[MAXLINE];
pid_t pid = fork ();
if (pid < )
err_sys ("fork error");
else if (pid > )
{
close (fd1[]); // write on pipe1 as stdin for co-process
close (fd2[]); // read on pipe2 as stdout for co-process
while (fgets (line, MAXLINE, stdin) != NULL) {
n = strlen (line);
if (write (fd1[], line, n) != n)
err_sys ("write error to pipe");
if ((n = read (fd2[], line, MAXLINE)) < )
err_sys ("read error from pipe"); if (n == ) {
err_msg ("child closed pipe");
break;
}
line[n] = ;
if (fputs (line, stdout) == EOF)
err_sys ("fputs error");
} if (ferror (stdin))
err_sys ("fputs error"); return ;
}
else {
close (fd1[]);
close (fd2[]);
if (fd1[] != STDIN_FILENO) {
if (dup2 (fd1[], STDIN_FILENO) != STDIN_FILENO)
err_sys ("dup2 error to stdin");
close (fd1[]);
} if (fd2[] != STDOUT_FILENO) {
if (dup2 (fd2[], STDOUT_FILENO) != STDOUT_FILENO)
err_sys ("dup2 error to stdout");
close (fd2[]);
} if (execl (argv[], "add2", (char *)) < )
err_sys ("execl error");
}

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

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

 int fd[];
if (pipe(fd) < )
err_sys("pipe error\n"); char line[MAXLINE];
pid_t pid = fork();
if (pid < )
err_sys("fork error\n");
else if (pid > )
{
close(fd[]);
while (fgets(line, MAXLINE, stdin) != NULL) {
n = strlen(line);
if (write(fd[], line, n) != n)
err_sys("write error to pipe\n")
if ((n = read(fd[], line, MAXLINE)) < )
err_sys("read error from pipe\n"); if (n == )
err_sys("child closed pipe\n");
line[n] = ;
if (fputs(line, stdout) == EOF)
err_sys("fputs error\n");
} if (ferror(stdin))
err_sys("fputs error\n"); return ;
}
else {
close(fd[]);
if (fd[] != STDIN_FILENO)
if (dup2(fd[], STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin\n"); if (fd[] != STDOUT_FILENO) {
if (dup2(fd[], STDOUT_FILENO) != STDOUT_FILENO)
err_sys("dup2 error to stdout\n");
close(fd[]);
} if (execl(argv[], argv[], (char *)) < )
err_sys("execl error\n"); }

代码清爽多了,不用去考虑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了:

 int fd[];
if (pipe(fd) < )
err_sys("pipe error\n"); if (fattach(fd[], "./pipe") < )
err_sys("fattach error\n"); printf("attach to file pipe ok\n"); close(fd[]);
char line[MAXLINE];
while (fgets(line, MAXLINE, stdin) != NULL) {
n = strlen(line);
if (write(fd[], line, n) != n)
err_sys("write error to pipe\n");
if ((n = read(fd[], line, MAXLINE)) < )
err_sys("read error from pipe\n"); if (n == )
err_sys("child closed pipe\n"); line[n] = ;
if (fputs(line, stdout) == EOF)
err_sys("fputs error\n");
} if (ferror(stdin))
err_sys("fputs error\n"); if (fdetach("./pipe") < )
err_sys("fdetach error\n"); printf("detach from file pipe ok\n");

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

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

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

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

 int fd, n, int1, int2;
char line[MAXLINE];
fd = open("./pipe", O_RDWR);
if (fd < )
err_sys("open file pipe failed\n"); printf("open file pipe ok, fd = %d\n", fd);
while ((n = read(fd, line, MAXLINE)) > ) {
line[n] = ;
if (sscanf(line, "%d%d", &int1, &int2) == ) {
sprintf(line, "%d\n", int1 + int2);
n = strlen(line);
if (write(fd, line, n) != n)
err_sys("write error\n"); printf("i am working on %s\n", line);
}
else {
if (write(fd, "invalid args\n", ) != )
err_sys("write msg error\n");
}
} 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与客户进程通讯。为了支持多路连接,我们的代码需要重新整理一下,首先看客户端:

 int fd;
char line[MAXLINE];
fd = cli_conn("./pipe");
if (fd < )
return ;

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

 int cli_conn(const char *name)
{
int fd;
if ((fd = open(name, O_RDWR)) < ) {
printf("open pipe file failed\n");
return -;
} if (isastream(fd) == ) {
close(fd);
return -;
} return fd;
}

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

再来看下服务端:

 int listenfd = serv_listen("./pipe");
if (listenfd < )
return ; int acceptfd = ;
int n = , int1 = , int2 = ;
char line[MAXLINE];
uid_t uid = ;
while ((acceptfd = serv_accept(listenfd, &uid)) >= )
{
printf("accept a client, fd = %d, uid = %ld\n", acceptfd, uid);
while ((n = read(acceptfd, line, MAXLINE)) > ) {
line[n] = ;
printf("source: %s\n", line);
if (sscanf(line, "%d%d", &int1, &int2) == ) {
sprintf(line, "%d\n", int1 + int2);
n = strlen(line);
if (write(acceptfd, line, n) != n) {
printf("write error\n");
return ;
}
printf("i am working on %d + %d = %s\n", int1, int2, line);
}
else {
if (write(acceptfd, "invalid args\n", ) != ) {
printf("write msg error\n");
return ;
}
}
} close(acceptfd);
} if (fdetach("./pipe") < ) {
printf("fdetach error\n");
return ;
} printf("detach from file pipe ok\n");
close(listenfd);

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

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

 int serv_listen(const char *name)
{
int tempfd;
int fd[];
unlink(name);
tempfd = creat(name, FIFO_MODE);
if (tempfd < ) {
printf("creat failed\n");
return -;
} if (close(tempfd) < ) {
printf("close temp fd failed\n");
return -;
} if (pipe(fd) < ) {
printf("pipe error\n");
return -;
} if (ioctl(fd[], I_PUSH, "connld") < ) {
printf("I_PUSH connld failed\n");
close(fd[]);
close(fd[]);
return -;
} printf("push connld ok\n");
if (fattach(fd[], name) < ) {
printf("fattach error\n");
close(fd[]);
close(fd[]);
return -;
} printf("attach to file pipe ok\n");
close(fd[]);
return fd[];
}

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

 int serv_accept(int listenfd, uid_t *uidptr)
{
struct strrecvfd recvfd;
if (ioctl(listenfd, I_RECVFD, &recvfd) < ) {
printf("I_RECVFD from listen fd failed\n");
return -;
} if (uidptr)
*uidptr = recvfd.uid; return recvfd.fd;
}

当有客户端连接上来的时候,使用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] 神奇的 Solaris pipe的更多相关文章

  1. apue 外传

    先上目录 chapter 3 [apue] dup2的正确打开方式 chapter 10 [apue] 等待子进程的那些事儿 chapter 14 [apue] 使用文件记录锁无法实现父子进程交互执行 ...

  2. [apue] 标准 I/O 库那些事儿

    前言 标准 IO 库自 1975 年诞生以来,至今接近 50 年了,令人惊讶的是,这期间只对它做了非常小的修改.除了耳熟能详的 printf/scanf,回过头来对它做个全方位的审视,看看到底优秀在哪 ...

  3. 【APUE】Chapter16 Network IPC: Sockets & makefile写法学习

    16.1 Introduction Chapter15讲的是同一个machine之间不同进程的通信,这一章内容是不同machine之间通过network通信,切入点是socket. 16.2 Sock ...

  4. gcc使用备忘

    本文为原创文章,转载请指明该文链接 Options Controling the kind of Output -x language 明确说明输入文件的编码语言,没有该选项的话, gcc 会根据输入 ...

  5. stat中的st_dev和st_rdev

    目录 stat中的st_dev和st_rdev title: stat中的st_dev和st_rdev date: 2019/11/27 21:04:25 toc: true --- stat中的st ...

  6. [15]APUE:pipe / FIFO

    管道 pipe 一.概述 管道(pipe / FIFO)是一种文件,属于 pipefs 文件系统类型,可以使用 read.write.close 等系统调用进行操作 其本质是内核维护了一块缓冲区与管道 ...

  7. APUE学习笔记——10 信号

    信号的基本概念     信号是软件中断,信号提供了解决异步时间的方法.     每一中信号都有一个名字,信号名以SIG开头. 产生信号的几种方式     很多条件可以产生信号:     终端交互:用户 ...

  8. [apue] linux 文件系统那些事儿

    前言 说到 linux 的文件系统,好多人第一印象是 ext2/ext3/ext4 等具体的文件系统,本文不涉及这些,因为研究具体的文件系统难免会陷入细节,甚至拉大段的源码做分析,反而不能从宏观的角度 ...

  9. APUE学习之多线程编程(一):线程的创建和销毁

    一.线程标识      和每个进程都有一个进程ID一样,每个线程也有一个线程ID,线程ID是以pthread_t数据类型来表示的,在Linux中,用无符号长整型表示pthread_t,Solaris ...

随机推荐

  1. mp-vue实现小程序回顶操作踩坑,wx.pageScrollTo使用无效填坑

    本来项目都写的差不多了,测试测着侧着就冒出了新的想法,我因为做的是问卷,因此会有用户必答题未答完的可能存在,本来市场部给的需求就是做一个弹窗就好了,她说想要做出跳回到用户未答的第一道题,好吧,既然都这 ...

  2. 模块基础 day15

    目录 模块的四种形式 内置模块 pip安装的模块 自定义模块 包(模块) import和from···import 循环导入 模块的搜索路径 python文件的两种用途 模块的四种形式 模块就是一系列 ...

  3. Python基础入门总结

    Python基础入门教学 基础中的基础 列表.元组(tuple).字典.字符串 变量和引用 函数 python视频教程下载 基础中的基础 解释型语言和编译型语言差距: Python概述 解释器执行原理 ...

  4. DB2中的MQT优化机制详解和实践

    MQT :物化查询表.是以一次查询的结果为基础  定义创建的表(实表),以量取胜(特别是在百万,千万级别的量,效果更显著),可以更快的查询到我们需要的结果.MQT有两种类型,一种是系统维护的MQT , ...

  5. CVE-2019-17671:Wordpress未授权访问漏洞复现

    0x00 简介 WordPress是一款个人博客系统,并逐步演化成一款内容管理系统软件,它是使用PHP语言和MySQL数据库开发的,用户可以在支持 PHP 和 MySQL数据库的服务器上使用自己的博客 ...

  6. 【BZOJ4720】【UOJ262】【NOIP2016】换教室

    Description 对于刚上大学的牛牛来说,他面临的第一个问题是如何根据实际情况申请合适的课程. 在可以选择的课程中,有 2n 节课程安排在n个时间段上.在第 i(1≤i≤n)个时间段上,两节内容 ...

  7. mysql设计规范二

    一.基本规范 必须使用InnoDB存储引擎 必须使用UTF8字符集 数据表.数据字段必须加入中文注释 二.设计规范 库名称.表名称.字段名称必须使用小写,最好不要使用驼峰式,使用“_”区分,例如use ...

  8. NOIP模拟 29

    T1第一眼觉得是网络流 看见4e6条边200次增广我犹豫了 O(n)都过不去的赶脚.. 可是除了网络流板子我还会什么呢 于是交了个智障的EK 还是用dijkstra跑的 居然有50分!$(RP--)$ ...

  9. 1005 csp-s 60 凉凉

    T1 嘟嘟噜 上来一看数据范围1e9就蒙蔽,然后不知所措的打了一个 $ O(n)$的无脑算法,由于本人真的脑小,导致O(n)的柿子推了好长时间,导致心态崩了,然后........ 今天能明白了log的 ...

  10. NOIP 模拟19

    考试状态一次不如一次,所以这次.......我经无言以对 考完试T1就A了,但不是考试时A的,所以屁用没有! 这次考试其实T1想的是正解但是自己傻逼了,感觉自己只能拿部分分,(而且我还把数据范围少看一 ...