說到 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\n");
4
5 char line[MAXLINE];
6 pid_t pid = fork();
7 if (pid < 0)
8 err_sys("fork error\n");
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\n")
16 if ((n = read(fd[0], line, MAXLINE)) < 0)
17 err_sys("read error from pipe\n");
18
19 if (n == 0)
20 err_sys("child closed pipe\n");
21 line[n] = 0;
22 if (fputs(line, stdout) == EOF)
23 err_sys("fputs error\n");
24 }
25
26 if (ferror(stdin))
27 err_sys("fputs error\n");
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\n");
36
37 if (fd[1] != STDOUT_FILENO) {
38 if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)
39 err_sys("dup2 error to stdout\n");
40 close(fd[1]);
41 }
42
43 if (execl(argv[1], argv[2], (char *)0) < 0)
44 err_sys("execl error\n");
45
46 }
代碼清爽多了,不用去考慮fd1[0]和fd2[1]是啥意思是一件很養腦的事。
不過這樣的代碼只能在Solaris上運行(聽說BSD也支持?),如果考慮到可移植性,還是寫上面的比較穩妥。
測試程序
2. solaris pipe 可以脫離父子關係建立
pipe 好用但是沒法脫離fork使用,一般的pipe如果想讓任意兩個進程通訊,得藉助它的變身fifo來實現。
關於FIFO,詳情可參考我之前寫的一篇文章:
而Solaris上的pipe沒這麼多事,加入兩個調用:fattach / fdetach,你就可以像使用FIFO一樣使用pipe了:
1 int fd[2];
2 if (pipe(fd) < 0)
3 err_sys("pipe error\n");
4
5 if (fattach(fd[1], "./pipe") < 0)
6 err_sys("fattach error\n");
7
8 printf("attach to file pipe ok\n");
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\n");
16 if ((n = read(fd[0], line, MAXLINE)) < 0)
17 err_sys("read error from pipe\n");
18
19 if (n == 0)
20 err_sys("child closed pipe\n");
21
22 line[n] = 0;
23 if (fputs(line, stdout) == EOF)
24 err_sys("fputs error\n");
25 }
26
27 if (ferror(stdin))
28 err_sys("fputs error\n");
29
30 if (fdetach("./pipe") < 0)
31 err_sys("fdetach error\n");
32
33 printf("detach from file pipe ok\n");
在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\n");
6
7 printf("open file pipe ok, fd = %d\n", 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\n", int1 + int2);
12 n = strlen(line);
13 if (write(fd, line, n) != n)
14 err_sys("write error\n");
15
16 printf("i am working on %s\n", line);
17 }
18 else {
19 if (write(fd, "invalid args\n", 13) != 13)
20 err_sys("write msg error\n");
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么?
而之前的例子可以,是因為請求是順序發送的,上個請求得到應答后才發送下個請求,所以不存在這個例子的問題(但是實用性也不高)。
測試程序
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\n");
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\n", acceptfd, uid);
12 while ((n = read(acceptfd, line, MAXLINE)) > 0) {
13 line[n] = 0;
14 printf("source: %s\n", line);
15 if (sscanf(line, "%d%d", &int1, &int2) == 2) {
16 sprintf(line, "%d\n", int1 + int2);
17 n = strlen(line);
18 if (write(acceptfd, line, n) != n) {
19 printf("write error\n");
20 return 0;
21 }
22 printf("i am working on %d + %d = %s\n", int1, int2, line);
23 }
24 else {
25 if (write(acceptfd, "invalid args\n", 13) != 13) {
26 printf("write msg error\n");
27 return 0;
28 }
29 }
30 }
31
32 close(acceptfd);
33 }
34
35 if (fdetach("./pipe") < 0) {
36 printf("fdetach error\n");
37 return 0;
38 }
39
40 printf("detach from file pipe ok\n");
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\n");
9 return -1;
10 }
11
12 if (close(tempfd) < 0) {
13 printf("close temp fd failed\n");
14 return -2;
15 }
16
17 if (pipe(fd) < 0) {
18 printf("pipe error\n");
19 return -3;
20 }
21
22 if (ioctl(fd[1], I_PUSH, "connld") < 0) {
23 printf("I_PUSH connld failed\n");
24 close(fd[0]);
25 close(fd[1]);
26 return -4;
27 }
28
29 printf("push connld ok\n");
30 if (fattach(fd[1], name) < 0) {
31 printf("fattach error\n");
32 close(fd[0]);
33 close(fd[1]);
34 return -5;
35 }
36
37 printf("attach to file pipe ok\n");
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\n");
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
也是沒問題的,既沒有出現多個請求粘連的情況,也沒有出現請求與應答錯位的情況。
測試程序
4.結論
Solaris 上的pipe不僅可以全雙工通訊、不依賴父子進程關係,還可以實現類似tcp那樣分離多個客戶端通訊連接的能力。
雖然Solaris前途未卜,但是希望一些好的東西還是能流傳下來,就比如這個神奇的pipe。
看完今天的文章,你是不是對特立獨行的Solaris又加深了一層了解?歡迎留言區說說你認識的Solaris。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】
※收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表
※網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益
※公開收購3c價格,不怕被賤賣!
※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象