管道是Unix系統IPC的最古老方式,有兩種侷限性:shell
(1) 歷史上它們是半雙工的(即數據只能在一個方向上流動),雖然如今某些系統提供了全雙工管道,可是爲了可移植性,不要抱有絕對的全雙工假設。服務器
(2) 管道只能在具備公共祖先的兩個進程之間使用(通常都是用於父子進程之間)。ide
管道是經過調用pipe函數建立的:函數
#include <unistd.h>大數據 int pipe(int fd[2]);this 返回值:成功,返回0;失敗,返回-1spa 說明:設計 fd返回兩個文件描述符:fd[0]用於讀,fd[1]用於寫,fd[1]的輸出恰好是fd[0]的輸入。指針 即shell爲每一條命令單首創建一個進程,而後管道將前一條命令的標準輸出與後一條命令的標準輸入相鏈接。excel 注: POSIX.1容許實現支持全雙工管道,對於這些實現,fd[0]和fd[1]以讀/寫方式打開。 |
以下給出了兩種描繪半雙工管道的方法,左圖中管道的兩端在一個進程中相互鏈接,右圖中則強調數據須要經過內核在管道中流動:
管道一般在單個進程中沒有太大用處,下圖顯示了父子進程之間的管道:進程先調用pipe,接着調用fork,從而建立從父進程到子進程的IPC管道:
fork以後具體要作什麼取決於咱們想要的數據流的方向。對於從父進程到子進程的管道,父進程關閉管道的讀端(fd[0]),子進程關閉管道的寫端(fd[1]):
當管道的一端被關閉後,下列兩條規則起做用:
(1) 當read一個寫端已被關閉的管道時,在全部數據都被讀取後,read返回0,表示文件結束。
(2) 當write一個讀端已被關閉的管道時,則產生信號SIGPIPE,若是忽略該信號或者捕捉該信號並從其處理程序返回,則write返回-1,errno設置爲EPIPE。
以下爲管道程序實例:
實例一:建立一個從父進程到子進程的管道,而且父進程經由該管道向子進程傳送數據:
1 [root@benxintuzi ipc]# cat pipe1.c 2 #include <unistd.h> 3 #include <stdio.h> 4 5 #define MAXLINE 1024 6 7 int main(void) 8 { 9 int n; 10 int fd[2]; 11 pid_t pid; 12 char line[MAXLINE]; 13 14 if (pipe(fd) < 0) 15 printf("pipe error\n"); 16 if ((pid = fork()) < 0) { 17 printf("fork error\n"); 18 } else if (pid > 0) { /* parent */ 19 close(fd[0]); 20 write(fd[1], "hello world\n", 12); /* write data to fd[1] */ 21 } else { /* child */ 22 close(fd[1]); 23 n = read(fd[0], line, MAXLINE); /* read data from fd[0] */ 24 write(STDOUT_FILENO, line, n); /* write data to standard output */ 25 } 26 27 return (0); 28 } 29 30 [root@benxintuzi ipc]# ./pipe1 31 [root@benxintuzi ipc]# hello world 32 33 [root@benxintuzi ipc]#
實例二:編寫一個程序,其功能是每次一頁地顯示已產生的輸出。爲了不先將全部數據寫到一個臨時文件中,而後再調用系統中有關程序顯示該文件,咱們但願經過管道將輸出直接送到分頁程序。爲此,先建立一個管道,fork一個子進程,使子進程的標準輸入成爲管道的讀端,而後調用exec,執行分頁程序:(說明點:1. 在調用fork以前,先建立一個管道。調用fork以後,父進程關閉讀端,子進程關閉寫端,而後子進程調用dup2,使其標準輸入成爲管道的讀端。當執行分頁程序時,其標準輸入就是管道的讀端了; 2. 咱們使用環境變量PAGER來得到用戶分頁程序名,若是沒有成功,則使用系統默認值,這是環境變量的常見用法。)
1 [root@benxintuzi ipc]# cat pipe2.c 2 #include <unistd.h> 3 #include <sys/wait.h> 4 #include <stdio.h> 5 6 #define DEF_PAGER "/bin/more" /* default pager program */ 7 #define MAXLINE 1024 8 9 int main(int argc, char *argv[]) 10 { 11 int n; 12 int fd[2]; 13 pid_t pid; 14 char *pager, *argv0; 15 char line[MAXLINE]; 16 FILE *fp; 17 18 if (argc != 2) 19 { 20 printf("usage: a.out <pathname>\n"); 21 return (-1); 22 } 23 24 if ((fp = fopen(argv[1], "r")) == NULL) 25 printf("can't open %s\n", argv[1]); 26 if (pipe(fd) < 0) 27 printf("pipe error\n"); 28 29 if ((pid = fork()) < 0) { 30 printf("fork error\n"); 31 } else if (pid > 0) { /* parent */ 32 close(fd[0]); /* close read end */ 33 34 /* parent copies argv[1] to pipe */ 35 while (fgets(line, MAXLINE, fp) != NULL) { 36 n = strlen(line); 37 if (write(fd[1], line, n) != n) 38 printf("write error to pipe\n"); 39 } 40 if (ferror(fp)) 41 printf("fgets error\n"); 42 43 close(fd[1]); /* close write end of pipe for reader */ 44 45 if (waitpid(pid, NULL, 0) < 0) 46 printf("waitpid error\n"); 47 return (0); 48 } else { /* child */ 49 close(fd[1]); /* close write end */ 50 if (fd[0] != STDIN_FILENO) { 51 if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) 52 printf("dup2 error to stdin\n"); 53 close(fd[0]); /* don't need this after dup2 */ 54 } 55 56 /* get arguments for execl() */ 57 if ((pager = getenv("PAGER")) == NULL) 58 pager = DEF_PAGER; 59 if ((argv0 = strrchr(pager, '/')) != NULL) 60 argv0++; /* step past rightmost slash */ 61 else 62 argv0 = pager; /* no slash in pager */ 63 64 if (execl(pager, argv0, (char *)0) < 0) 65 printf("execl error for %s\n", pager); 66 } 67 return (0); 68 } 69 [root@benxintuzi ipc]# ./pipe2 pipe2.c 70 #include <unistd.h> 71 #include <sys/wait.h> 72 #include <stdio.h> 73 74 #define DEF_PAGER "/bin/more" /* default pager program */ 75 #define MAXLINE 1024 76 77 int main(int argc, char *argv[]) 78 { 79 int n; 80 int fd[2]; 81 pid_t pid; 82 char *pager, *argv0; 83 char line[MAXLINE]; 84 FILE *fp; 85 86 if (argc != 2) 87 { 88 printf("usage: a.out <pathname>\n"); 89 return (-1); 90 } 91 92 if ((fp = fopen(argv[1], "r")) == NULL) 93 printf("can't open %s\n", argv[1]); 94 if (pipe(fd) < 0) 95 printf("pipe error\n"); 96 97 if ((pid = fork()) < 0) { 98 printf("fork error\n"); 99 } else if (pid > 0) { /* parent */ 100 close(fd[0]); /* close read end */ 101 102 /* parent copies argv[1] to pipe */ 103 while (fgets(line, MAXLINE, fp) != NULL) { 104 --More--
實例三:父子進程同步函數的管道實現:TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT、WAIT_CHILD:(說明點:父進程在調用TELL_CHILD時,經由上一個管道寫一個字符「p」,子進程在調用TELL_PARENT時,經由下一個管道寫一個字符「c」,相應的WAIT_XXX函數調用read讀一個字符,沒有讀到字符時則阻塞)
1 static int pfd1[2], pfd2[2]; 2 3 void TELL_WAIT(void) 4 { 5 if (pipe(pfd1) < 0 || pipe(pfd2) < 0) 6 printf("pipe error\n"); 7 } 8 9 void TELL_PARENT(pid_t pid) 10 { 11 if (write(pfd2[1], "c", 1) != 1) 12 printf("write error\n"); 13 } 14 15 void WAIT_PARENT(void) 16 { 17 char c; 18 19 if (read(pfd1[0], &c, 1) != 1) 20 printf("read error\n"); 21 22 if (c != 'p') 23 { 24 printf("WAIT_PARENT: incorrect data\n"); 25 return ; 26 } 27 28 } 29 30 void TELL_CHILD(pid_t pid) 31 { 32 if (write(pfd1[1], "p", 1) != 1) 33 printf("write error\n"); 34 } 35 36 void WAIT_CHILD(void) 37 { 38 char c; 39 40 if (read(pfd2[0], &c, 1) != 1) 41 printf("read error\n"); 42 43 if (c != 'c') 44 { 45 printf("WAIT_CHILD: incorrect data\n"); 46 return ; 47 } 48 }
常見的操做是建立一個鏈接到另外一個進程的管道,而後讀其輸出或向其輸入端發送數據,爲此,標準I/O庫提供了兩個函數popen和pclose。這兩個函數的功能是:建立一個管道,fork一個子進程,關閉未使用的管道端,而後執行一個shell運行命令,等待命令終止(使用popen能夠減小代碼編寫量)。
#include <stdio.h> FILE* popen(const char* cmdstring, const char* type); 返回值:成功,返回文件指針;失敗,返回NULL int pclose(FILE* fp); 返回值:成功,返回cmdstring的終止狀態;失敗,返回-1 說明: 函數popen先執行fork,而後調用exec執行cmdstring,而且返回一個標準I/O文件指針。若是type是r,則文件指針鏈接到cmdstring的標準輸出;若是type是w,則文件指針鏈接到cmdstring的標準輸入,見下圖:
pclose函數關閉標準I/O流,等待命令終止,而後返回shell的終止狀態。 cmdstring由Bourne shell如下列方式執行:sh –c cmdstring |
shell命令${PAGER:-more}的意思是:若是shell變量PAGER已經定義,且其值非空,則使用其值,不然使用字符串more。利用popen函數重寫實例二:
1 [root@benxintuzi ipc]# cat pipe3.c 2 #include <stdio.h> 3 #include <sys/wait.h> 4 5 #define MAXLINE 1024 6 #define PAGER "${PAGER:-more}" /* environment variable, or default */ 7 8 int main(int argc, char *argv[]) 9 { 10 char line[MAXLINE]; 11 FILE *fpin, *fpout; 12 13 if (argc != 2) 14 { 15 printf("usage: a.out <pathname>\n"); 16 return (-1); 17 } 18 19 if ((fpin = fopen(argv[1], "r")) == NULL) 20 printf("can't open %s\n", argv[1]); 21 22 if ((fpout = popen(PAGER, "w")) == NULL) 23 printf("popen error\n"); 24 25 /* copy argv[1] to pager */ 26 while (fgets(line, MAXLINE, fpin) != NULL) { 27 if (fputs(line, fpout) == EOF) 28 printf("fputs error to pipe\n"); 29 } 30 if (ferror(fpin)) 31 printf("fgets error\n"); 32 if (pclose(fpout) == -1) 33 printf("pclose error\n"); 34 35 return (0); 36 } 37 [root@benxintuzi ipc]# ./pipe3 pipe2.c 38 #include <unistd.h> 39 #include <sys/wait.h> 40 #include <stdio.h> 41 42 #define DEF_PAGER "/bin/more" /* default pager program */ 43 #define MAXLINE 1024 44 45 int main(int argc, char *argv[]) 46 { 47 int n; 48 int fd[2]; 49 pid_t pid; 50 char *pager, *argv0; 51 char line[MAXLINE]; 52 FILE *fp; 53 54 if (argc != 2) 55 { 56 printf("usage: a.out <pathname>\n"); 57 return (-1); 58 } 59 60 if ((fp = fopen(argv[1], "r")) == NULL) 61 printf("can't open %s\n", argv[1]); 62 if (pipe(fd) < 0) 63 printf("pipe error\n"); 64 65 if ((pid = fork()) < 0) { 66 printf("fork error\n"); 67 } else if (pid > 0) { /* parent */ 68 close(fd[0]); /* close read end */ 69 70 /* parent copies argv[1] to pipe */ 71 while (fgets(line, MAXLINE, fp) != NULL) { 72 --More--
協同進程:
當一個進程既要產生某個程序的輸入,又讀取該程序的輸出時,它就變成了協同進程(coprocess)。協同進程一般在shell後臺運行,其標準輸入和標準輸出經過管道鏈接到另外一個程序。popen只提供鏈接到另外一個進程的標準輸入或標準輸出的一個單向管道,而協同進程則有鏈接到另外一個進程的兩個單向管道:一個鏈接到其標準輸入,另外一個則來自其標準輸出。
實例:從標準輸入讀取兩個數,計算它們的和,而後將和寫至其標準輸出。
1 [root@benxintuzi ipc]# cat coprocess.c 2 #include <unistd.h> 3 #include <stdio.h> 4 5 #define MAXLINE 1024 6 7 int main(void) 8 { 9 int n, int1, int2; 10 char line[MAXLINE]; 11 12 while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) { 13 line[n] = 0; /* null terminate */ 14 if (sscanf(line, "%d%d", &int1, &int2) == 2) { 15 sprintf(line, "%d\n", int1 + int2); 16 n = strlen(line); 17 if (write(STDOUT_FILENO, line, n) != n) 18 printf("write error\n"); 19 } else { 20 if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) 21 printf("write error\n"); 22 } 23 } 24 25 return (0); 26 } 27 28 [root@benxintuzi ipc]# ./coprocess 29 1 2 30 3 31 10 20 32 30 33 100 200 34 300 35 ^C 36 [root@benxintuzi ipc]#
實例:將上述程序編譯成爲add2協同進程,而後下列程序建立了兩個管道,父進程、子進程各自關閉了它們不須要的管道端,必須使用兩個管道:一個用做協同進程的標準輸入,另外一個用做它的標準輸出。而後子進程調用dup2使管道描述符移至其標準輸入和標準輸出,最後調用了excel執行add2:
1 [root@benxintuzi ipc]# gcc coprocess.c -o add2 2 [root@benxintuzi ipc]# cat coprocess2.c 3 #include <unistd.h> 4 #include <signal.h> 5 #include <stdio.h> 6 7 #define MAXLINE 1024 8 9 static void sig_pipe(int); /* our signal handler */ 10 11 int main(void) 12 { 13 int n, fd1[2], fd2[2]; 14 pid_t pid; 15 char line[MAXLINE]; 16 17 if (signal(SIGPIPE, sig_pipe) == SIG_ERR) 18 printf("signal error\n"); 19 20 if (pipe(fd1) < 0 || pipe(fd2) < 0) 21 printf("pipe error\n"); 22 23 if ((pid = fork()) < 0) { 24 printf("fork error\n"); 25 } else if (pid > 0) { /* parent */ 26 close(fd1[0]); 27 close(fd2[1]); 28 29 while (fgets(line, MAXLINE, stdin) != NULL) { 30 n = strlen(line); 31 if (write(fd1[1], line, n) != n) 32 printf("write error to pipe\n"); 33 if ((n = read(fd2[0], line, MAXLINE)) < 0) 34 printf("read error from pipe\n"); 35 if (n == 0) { 36 printf("child closed pipe\n"); 37 break; 38 } 39 line[n] = 0; /* null terminate */ 40 if (fputs(line, stdout) == EOF) 41 printf("fputs error\n"); 42 } 43 44 if (ferror(stdin)) 45 printf("fgets error on stdin\n"); 46 exit(0); 47 } else { /* child */ 48 close(fd1[1]); 49 close(fd2[0]); 50 if (fd1[0] != STDIN_FILENO) { 51 if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) 52 printf("dup2 error to stdin\n"); 53 close(fd1[0]); 54 } 55 56 if (fd2[1] != STDOUT_FILENO) { 57 if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) 58 printf("dup2 error to stdout\n"); 59 close(fd2[1]); 60 } 61 if (execl("./add2", "add2", (char *)0) < 0) 62 printf("execl error\n"); 63 } 64 exit(0); 65 } 66 67 static void sig_pipe(int signo) 68 { 69 printf("SIGPIPE caught\n"); 70 exit(1); 71 } 72 73 [root@benxintuzi ipc]# ./coprocess2 74 100 50 75 150 76 100 500 77 600
FIFO有時被稱爲命名管道,未命名的管道只能在兩個相關的進程之間使用,並且這兩個相關的進程還要有一個共同的祖先進程。可是,經過FIFO,不相關的進程之間也能交換數據。
使用以下函數建立FIFO:
#include <sys/stat.h> int mkfifo(const char* path, mode_t mode); int mkfifoat(int fd, const char* path, mode_t mode); 返回值:成功,返回0;失敗,返回-1 說明: mkfifoat與mkfifo類似,像以前其餘*at系列函數同樣,有3種情形: (1) 若是path參數指定了絕對路徑名,則fd被忽略,此時mkfifoat和mkfifo同樣。 (2) 若是path參數指定了相對路徑名,則fd參數是一個打開目錄的有效文件描述符,路徑名和目錄有關。 (3) 若是path參數指定了相對路徑名,而且fd參數指定了AT_FDCWD,則路徑名以當前目錄開始,mkfifoat和mkfifo相似。 當咱們使用mkfifo或者mkfifoat函數建立FIFO時,要用open打開,確是,正常的I/O函數(如close、read、write、unlink)都須要FIFO。當open一個FIFO時,非阻塞標誌(O_NONBLOCK)會產生以下影響: (1) 沒有指定O_NONBLOCK時,只讀open要阻塞到某個其餘進程爲寫而打開這個FIFO爲止。相似地,只寫open要阻塞到某個其餘進程爲讀而打開這個FIFO爲止。 (2) 若是指定了P_NONBLOCK,則只讀open當即返回。可是,若是沒有進程爲讀而打開這個FIFO,那麼只寫open將返回-1,並將errno設置爲ENXIO。 一個給定的FIFO有多個寫進程是很常見的,這就意味着,若是不但願多個進程所寫的數據交叉,則必須考慮原子寫操做。和管道同樣,常量PIPE_BUF說明了可被原子地寫到FIFO的最大數據量。 |
FIFO主要有如下兩種用途:
(1) shell命令使用FIFO將數據從一條管道傳送到另外一條管道,無需建立中間臨時文件。
實例:考慮這樣一個過程,他須要對一個輸入文件進行兩次處理,示意圖以下:
咱們可使用FIFO和tee命令以下處理:
mkfifo fifo1
prog3 < fifo1 &
prog1 < (輸入文件) | tee fifo1 | prog2
執行流程以下:
(2) 客戶進程-服務器進程應用程序中,FIFO用做匯聚點,在客戶進程和服務器進程之間傳遞數據。
實例:有一個服務器進程,它與不少客戶進程相關,每一個客戶進程均可將請求寫到一個該服務器進程建立的FIFO中。因爲該FIFO有多個寫進程,所以客戶進程每次發送給服務器的數據長度要小於PIPE_BUF字節,這樣就能避免客戶進程之間的寫交叉。
可是這種類型的FIFO設計有問題,服務器如何迴應各個客戶進程呢?一種解決方法是,每一個客戶進程都在其請求中包含它的進程ID,而後服務器進程爲每一個客戶進程建立一個FIFO,所使用的路徑名是以客戶進程的進程ID爲基礎的。例如,服務進程能夠用名字/tmp/serv1.XXXXX建立FIFO,其中XXXXX被替換成客戶進程的進程ID,以下圖所示: