Unix環境高級編程(十六)進程間通信
進程間通信(IPC)是指能在兩個進程間進行數據交換的機制。現代OS都對進程有保護機制,因此兩個進程不能直接交換數據,必須通過一定機制來完成。
IPC的機制的作用:
(1)一個軟件也能更容易跟第三方軟件或內核進行配合的集成,或移植.如管道,在shell 下執行 ps –aux | grep bash。
(2)簡化軟件結構, 可以把一個軟件劃分多個進程或線程,通過IPC,集成在一起工作.如消息隊列。
(3)讓操作系統各個模塊交換數據,包括內核與應用程序機制。
(4)提供進程之間或同一進程之間多線程的同步機制,如信號量。
1、管道
管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道 只能用於父子進程或者兄弟進程之間(具有親緣關系的進程)單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,並且只存在與內存中。
數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩沖區的末尾,並且每次都是從緩沖區的頭部讀出數據。
管道的創建:int pipe(int fd[2]) ;
管道的讀寫:管道文件也是一種文件,用write,read
即可完成讀寫。管道兩端可分別用描述字fd[0]以及fd[1]來描述,需要註意的是,管道的兩端是固定了任務的。即一端只能用於讀,由描述字fd[0]表示,稱其為管道讀端;另一端則只能用於寫,由描述字fd[1]來表示,稱其為管道寫端。如果試圖從管道寫端讀取數據,或者向管道讀端寫入數據都將導致錯誤發生。
管道的關閉:管道文件也是一種文件,因此用close關閉即可。
管道的局限:(1)只支持單向數據流;
(2)只能用於具有親緣關系的進程之間; (3)沒有名字;
(4)管道的緩沖區是有限的(管道制存在於內存中,在管道創建時,為緩沖區分配一個頁面大小);
(5)管道所傳送的是無格式字節流,這就要求管道的讀出方和寫入方必須事先約定好數據的格式,比如多少字節算作一個消息(或命令、或記錄)等等。
現在使用管道實現進程的同步,父進程讀取子進程輸入的數據、子進程讀取父進程恢復的數據。實現TELL_WAIT、TELL_PARENT、TELL_CHILD、TELL_PARENT及WAIT_CHILD函數。程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <errno.h> 5 #include <sys/types.h> 6 7 static int fd1[2],fd2[2]; 8 9 void TELL_WAIT() 10 { 11 pipe(fd1); 12 pipe(fd2); 13 } 14 15 void TELL_PARENT(pid_t pid) 16 { 17 write(fd2[1],"c",1); 18 } 19 void WAIT_PARENT(void) 20 { 21 char c; 22 read(fd1[0],&c,1); 23 if(c!=‘p‘) 24 { 25 printf("WAIT_PARENT: Incorretc data"); 26 exit(0); 27 } 28 else 29 printf("Read from parent.\n"); 30 } 31 void TELL_CHILD(pid_t pid) 32 { 33 write(fd1[1],"p",1); 34 } 35 void WAIT_CHILD() 36 { 37 char c; 38 read(fd2[0],&c,1); 39 if(c!=‘c‘) 40 { 41 printf("WAIT_CHILD: Incorretc data"); 42 exit(0); 43 } 44 else 45 printf("Read from child.\n"); 46 } 47 48 int main() 49 { 50 pid_t pid; 51 TELL_WAIT(); 52 pid =fork(); 53 if(pid == -1) 54 { 55 perror("fork() error"); 56 exit(-1); 57 } 58 if(pid == 0) 59 { 60 printf("child process exec.\n"); 61 WAIT_PARENT(); 62 TELL_PARENT(getppid()); 63 } 64 else 65 { 66 printf("Parent process exec.\n"); 67 TELL_CHILD(pid); 68 WAIT_CHILD(); 69 70 } 71 exit(0); 72 }
程序執行結果如下:
popen和pclose函數
常見的操作時創建一個管道連接到另外一個進程,然後讀取其輸出或向其輸入端發送數據。popen和pcolse函數實現的操作是:創建一個管道,調用fork產生一個子進程,關閉管道的不使用端,執行一個shell以運行命令,然後等待命令終止。函數原型如下:
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
函數popen先執行fork,然後調用exec執行cmdstring,並且返回一個標準I/O文件指針。如果type是“r”,則文件指針連接到cmdstring的標準輸出,如果type是“w”,則文件指針連接到cmdstring的標準輸入。popen特別適用於構造簡單的過濾程序,它變換運行命令的輸入或輸出。寫一個程序,將標準輸入復制到標準輸出,復制的時候將所有的大寫字母變換為小寫字母,程序分為兩部分,轉換程序如下:
1 #include <stdio.h> 2 #include <ctype.h> 3 #include <stdlib.h> 4 int main() 5 { 6 int c; 7 while((c = getchar()) != EOF) 8 { 9 if(isupper(c)) 10 c= tolower(c); 11 if(putchar(c) == EOF) 12 printf("output error"); 13 if(c==‘\n‘) 14 fflush(stdout); 15 } 16 exit(0); 17 }
將可執行文件保存為change。輸入輸出程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <errno.h> 5 #include <errno.h> 6 7 #define MAXLINE 1024 8 9 int main() 10 { 11 char line[MAXLINE]; 12 FILE *fpin; 13 if((fpin = popen(".//change","r")) == NULL) 14 { 15 perror("popen() error"); 16 exit(-1); 17 } 18 for(; ;) 19 { 20 fputs("prompt> ",stdout); 21 fflush(stdout); 22 if(fgets(line,MAXLINE,fpin) == NULL) 23 break; 24 if(fputs(line,stdout) == EOF) 25 { 26 perror("fputs error to pipe"); 27 } 28 } 29 if(pclose(fpin) == -1) 30 { 31 perror("pclose() error"); 32 exit(-1); 33 } 34 putchar(‘\n‘); 35 exit(0); 36 }
程序執行結果如下
協同進程:當一個進程產生某個過濾程序的輸入,同時又讀取該過濾程序的輸出。popen只提供鏈接到另一個進程的標準輸入或標準輸出的一個單向管道,對於協同進程,則連接到另一個進程的兩個單向管道,一個接到標準輸入,一個接標準輸出。寫個程序展示一下協同進程,程序從標準輸入讀入兩個整數,調用程序計算它們的和,然後將結果輸出到標準輸出。過濾程序即求和程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <fcntl.h> 5 #include <unistd.h> 6 #define MAXLINE 1024 7 8 int main() 9 { 10 int n,int1,int2; 11 char line[MAXLINE]; 12 while((n=read(STDIN_FILENO,line,MAXLINE)) > 0) 13 { 14 line[n] = ‘\0‘; 15 if(sscanf(line,"%d%d",&int1,&int2) == 2) 16 { 17 sprintf(line,"%d\n",int1+int2); 18 n = strlen(line); 19 if(write(STDOUT_FILENO,line,n) != n) 20 { 21 perror("write() error"); 22 exit(-1); 23 } 24 } 25 else if(write(STDOUT_FILENO,"invalid arg\n",13) != 13) 26 { 27 perror("write() error"); 28 exit(-1); 29 } 30 } 31 exit(0); 32 }
編譯執行保存為可執行文件為add。
協同程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <errno.h> 5 #include <signal.h> 6 #include <string.h> 7 #define MAXLINE 1024 8 9 static void sig_pipe(int); 10 11 int main() 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 { 19 perror("signal() error"); 20 exit(-1); 21 } 22 if(pipe(fd1) == -1||pipe(fd2) == -1) 23 { 24 perror("pipe() error"); 25 exit(-1); 26 } 27 if((pid =fork()) == -1) 28 { 29 perror("fork() error"); 30 exit(-1); 31 } 32 if(pid == 0) 33 { 34 close(fd1[1]); 35 close(fd2[0]); 36 if(fd1[0] != STDIN_FILENO) 37 if(dup2(fd1[0],STDIN_FILENO) != STDIN_FILENO) 38 { 39 perror("dup2 error in stdin"); 40 close(fd1[0]); 41 exit(-1); 42 }; 43 if(fd2[1] != STDOUT_FILENO) 44 if(dup2(fd2[1],STDOUT_FILENO) != STDOUT_FILENO) 45 { 46 perror("dup2 error in stdout"); 47 close(fd2[1]); 48 exit(-1); 49 }; 50 if(execl(".//add","add",(char *)0) == -1) 51 { 52 perror("execl() error"); 53 exit(-1); 54 } 55 } 56 else 57 { 58 close(fd1[0]); 59 close(fd2[1]); 60 printf("Enter two number: "); 61 while(fgets(line,MAXLINE,stdin) != NULL) 62 { 63 n = strlen(line); 64 if(write(fd1[1],line,n) != n) 65 { 66 perror("write errot to pipe"); 67 exit(-1); 68 } 69 if((n=read(fd2[0],line,MAXLINE)) ==-1) 70 { 71 perror("read error to pipe"); 72 exit(-1); 73 } 74 if(n== 0) 75 { 76 printf("child close pipe.\n"); 77 break; 78 } 79 line[n] = ‘\0‘; 80 printf("The result is: "); 81 fputs(line,stdout); 82 } 83 } 84 } 85 86 static void sig_pipe(int signo) 87 { 88 printf("SIGPIPE caught\n"); 89 exit(1); 90 }
程序執行結果如下:
2、FIFO
FIFO不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的文件形式存在於文件系統中。這樣,即使與FIFO的創建進程不存在親緣關系的進程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信。FIFO嚴格遵循先進先出(first in first out),對管道及FIFO的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如lseek()等文件定位操作。
命名管道的命名管道創建:int mkfifo(const char * pathname, mode_t mode) 。
命名管道的打開:命名管道比管道多了一個打開操作:open ,在open時,用O_NONBLOCK 標誌表示非阻塞模式,如fd=open(“/tmp/fifo”,O_RDONLY|O_NONBLOCK,0)。
命名管道的讀入:read 讀取管道數據,讀取分為阻塞和非阻塞模式,阻塞模式下,如果沒有數據被入,進程會在read處停下來.直到有新數據被寫入,或管道被關閉,才會繼續。
命名管道的寫入:write 寫入管道數據,PIPE_BUF表示一次觸發管道讀操作最大長度.如果每次寫入數據長於PIPE_BUF ,write將會多次觸發read 操作。
命名管道的關閉:管道文件也是一種文件,因此用close關閉即可。
FIFO的兩種用途:
(1)FIFO有shell命令使用以便將數據從一條管道線傳送到另一條,為此無需創建中間臨時文件。
(2)FIFO用於客戶進程—服務器進程應用程序中,以在客戶進程和服務器進程之間傳遞數據。
3、XSI IPC
消息隊列、信號量、共享存儲區相似的特征如下:具有標識符和鍵,標識符是IPC對象的內部名,每個IPC對象都與一個鍵相關聯,創建IPC結構需要指定一個鍵,鍵的數據類型為key_t。每個IPC都設置了權限結構。
優點及缺點:IPC結構是在系統範圍內起作用,沒有訪問計數。在文件系統中沒有名字,不使用文件描述符,不能對它們使用多路轉接I/O函數。優點:可靠、流是受控的,面向記錄、可以用非先進先出方式處理。
消息隊列(Messge Queue):消息隊列是消息的鏈接表,包括Posix消息隊列SystemV消息隊列。它克服了前兩種通信方式中信息量有限的缺點,具有寫權限的進程可以按照一定的規則向消息隊列中添加新消息;對消息隊列有讀權限的進程則可以從消息隊列中讀取消息。 信號量(Semaphore):主要作為進程之間以及同一進程的不同線程之間的同步和互斥手段。 共享內存(Shared memory):可以說這是最有用的進程間通信方式。它使得多個進程可以訪問同一塊內存空間,不同進程可以及時看到對方進程中對共享內存中數據的更新。這種通信方式需要依靠某種同步機制,如互斥鎖和信號量等。 建議:要學會使用管道和FIFO,因為大量應用程序中仍可以有效地使用這兩種技術,在新的應用程序中,要盡可能避免使用消息隊列及信號量,應當考慮全雙工管道和記錄鎖。Unix環境高級編程(十六)進程間通信