[apue] 神奇的 Solaris pipe
说到 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也支持?),如果考虑到可移植性,还是写上面的比较稳妥。
测试程序
2. solaris pipe 可以脱离父子关系建立
pipe 好用但是没法脱离fork使用,一般的pipe如果想让任意两个进程通讯,得借助它的变身fifo来实现。
关于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么?
而之前的例子可以,是因为请求是顺序发送的,上个请求得到应答后才发送下个请求,所以不存在这个例子的问题(但是实用性也不高)。
测试程序
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
也是没问题的,既没有出现多个请求粘连的情况,也没有出现请求与应答错位的情况。
测试程序
4.结论
Solaris 上的pipe不仅可以全双工通讯、不依赖父子进程关系,还可以实现类似tcp那样分离多个客户端通讯连接的能力。
虽然Solaris前途未卜,但是希望一些好的东西还是能流传下来,就比如这个神奇的pipe。
看完今天的文章,你是不是对特立独行的Solaris又加深了一层了解?欢迎留言区说说你认识的Solaris。
[apue] 神奇的 Solaris pipe的更多相关文章
- apue 外传
先上目录 chapter 3 [apue] dup2的正确打开方式 chapter 10 [apue] 等待子进程的那些事儿 chapter 14 [apue] 使用文件记录锁无法实现父子进程交互执行 ...
- [apue] 标准 I/O 库那些事儿
前言 标准 IO 库自 1975 年诞生以来,至今接近 50 年了,令人惊讶的是,这期间只对它做了非常小的修改.除了耳熟能详的 printf/scanf,回过头来对它做个全方位的审视,看看到底优秀在哪 ...
- 【APUE】Chapter16 Network IPC: Sockets & makefile写法学习
16.1 Introduction Chapter15讲的是同一个machine之间不同进程的通信,这一章内容是不同machine之间通过network通信,切入点是socket. 16.2 Sock ...
- gcc使用备忘
本文为原创文章,转载请指明该文链接 Options Controling the kind of Output -x language 明确说明输入文件的编码语言,没有该选项的话, gcc 会根据输入 ...
- 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 ...
- [15]APUE:pipe / FIFO
管道 pipe 一.概述 管道(pipe / FIFO)是一种文件,属于 pipefs 文件系统类型,可以使用 read.write.close 等系统调用进行操作 其本质是内核维护了一块缓冲区与管道 ...
- APUE学习笔记——10 信号
信号的基本概念 信号是软件中断,信号提供了解决异步时间的方法. 每一中信号都有一个名字,信号名以SIG开头. 产生信号的几种方式 很多条件可以产生信号: 终端交互:用户 ...
- [apue] linux 文件系统那些事儿
前言 说到 linux 的文件系统,好多人第一印象是 ext2/ext3/ext4 等具体的文件系统,本文不涉及这些,因为研究具体的文件系统难免会陷入细节,甚至拉大段的源码做分析,反而不能从宏观的角度 ...
- APUE学习之多线程编程(一):线程的创建和销毁
一.线程标识 和每个进程都有一个进程ID一样,每个线程也有一个线程ID,线程ID是以pthread_t数据类型来表示的,在Linux中,用无符号长整型表示pthread_t,Solaris ...
随机推荐
- 数据结构(三十三)最小生成树(Prim、Kruskal)
一.最小生成树的定义 一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边. 在一个网的所有生成树中,权值总和最小的生成树称为最小代价生成树(Minimum ...
- 如何在vue里引入Bootstrap
一.引入jquery 步骤: 1. 安装jquery $ npm install jquery --save-dev 2.在webpack.config.js 添加内容 + const webpack ...
- swiper轮播
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- 微服务SpringCloud之服务网关zuul二
Zuul的核心 Filter是Zuul的核心,用来实现对外服务的控制.Filter的生命周期有4个,分别是“PRE”.“ROUTING”.“POST”.“ERROR”,整个生命周期可以用下图来表示. ...
- SpringBoot与MybatisPlus整合之公用字段填充(十一)
在实际开发中,适合用于记录创建人修改人 pom.xml <dependencies> <dependency> <groupId>org.springframewo ...
- 回话技术-Cookie-记录上一次访问时间
效果: 第一次访问: 再次访问: <%@ page import="java.util.Date" %> <%@ page import="java.t ...
- vue页面首次加载缓慢原因及解决方案
第一次打包vue的项目部署到服务器下时,发现初次加载特别的缓慢,将近20s页面才加载出来,完全没有开发环境上的那么流畅.主要原因是页面在打包后如果不进行相关配置会导致资源文件特别的大,一次想要全部加载 ...
- MIT线性代数:15.子空间的投影
- [2018-07-19] 安装python
1.Python官网 https://www.python.org/downloads/ 2.应该使用Python2.x还是Python3.x? Python有2.x和3.x两个版本,这两个版本是不兼 ...
- [2018-03-08] virtualenv
virtualenv 的有点 1.使不同应用开发环境独立 2.环境升级不影响其他应用,也不会影响全局的python环境 3.它可以防止系统中出现包管理混乱和版本的冲突 新建 virtualenv ...