進程間通信的研究(一)

<!--參照教輔>

一.進程通信

程序員必須讓擁有依賴關係的進程集協調,這樣才能達到進程的共同目標。可以使用兩種技術來達到協調。第一種技術在具有通信依賴關係的兩個進程間傳遞信息。這種技術稱做進程間通信(interprocess communication)。第二種技術是同步,當進程間相互具有合作依賴時使用。這兩種類型的依賴關係可以同時存在。

一般而言,進程有單獨的地址空間。我們可以瞭解下可執行程序被裝載到內存後建立的一系列映射等理解這一點。如此以來意味着如果我們有兩個進程(進程A和進程B),那麼,在進程A中聲明的數據對於進程B是不可用的。而且,進程B看不到進程A中發生的事件,反之亦然。如果進程A和B一起工作來完成某個任務,必須有一個在兩個進程間通信信息和時間的方法。我們這裏可以去看看基本的進程組件。注意進程有一個文本、數據以及堆棧片斷。進程可能也有從自由存儲空間中分配的其它內存。進程所佔有的數據一般位於數據片斷、堆棧片斷或進程的動態分配內存中。數據對於其它進程來說是受保護的。爲了讓一個進程訪問另外一個進程的數據,必須最終使用操作系統調用。與之類似,爲了讓一個進程知道另一個進程中文本片斷中發生的事件,必須在進程間建立一種通信方式。這也需要來自操作系統API的幫助。當進程將數據發送到另一進程時,稱做IPC(interprocess communication,進程間通信)。

二.進程間通信的方式,已知的有以下8種:

1.無名管道( pipe ):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用。進程的親緣關係通常是指父子進程關係。

