在 AIX 應用開發中會遇到進程間通訊的需求,進程間通訊的方法有很多,例如通過共享內存、信號燈、內存映射文件、數據管道、文件、Socket 等等。這裡主要介紹一種通過數據管道和系統標准輸入輸出文件描述符相結合的方式來實現進程間通訊和數據交互。本文面向 AIX 或其他 UNIX 平台 C 語言的開發者,讀者需要具備一定進程間匿名管道通訊的知識,並且對文件描述符、基本的 I/O 操作有一定了解。
什麼是匿名管道
管道是進程間協同工作的一種方式,單獨構成一種獨立的文件系統,管道是半雙工的。而匿名管道數據只能向一個方向流動,雙方通信時,需要建立起兩個管道;只能用於父子進程或者兄弟進程之間(具有親緣關系的進程)。
文件指針與管道
當我們要讀寫一個文件,需要用到文件指針,它是一個指向結構體的指針。我們對管道進行讀寫操作時也需要用到文件指針,通過文件指針來對管道一端進行寫,而另一端的進程則通過文件指針進行讀。如果文件指針指向的是標准輸入,那麼該進程則是從標准輸入中讀取數據,本文利用文件指針進行匿名管道的讀寫操作。
通過匿名管道實現進程間通訊
通信場景如下:現在有兩個進程 A、進程 B( 是進程 A 的子進程 ),進程 A 從數據庫中讀取一條待處理數據 M,數據 M 中存儲了進程 A 需要調用的可執行程序名稱及需要傳遞給可執行程序的參數。由於參數很多,並且參數長度及個數以及類型都是變化的,所以這裡不采取參數傳遞方法,采用了匿名管道進程間通信方法。
圖1. 匿名管道通信
進程 A 和進程 B 的標准輸出都輸出到指定的日志文件 logfile 中,進程 A 實現通過調用 popen 會另外 fork 一個進程 B, 而進程 A 和進程 B 之間建立起了一個數據管道(這裡進程 A 寫入,進程 B 讀出)。這裡要做一下說明,進程 B 本身就是 a.out的可執行程序,popen 原理是 fork 後再執行 exec 家族函數。我們知道 exec 家族函數特點就是調用進程的實體,包括代碼段,數據段和堆棧等都已經被新的內容取代。另外,子進程(進程 B)的標准輸入就是數據管道的”讀端”,而管道”寫端”在父進程以文件指針形式存在。這樣父進程(進程 A)可以通過對 popen 返回的文件指針操作進行寫操作,子進程(進程 B)可以通過讀取標准輸入來獲取數據管道傳遞過來的數據。並且子進程的標准輸出與父進程是相同的,由於調用 popen 時( FILE * popen( const char * command , const char * type) ),command 參數執行了重定向標准輸出到 logfile,所以在子進程 ( 進程 B) 中調用任何 printf 標准輸出函數都會將數據寫到 logfile 中去。
清單 1. 進程 A 簡要代碼(fileA.sqc)
fileA.sqc #include #include #include #include … static int npWrite( FILE *fp , const char * tovalue, FILE *fp1 ) { int iLen = 0, iRetLen = 0; iLen = strlen( tovalue ); … . iRetLen = fwrite( tovalue , sizeof( char ) , iLen , fp ); … return 0; } int request() { FILE * fp = 0x00; FILE * fpw =0x00; char logfile[100]; char batfile[100]; char binpath[100]; char execfile[100]; … .. (1) sprintf( execfile , "%s/%s %s %s %s %s 1>>%s", binpath,”a.out”,“123456”,”2011-01-01”,”test”,“A0001”,logfile); fpw = 0x00; (2) /* 通過 popen 當前進程 ( 進程 A) 會通過 fork 和 exec 系統調用來啟動一個子進程,( 子進程代 ** 碼見 fileB.sqc), 子進程也就是我們前面講述到的進程 B。那麼進程 A 是進程 B 的父進程 . */ fpw = popen( execfile ,"w"); if ( fpw == 0x00 ) { fprintf( fp , " popen error %s\n.", strerror( errno ) ); fclose( fp ); return -1; } … . if ( npWrite( fpw , “hello world” , fp ) ) return -1; … . pclose( fpw ); fflush( fp ); … . fclose( fp ); return 0; }
源文件 fileA.sqc中調用 popen 函數來創建匿名管道,該函數需要注意以下幾點:
1) popen 函數用創建管道的方式啟動一個進程 ( 進程 B,即調用 popen 函數的進程的子進程 ) 並調用 Shell。管道是單向的,所以只能定義成只讀或者只寫。
2) popen 函數的返回值是一個普通的標准 I/O 流,它只能用 pclose 而不是 fclose 函數來關閉。向這個流的寫入被轉化為對 command 命令的標准輸入;而 command 命令的標准輸出則和調用 popen 函數的進程相同,除非這個被 command 命令自己改變。相反的 , 讀取一個“被 popen 了的”流,就相當於讀取 command 命令的標准輸出,而 command 的標准輸入則是和調用 popen 函數的進程相同。
3) popen 函數的輸出流默認是被全緩沖的,poepn 函數等待相關的進程結束並返回一個 command 命令的退出狀態 , 就像 wait4 函數 一樣。
在 fileA.sqc中(1)標示的 popen 將執行命令的標准輸出定向到了 logfile 中,由於 popen 會 fork 一個進程去執行 popen 參數 command 指定的程序,一般子進程標准輸出不重定向的情況下,子進程的標准輸出與父進程相同,子進程可以從標准輸入中得到父進程傳遞的數據流(通過管道),並且也可通過子進程輸出到標准輸出,在 logfile 中父進程可以知道子進程的輸出結果。request 函數實現讀取數據調用處理程序並將數據寫入管道的功能。
清單 2. 進程 B 部分源代碼(fileB.sqc)
static int npRead(FILE * fp , int i ) { int iLen = 3; int iRetLen = 0; int ireadLen =0 ; char buflen[3+1]; char buffer[100]; memset( buffer , 0x00 , sizeof( buffer )); memset( buflen , 0x00 , sizeof( buflen )); ireadLen = … .; … ireadLen = fread( buffer , sizeof( char ) , iRetLen , stdin ); … buffer[iRetLen] = 0x00; … // 輸出到與父進程相同的 LOG 文件中 printf( "recv[%d][%s]\n" ,iRetLen ,buffer ); … . return 0; } void nGetNetData() { int i = 0; for ( i = 0; i < n ; i++ ) { npRead( stdin , i ); } } 進程 B 調用 nGetNetData 函數就可以從標准輸入到進程 A 傳遞數據了,並且在 npRead 中通過調用 printf 將日志信息輸出到了在進程 A 中指定的文件 logfile 中。
結束語
1、使用匿名管道並結合我們常用標准輸入輸出函數實現進程間通訊很方便,但不適用於無親緣關系的進程。
2、對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是單獨構成一種文件系統,並且只存在內存中,通信雙方的進程通過標准輸入輸出 API 進行通信寫入和讀取。
3、管道也是一種文件類型,理解其原理更有利於我們使用它。
4、標准輸入輸出和普通的文件描述符相同,我們可以根據需要利用 shell 或者 dup 函數都可以實現重定向。這樣我們就可以更好的利用標准 I/O 庫為我們工作了。
原文:
http://www.ibm.com/developerworks/cn/aix/library/1106_chenye_commbypipe/index.html?ca=drs-