说到 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. IntelliJ IDEA 2019.2最新版本免费激活码

    IntelliJ IDEA 2019.2最新版本免费激活码 支持IDEA所有版本 正版授权激活码 今天更新了一下,支持java13等新功能.下面是激活码 812LFWMRSH-eyJsaWNlbnNl ...

  2. solr学习篇(二) solr 分词器篇

    关于solr7.4搭建与配置可以参考 solr7.4 安装配置篇  在这里我们探讨一下分词的配置 目录 关于分词 配置分词 验证成功 1.关于分词 1.分词是指将一个中文词语拆成若干个词,提供搜索引擎 ...

  3. Object 对象方法学习之(1)—— 使用 Object.assign 复制对象、合并对象

    作用 Object.assign() 方法用于把一个或多个源对象的可枚举属性值复制到目标对象中,返回值为目标对象. 语法 Object.assign(target, ...sources) 参数 ta ...

  4. 消息中间件-RabbitMQ环境搭建

    一直在传统行业工作(早九晚五不加班),没有考虑消息中间件的性能,所以一直再用activeMQ也没有想过学习别的中间件,时间长也没什么技术上的进步,而且感觉到了 工作的麻木,所以决定学一些新的技术(其实 ...

  5. MySQL 字段值为NULL,PHP用json转换,传给js,显示null

    这个问题出在php的json_encode环节,这个函数返回的json数据中会把空值写作null. 想通过在js端这样把null转为空字符串是不可以的: JSON.parse(JSON.stringi ...

  6. .net layui 批量导出

    .net开发,前台使用layui框架,后台使用WCF 废话不多,直接上代码 1>文件引用: admin.css layui.css layui.js jquery.min.js layerToo ...

  7. 【MySQL】MySQL忘记密码或修改密码的方法

    MySQL修改新密码方法 记得原密码情况下,修改新密码:登录到数据库后,输入 set password for 用户名@localhost = '新密码';  来设置新的密码,别忘记分号哦.如图所示: ...

  8. Java后端开发工作 - 写接口

    我在公司的工作内容是,对于一个BS应用,负责服务器端开发工作,Java语言.与前端开发人员合作,最终提供给前端RESTFUL接口,保证页面正常响应. 经验之谈 一个接口可以理解为一个业务逻辑,一个业务 ...

  9. Alibaba 镜像

      <mirrors> <mirror> <id>alimaven</id> <name>aliyun maven</name> ...

  10. SROP的一个实例

    以前一直只是大概看过这种技术,没实践过,今天刚好遇到一道题,实践了一波,确实很方便 unmoxiao@cat ~/s/pd_ubuntu> r2 -A smallest 00:54:15 War ...