2.高級管道(popen):將另一個程序當做一個新的進程在當前程序進程中啓動,則它算是當前程序的子進程,這種方式我們成爲高級管道方式。
3.有名管道 (named pipe) : 有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間的通信。
4.信號量( semophore ) : 信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。
5.消息隊列( message queue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
6.信號 ( sinal ) : 信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。
7.共享內存( shared memory ) :共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號兩,配合使用,來實現進程間的同步和通信。
8.套接字( socket ) : 套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同機器間的進程通信。

三.環境變量、文件描述符:

當創建一個子進程時,它接受了父進程許多資源的拷貝。子進程接受了父進程的文本、堆棧以及數據片斷的拷貝。子進程也接受了父進程的環境數據以及所有文件描述符的拷貝。子進程從父進程繼承資源的過程創造了進程間通信的一個機會。父進程可以在它的數據片斷或環境中設置一定的變量,子進程於是接受這些值。同樣,父進程也可以打開一個文件,推進到文件內的期望位置,子進程接着就可以在父進程離開讀/寫指針的準確位置訪問該文件。這類通信的缺陷在於它是單向的、一次性的通信。也就是說,除了文件描述外,如果子進程繼承了任何其它數據,也僅僅是父進程拷貝的所有數據。 一旦創建了子進程,由子進程對這些變量的任何改變都不會反映到父進程的數據中。同樣,創建子進程後,對父進程數據的任何改變也不會反映到子進程中。所以,這種類型的進程間通信更像指揮棒傳遞。一旦父進程傳遞了某些資源的拷貝,子進程對它的使用就是獨立的,必須使用原始傳遞資源。

四.命令行參數:

通過命令行參數(command-line argument)可以完成另一種單向、一次性的進程間通信我前面的文章已經提到過使用命令行參數。命令行參數在調用一個exec或派生調用操作系統時傳遞給子進程。命令行參數通常在其中一個參數中作爲NULL終止字符串傳遞給exec或派生函數調用。這些函數可以按單向、一次性方式給子進程傳遞值。WINDOWS有調用執行exe程序的API。大家可以去參考一下ShellExecuteA函數相關。

五.管道通信:

繼承資源以及命令行參數是最簡單形式的進程間通信。它們同時有兩個主要限制。除了文件描述符外,繼承資源是IPC的單向、一次性形式。傳遞命令參數也是單向、一次性的IPC方法。這些方法也只有限制於關聯進程,如果不關聯,命令行參數和繼承資源不能使用。還有另一種結構,稱做管道(Pipe),它可以用於在關聯進程間以及無關聯進程間進行通信。管道是一種數據結構,像一個序列化文件一樣訪問。它形成了兩個進程間的一種通信渠道。管道結構通過使用文本和寫方式來訪問。如果進程A希望通過管道發送數據給進程B,那麼進程A向管道寫入數據。爲了讓進程B接收此數據,進程B必須讀取管道,與命令行參數的IPC形式不一樣。管道可以雙向通信。兩進程間的數據流是雙向通信的。管道可以在程序的整個執行期間使用,在進程間發送和接收數據。所以,管道充當可訪問管道的進程間的一種可活鏈接,有兩種基本管道類型:

1.  匿名管道

2.  命名管道

匿名管道

上面的圖可以看出在沒有管道時,兩進程是不能互寫的。

匿名管道

建立管道後就可以相互通信了。只有關聯進程可以使用匿名管道來通信。無關聯進程必須使用命名管道。

匿名管道:通過文件描述符或文件句柄提供對匿名管道的訪問。對系統API的調用創建一個管道,並返回一個文件描述符。這個文件描述符是用作read()或write()函數的一個參數。當通過文件描述符調用read()或write()時,數據的源和目標就是管道。例如,在OS/2環境中使用操作系統函數DosCreatePipe()創建匿名管道:

int mian( void )

{

    PFHILE readHandle;

    PFHILE writeHandle;

    DosCreatePipe( readHandle, writeHandle, size );

    …

    …

    …

 }

在WINDOWS下例如我寫的ASM集成環境通過管道與DOS命令行通信的MFC下代碼塊:

void CIDEManager::Commond( CString cmd, char* buf, unsigned int bufsize )

{

    SECURITY_ATTRIBUTES sa;

    HANDLE hRead, hWrite;

    sa.nLength              = sizeof( SECURITY_ATTRIBUTES );

    sa.lpSecurityDescriptor = NULL;

    sa.bInheritHandle       = TRUE;

    if ( !CreatePipe( &hRead, &hWrite, &sa, 0 ) )  // 創建管道

    {

        return;

    }

    STARTUPINFO si;

    PROCESS_INFORMATION pi;

    si.cb = sizeof( STARTUPINFO );

    GetStartupInfo( &si );

    si.hStdError   = hWrite;

    si.hStdOutput  = hWrite;

    si.wShowWindow = SW_HIDE;

    si.dwFlags     = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;

    if ( !CreateProcess( NULL, ( LPTSTR )( LPCTSTR )cmd, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi ) )

    {

        return;

    }

    CloseHandle( hWrite );

    DWORD bytesRead;

    while ( TRUE )

    {

        memset( buf, 0, bufsize );

        if ( ReadFile( hRead, buf, bufsize, &bytesRead, NULL ) != NULL )

        {

            break;

        }

        Sleep( 200 );

    }

    CloseHandle( hRead );

    return;

 }

命名管道:將管道用作兩個無關聯進程間的通信渠道,程序員必須使用命名管道,它可以看作一種具有某名字的特殊類型文件。進程可以根據它的名字訪問這個管道。通過匿名管道,父和子進程可以單獨使用文件描述符來訪問他們所共享的管道,因爲子進程繼承了父進程的文件描述符,同時文件描述符用read()或write()函數的參數。因爲無關進程不能訪問彼此的文件描述符,所以不能使用匿名管道。由於命名管道提供該管道的一個等價文件名,任何知道此管道名字的進程都可以訪問它。下面是命名管道相對於匿名管道的優點:

        命名管道可以被無關聯進程使用。

        命名管道可以持久。創建它的程序退出後,它們仍然可以存在。

        命名管道可以在網絡或分佈環境中使用。

        命名管道容易用於多對一關係中。

與訪問匿名管道一樣,命名管道也是通過read()或write()函數來訪問。兩者之間的主要區別在於命名管道的創建方式以及誰可以反問它們。命名管道可以建立一個進程間通信的C/S模型。訪問命名管道的進程可能都位於同一臺機器上,或位於通過網絡通信的不同機器上。由於管道的名字可以通過管道所在服務器的邏輯名,所以能夠跨網絡訪問管道。例如,////ServerName//Pipe//MyPipe(不區分大小寫)可以作爲一個管道名字。假如Server1是網絡服務器的名字。當打開或訪問這個管道的調用解析文件名時,首先應該定位Server1,然後訪問MyPipe。例子如下:

服務器端:

int main( void )

{

    HANDLE pipehandle;

    char buf[ 256 ];

    DWORD bytesRead;

      if( ( pipehandle = CreateNamedPipe( "////.//Pipe//cao", PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, 1, 0, 0, 5000, NULL ) ) ==                 INVALID_HANDLE_VALUE )

    {

        printf( "CreateNamedPipe failed with error %d/n", GetLastError() );

        system( "pause" );

        return 0;

    }

    printf( "server is running/n" ); 

    if( ConnectNamedPipe( pipehandle, NULL ) == 0 )

    {

        printf( "connectNamedPipe failed with error %d/n", GetLastError() );

        CloseHandle( pipehandle );

        system( "pause" );

        return 0;

    }    

    if( ReadFile( pipehandle, buf, sizeof( buf ), &bytesRead, NULL ) == 0 )

    {

        printf( "ReadFile failed with error %d/n", GetLastError() );

        CloseHandle( pipehandle );

        system( "pause" );

        return 0;

    }

    printf( "%s/n", buf );      

    if ( DisconnectNamedPipe( pipehandle ) == 0 )

    {

        printf( "DisconnectNamedPipe failed with error %d/n", GetLastError() );

        CloseHandle( pipehandle );

        system( "pause" );

        return 0;

    }

    system( "pause" );

    return 0;

}

客戶端:

int main( void )

{

    HANDLE pipehandle;

    DWORD writesbytes;

    char buff[ 256 ];

    if( WaitNamedPipe( "////.//Pipe//cao", NMPWAIT_WAIT_FOREVER ) == 0 )

    {

        printf( "WaitNamedPipe failed with error %d/n", GetLastError() );

        system( "pause" );

        return 0;

    }

     if( ( pipehandle = CreateFile( "////.//Pipe//cao", GENERIC_READ | GENERIC_WRITE, 0, ( LPSECURITY_ATTRIBUTES )NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, ( HANDLE )NULL ) ) == INVALID_HANDLE_VALUE )

    {

        printf( "CreateFile failed with error %d/n", GetLastError() );

        system( "pause" );

        return 0;

    }

    ZeroMemory( &buff, sizeof( buff ) );

    gets( buff );

    if( WriteFile( pipehandle, buff, sizeof( buff ), &writesbytes, NULL ) == 0 )

    {

        printf( "WriteFile failed with error %d/n", GetLastError() );

        CloseHandle( pipehandle );

        system( "pause" );

        return 0;

    }    

    printf( "write %d bytes", writesbytes );

    CloseHandle( pipehandle );

    system( "pause" );

    return 0;

}

命名管道不僅可用於無關聯進程間、位於不同機器上的兩進程間的通信,而且可用於多對一通信,可以建立服務器進程,允許同時通過多個客戶訪問命名管道。命名管道常常用於多線程服務器。

六.共享內存

        共享內存也可以實現進程間的通信。進程需要可以被其他進程瀏覽的內存塊。希望訪問這個內存塊的其他進程請求對它的訪問,或由創建它的進程授予訪問內存塊的權限。可以訪問特定內存塊的所有進程對它具有即時可見性。共享內存被映射到使用它的每個進程的地址空間。所以,它看起來像是另一個在進程內聲明的變量。當一個進程寫共享內存,所有的進程都立即知道寫入的內容,而且可以訪問。

        進程間共享內存的關係與函數間全局變量的關係相似。程序中的所有函數都可以使用全局變量的值。同樣,共享內存塊可以被正在執行的所有進程訪問。內存塊可能共享一個邏輯地址,進程也可以共享某些物理地址。

        共享內存塊的創建必須由一個系統API調用來完成。在WIN32環境中,使用CreateFileMapping()、MapViewOfFile()以及MapViewOfFileEx() API能很好地完成。

        共享內存分配位於WIN32系統中2~3GB地址範圍內。一旦調用MapViewOfFile()和MapViewOfFileEx(),共享文件映射對象的所有進程都可以立即訪問此內存塊,而且在需要時,可以讀寫此內存塊。

七.動態數據交換

        動態數據交換( dynamic data exchange ) 是當今可用的進程間通信最強大和完善的形式之一。動態數據交換使用消息傳遞、共享內存、事務協議、客戶/服務器範疇、同步規則以及會話協議來讓數據和控制信息在進程間流動。動態數據交換對話( dynamic data exchange session, DDE )的基本模型是客戶、服務器。服務器對來自客戶的數據或動作作出反應。客戶和服務器可以以多種關係來通信。

        一個服務器可以與任意數量的客戶通信。一個客戶也可以與任意數量的服務器通信。單個DDE代理既可以是客戶,也可以是服務器。也就是說,進程可以從一個正爲另一個進程執行服務的DDE代理請求服務。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章