進程間通信就是在不同進程之間傳播或交換信息,那麼不同進程之間存在著什麼雙方都可以訪問的介質呢?進程的用戶空間是互相獨立的,一般而言是不能互相訪問的,唯一的例外是共享內存區。但是,系統空間卻是“公共場所”,所以內核顯然可以提供這樣的條件。除此以外,那就是雙方都可以訪問的外設了。在這個意義上,兩個進程當然也可以通過磁盤上的普通文件交換信息,或者通過“注冊表”或其它數據庫中的某些表項和記錄交換信息。廣義上這也是進程間通信的手段,但是一般都不把這算作“進程間通信”。因為那些通信手段的效率太低了,而人們對進程間通信的要求是要有一定的實時性。Linux下進程通信的八種方法:管道(pipe),命名管道(FIFO),內存映射(mapped memeory),消息隊列(message queue),共享內存(shared memory),信號量(semaphore),信號(signal),套接字(Socket).
(1) 管道(pipe):管道允許一個進程和另一個與它有共同祖先的進程之間進行通信;管道是Linux支持的最初Unix IPC形式之一,具有以下特點:
管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道;只能用於父子進程或者兄弟進程之間(具有親緣關系的進程);
單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,並且只存在與內存中。數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩沖區的末尾,並且每次都是從緩沖區的頭部讀出數據。
管道兩端用描述字fd[0]以及fd[1]來描述,管道的兩端是固定了任務的。fd[0]只能用於讀,稱其為管道讀端;另一端則只能用於寫,由描述字fd[1]來表示,稱其為管道寫端。
(2) 命名管道(FIFO):類似於管道,但是它可以用於任何兩個進程之間的通信,命名管道在文件系統中有對應的文件名。命名管道通過命令mkfifo或系統調用mkfifo來創建;管道應用的一個重大缺陷就是沒有名字,因此只能用於親緣進程之間的通信。後來從管道為基礎提出命名管道(named pipe,FIFO)的概念,該限制得到了克服。FIFO不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的文件形式存在於文件系統中。這樣,即使與FIFO的創建進程不存在親緣關系的進程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信(能夠訪問該路徑的進程以及FIFO的創建進程之間),因此,通過FIFO不相關的進程也能交換數據。值得注意的是,FIFO嚴格遵循先進先出(first
in first out),對管道及FIFO的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如lseek()等文件定位操作。
(3) 信號(signal):信號是比較復雜的通信方式,用於通知接收進程有某種事情發生,除了用於進程間通信外,進程還可以發送信號給進程本身;Linux除了支持UNIX早期信號語義函數signal外,還支持語義符合POSIX.1標准的信號函數sigaction(實際上,該函數是基於BSD的,BSD即能實現可靠信號機制,又能夠統一對外接口,用sigaction函數重新實現了signal函數的功能);在一個信號的生命周期中有兩個階段:生成和傳送。當一個事件發生時,需要通知一個進程,這時生成一個信號。當進程識別出信號的到來,就采取適當的動作來傳送或處理信號。在信號到來和進程對信號進行處理之間,信號在進程上掛起(pending)。
內核為進程生產信號,來響應不同的事件,這些事件就是信號源。主要的信號源如下:
異常:進程運行過程中出現異常;
其它進程:一個進程可以向另一個或一組進程發送信號;
終端中斷:Ctrl-C,Ctrl-\等;
作業控制:前台、後台進程的管理;
分配額:CPU超時或文件大小突破限制;
通知:通知進程某事件發生,如I/O就緒等;
報警:計時器到期。
在 Linux 中,信號的種類和數目與硬件平台有關。內核用一個字代表所有的信號,每個信號占一位,因此一個字的位數就是系統可以支持的最多信號種類數。(4) 內存映射(mapped memory):內存映射允許任何多個進程間通信,每一個使用該機制的進程通過把一個共享的文件映射到自己的進程地址空間來實現它;
內存映射實際上是把文件映射到一塊內存上,在進程中返回映射的地址,以後對該文件的操作就象操作內存一樣,加快了文件/設備的訪問速度。
與內存映射相關的另外一個概念就是共享內存,A,B進程共享內存的意思是將共享內存映射到A,B各自的進程地址空間中去,以後無論進程A或者是進程B對共享內存的讀寫,都彼此知道。
從功能上區分,內存映射是為了加快文件/設備的讀寫速度,而共享內存是加快多個進程間通信。普通的進程讀寫文件,需要4次內核copy數據,而內存映射只需要2次,一次是把文件讀到內存,另一次是把數據從內存寫到文件中去。
(5) 消息隊列(message queue): 消息隊列是消息的連接表,包括POSIX消息對和System V消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能成該無格式字節流以及緩沖區大小受限等缺點;Linux的消息隊列(queue)實質上是一個鏈表, 它有消息隊列標識符(queue ID). msgget創建一個新隊列或打開一個存在的隊列; msgsnd向隊列末端添加一條新消息; msgrcv從隊列中取消息, 取消息是不一定遵循先進先出的, 也可以按消息的類型字段取消息.
消息隊列也稱為報文隊列,消息隊列是隨內核持續的,只有在內核重起或顯示刪除一個消息隊列時,該消息隊列才會真正刪除系統中記錄消息隊列的數據結構struct ipc_ids msg_ids位於內核中,系統中所有消息隊列都可以在結構msg_ids中找到訪問入口
消息隊列其實就是一個消息的鏈表,每個消息隊列有一個隊列頭,稱為struct msg_queue,這個隊列頭描述了消息隊列的key值,用戶ID,組ID等信息,但它存於內核中而結構體struct msqid_ds能夠返回或設置消息隊列的信息,這個結構體位於用戶空間中,與msg_queue結構相似
消息隊列允許一個或多個進程向它寫入或讀取消息,消息隊列是消息的鏈表。消息是按消息類型訪問,進程必須指定消息類型來讀取消息,同樣,當向消息隊列中寫入消息時也必須給出消息的類型,如果讀隊列使用的消息類型為0,則讀取隊列中的第一條消息。內核空間的結構體msg_queue描述了對應key值消息隊列的情況,而對應於用戶空間的msqid_ds這個結構體,因此,可以操作msgid_ds這個結構體來操作消息隊列。
(6) 信號量(semaphore):信號量主要作為進程間以及同進程不同線程之間的同步手段;信號量及信號量上的P,V操作是E.W.Dijkstra 在1965年提出的一種解決同步、互斥問題的較通用的方法,並在很多操作系統中得以實現, Linux改進並實現了這種機制。
信號量(semaphore )實際是一個整數,它的值由多個進程進行測試(test)和設置(set)。就每個進程所關心的測試和設置操作而言,這兩個操作是不可中斷的,或稱“原子”操作,即一旦開始直到兩個操作全部完成。測試和設置操作的結果是:信號量的當前值和設置值相加,其和或者是正或者為負。根據測試和設置操作的結果,一個進程可能必須睡眠,直到有另一個進程改變信號量的值。信號量可用來實現所謂的“臨界區”的互斥使用,臨界區指同一時刻只能有一個進程執行其中代碼的代碼段。為了進一步理解信號量的使用,下面我們舉例說明。假設你有很多相互協作的進程,它們正在讀或寫一個數據文件中的記錄。你可能希望嚴格協調對這個文件的存取,於是你使用初始值為1的信號量,在這個信號量上實施兩個操作,首先測試並且給信號量的值減1,然後測試並給信號量的值加1。當第一個進程存取文件時,它把信號量的值減1,並獲得成功,信號量的值現在變為0,這個進程可以繼續執行並存取數據文件。但是,如果另外一個進程也希望存取這個文件,那麼它也把信號量的值減1,結果是不能存取這個文件,因為信號量的值變為-1。這個進程將被掛起,直到第一個進程完成對數據文件的存取。當第一個進程完成對數據文件的存取,它將增加信號量的值,使它重新變為1,現在,等待的進程被喚醒,它對信號量的減1操作將獲得成功。
上述的進程互斥問題,是針對進程之間要共享一個臨界資源而言的,信號量的初值為1。實際上,信號量作為資源計數器,它的初值可以是任何正整數,其初值不一定為0或1。另外,如果一個進程要先獲得兩個或多個的共享資源後才能執行的話,那麼,相應地也需要多個信號量,而多個進程要分別獲得多個臨界資源後方能運行,這就是信號量集合機制,Linux討論的就是信號量集合問題。
(7) 共享內存 (shared memory):它使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。這是針對其他通信機制運行效率較低而設計的。它往往與其他通信機制,如信號量結合使用,以達到進程間的同步及互斥;分配一個新的共享內存塊會創建新的內存頁面。因為所有進程都希望共享對同一塊內存的訪問,只應由一個進程創建一塊新的共享內存。再次分配一塊已經存在的內存塊不會創建新的頁面,而只是會返回一個標識該內存塊的標識符。一個進程如需使用這個共享內存塊,則首先需要將它綁定到自己的地址空間中。這樣會創建一個從進程本身虛擬地址到共享頁面的映射關系。當對共享內存的使用結束之後,這個映射關系將被刪除。當再也沒有進程需要使用這個共享內存塊的時候,必須有一個(且只能是一個)進程負責釋放這個被共享的內存頁面。
共享內存是指把共享數據放到共享內存區域,任何需要訪問共享內存區域數據的進程都在自己的進程地址空間中開辟一個新的內存區域,用來映射共享內存數據的物理頁面,所有需要訪問共享內存區域的進程都要把該共享區域映射到本進程的地址空間中去,系統用shmget獲得或創建一個IPC的共享內存區域,並返回相應的標識符,通過shmat將共享內存區域映射到進程的地址空間中去,每一個共享內存區域都對應shm文件系統上的一個文件,相當於映射shm文件系統上的同名文件到共享內存區域,shmdt是解除對共享內存區的映射,shmctl是對共享內存區的控制操作。
共享內存作用是加快進程間的通信,共享內存的修改對進程是可見的,將共享內存區域映射到進程地址空間中去,而內存映射是加快進程訪問文件/設備的速度(1)系統V共享內存,不把數據寫入磁盤,而mmap()映射普通文件可以指定何時把數據寫入磁盤
V共享內存是通過特殊的文件系統shm中的文件實現的,文件系統shm的安裝點在交換區上,重新引導後,數據會丟失
(2)系統V共享內存是隨內核持續的,所有訪問共享內存的進程都已經終止,它仍然存在,對內核引導前,對該共享內存區域的任何改寫操作一直保留
(3)mmap()映射普通文件是隨進程持續的,一定要注意何時終止進程
(8) 套接字(Socket):它是更為通用的進程間通信機制,可用於不同機器之間的進程間通信。起初是由UNIX系統的BSD分支開發出來的,但現在一般可以移植到其他類UNIX系統上:Linux和System V的變種都支持套接字;