歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Unix知識 >> 關於Unix >> Linux核心--6.進程間通訊機制

Linux核心--6.進程間通訊機制

日期:2017/3/6 15:40:45   编辑:關於Unix
第五章 進程間通訊機制 進程在核心的協調下進行相互間的通訊。Linux支持大量進程間通訊(IPC)機制。除了信號和管道外,Linux 還支持 Unix 系統V中的IPC機制。 5.1 信號 信號是 Unix系統 中的最古老的進程間通訊方式。它們用來向一個或多個進程發送異步事件信 第五章 進程間通訊機制

進程在核心的協調下進行相互間的通訊。Linux支持大量進程間通訊(IPC)機制。除了信號和管道外,Linux 還支持Unix系統V中的IPC機制。 


5.1  信號
信號是Unix系統中的最古老的進程間通訊方式。它們用來向一個或多個進程發送異步事件信號。信號可以從鍵盤中斷中產生,另外進程對虛擬內存的非法存取等系統錯誤環境下也會有信號產生。信號還被shell程序用來向其子進程發送任務控制命令。 
系統中有一組被詳細定義的信號類型,這些信號可以由核心或者系統中其它具有適當權限的進程產生。使用kill命令(kill -l)可以列出系統中所有已經定義的信號。在我的系統(Intel系統)上運行結果如下: 

 1) SIGHUP  2) SIGINT  3) SIGQUIT  4) SIGILL
 5) SIGTRAP  6) SIGIOT  7) SIGBUS  8) SIGFPE
 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR

當我在Alpha AXP中運行此命令時,得到了不同的信號個數。除了兩個信號外,進程可以忽略這些信號中的絕大部分。其一是引起進程終止執行的SIGSTOP信號,另一個是引起進程退出的SIGKILL信號。 至於其它信號,進程可以選擇處理它們的具體方式。進程可以阻塞信號,如若不阻塞,則可以在自行處理此信號和將其轉交核心處理之間作出選擇。如果由核心來處理此信號,它將使用對應此信號的缺省處理方法。 比如當進程接收到SIGFPE(浮點數異常)時,核心的缺省操作是引起core dump和進程的退出。信號沒有固有的相對優先級。如果在同一時刻對於一個進程產生了兩個信號,則它們將可能以任意順序到達進程並進行處理。同時Linux並不提供處理多個相同類型信號的方式。即進程無法區分它是收到了1個還是42個SIGCONT信號。 

Linux通過存儲在進程task_struct中的信息來實現信號。信號個數受到處理器字長的限制。32位字長的處理器最多可以有32個信號而64位處理器如Alpha AXP可以有最多64個信號。當前未處理的信號保存在signal域中,並帶有保存在blocked中的被阻塞信號的屏蔽碼。除了SIGSTOP和SIGKILL外,所有的信號都能被阻塞。當產生可阻塞信號時,此信號可以保持一直處於待處理狀態直到阻塞釋放。Linux保存著每個進程處理每個可能信號的信息,它們保存在每個進程task_struct中的sigaction數組中。這些信息包括進程希望處理的信號所對應的過程地址,或者指示是忽略信號還是由核心來處理它的標記。通過系統調用,進程可以修改缺省的信號處理過程,這將改變某個信號的sigaction以及阻塞屏蔽碼。 

並不是系統中每個進程都可以向所有其它進程發送信號:只有核心和超級用戶具有此權限。普通進程只能向具有相同uid和gid的進程或者在同一進程組中的進程發送信號。信號是通過設置task_struct結構中signal域裡的某一位來產生的。如果進程沒有阻塞信號並且處於可中斷的等待狀態,則可以將其狀態改成Running,同時如確認進程還處在運行隊列中,就可以通過信號喚醒它。這樣系統下次發生調度時,調度管理器將選擇它運行。如果進程需要缺省的信號處理過程,則Linux可以優化對此信號的處理。例如SIGWINCH(X窗口的焦點改變)信號,其缺省處理過程是什麼也不做。 

信號並非一產生就立刻交給進程,而是必須等待到進程再次運行時才交給進程。每次進程從系統調用中退出前,它都會檢查signal和blocked域,看是否有可以立刻發送的非阻塞信號。這看起來非常不可靠,但是系統中每個進程都在不停地進行系統調用,如向終端輸出字符。當然進程可以選擇去等待信號,此時進程將一直處於可中斷狀態直到信號出現。對當前不可阻塞信號的處理代碼放置在sigaction結構中。 

如果信號的處理過程被設置成缺省則由核心來應付它。SIGSTOP信號的缺省處理過程是將當前進程的狀態改變成為Stopped並運行調度管理器以選擇一個新進程繼續運行。SIGFPE的缺省處理過程則是引起core dump並使進程退出。當然,進程可以定義其自身的信號處理過程。一旦信號產生,這個過程就將被調用。它的地址存儲在sigaction結構中。核心必須調用進程的信號處理例程,具體如何去做依賴於處理器類型,但是所有的CPU 必須處理這個問題:如果信號產生時,當前進程正在核心模式下運行並且馬上要返回調用核心或者系統例程的進程,而該進程處在用戶模式下。解決這個問題需要操縱進程的堆棧及寄存器。進程的程序計數器被設置成其信號處理過程的地址,而參數通過調用框架或者寄存器傳遞到處理例程中。當進程繼續執行時,信號處理例程好象普通的函數調用一樣。 

Linux是POSIX兼容的,所以當某個特定信號處理例程被調用時,進程可以設定哪個信號可以阻塞。這意味著可以在進程信號處理過程中改變blocked屏蔽碼。當信號處理例程結束時,此blocked屏蔽碼必須設置成原有值。 因此,Linux添加了一個過程調用來進行整理工作,通過它來重新設置被發送信號進程調用棧中的原有blocked屏蔽碼。 對於同一時刻幾個信號處理過程,Linux通過堆棧方式來優化其使用,每當一個處理過程退出時,下一個處理過程必須等到整理例程結束後才執行。 


5.2  管道
一般的Linux shell程序都允許重定向。如 

$ ls | pr | lpr


在這個管道應用中,ls列當前目錄的輸出被作為標准輸入送到pr程序中,而pr的輸出又被作為標准輸入送到lpr程序中。管道是單向的字節流,它將某個進程的標准輸出連接到另外進程的標准輸入。但是使用管道的進程都不會意識到重定向的存在,並且其執行結果也不會有什麼不同。shell程序負責在進程間建立臨時的管道。 






圖5.1 管道 

在Linux中,管道是通過指向同一個臨時VFS inode的兩個file數據結構來實現的,此VFS inode指向內存中的一個物理頁面。圖5.1中每個file數據結構指向不同的文件操作例程向量,一個是實現對管道的寫,另一個從管道中讀。 

這樣就隱藏了讀寫管道和讀寫普通的文件時系統調用的差別。當寫入進程對管道寫時,字節被拷貝到共享數據頁面中,當讀取進程從管道中讀時,字節從共享數據頁面中拷貝出來。Linux必須同步對管道的訪問。它必須保證讀者和寫者以確定的步驟執行,為此需要使用鎖、等待隊列和信號等同步機制。 

當寫者想對管道寫入時,它使用標准的寫庫函數。表示打開文件和打開管道的描敘符用來對進程的file數據 結構集合進行索引。Linux系統調用使用由管道file數據結構指向的write過程。這個write過程用保存在表示管道的VFS inode中的信息來管理寫請求。 

如果沒有足夠的空間容納對所有寫入管道的數據,只要管道沒有被讀者加鎖。則Linux為寫者加鎖,並把從寫入進程地址空間中寫入的字節拷貝到共享數據頁面中去。如果管道被讀者加鎖或者沒有足夠空間存儲數據,當前進程將在管道inode的等待隊列中睡眠,同時調度管理器開始執行以選擇其它進程來執行。如果寫入進程是可中斷的,則當有足夠的空間或者管道被解鎖時,它將被讀者喚醒。當數據被寫入時,管道的VFS inode被解鎖,同時任何在此inode的等待隊列上睡眠的讀者進程都將被喚醒。 

從管道中讀出數據的過程和寫入類似。 

進程允許進行非阻塞讀(這依賴於它們打開文件或者管道的方式),此時如果沒有數據可讀或者管道被加鎖, 則返回錯誤信息表明進程可以繼續執行。阻塞方式則使讀者進程在管道inode的等待隊列上睡眠直到寫者 進程結束。當兩個進程對管道的使用結束時,管道inode和共享數據頁面將同時被遺棄。 

Linux還支持命名管道(named pipe),也就是FIFO管道,因為它總是按照先進先出的原則工作。第一個被寫入 的數據將首先從管道中讀出來。和其它管道不一樣,FIFO管道不是臨時對象,它們是文件系統中的實體並且 可以通過mkfifo命令來創建。進程只要擁有適當的權限就可以自由使用FIFO管道。打開FIFO管道的方式稍有不同。其它管道需要先創建(它的兩個file數據結構,VFS inode和共享數據頁面)而FIFO管道已經存在,只需要由使用者打開與關閉。在寫者進程打開它之前,Linux必須讓讀者進程先打開此FIFO管道;任何讀者進程從中讀取之前必須有寫者進程向其寫入數據。FIFO管道的使用方法與普通管道基本相同,同時它們使用相同數據結構和操作。 


5.3  套接口

5.3.1  系統V IPC機制
Linux支持Unix系統V(1983)版本中的三種進程間通訊機制。它們是消息隊列、信號燈以及共享內存。這些系統V IPC機制使用共同的授權方法。只有通過系統調用將標志符傳遞給核心之後,進程才能存取這些資源。這些系統V IPC對象使用與文件系統非常類似的訪問控制方式。對象的引用標志符被用來作為資源表中的索引。這個索引值需要一些處理後才能得到。 
系統中所有系統V IPC對象的Linux數據結構包含一個ipc_perm結構,它含有進程擁有者和創建者及組標志符。另外還有對此對象(擁有者,組及其它)的存取模式以及IPC對象鍵。此鍵值被用來定位系統V IPC對象的引用標志符。這樣的鍵值一共有兩組:公有與私有。如果此鍵為公有,則系統中任何接受權限檢查的進程都可以找到系統V IPC對象的引用標志符。系統V IPC對象絕不能用一個鍵值來引用,而只能使用引用標志符。 


5.3.2  消息隊列
消息隊列允許一個或者多個進程向它寫入與讀取消息。Linux維護著一個msgque消息隊列鏈表,其中每個元素 指向一個描敘消息隊列的msqid_ds結構。當創建新的消息隊列時,系統將從系統內存中分配一個msqid_ds結構,同時將其插入到數組中。 





圖5.2 系統V IPC消息隊列 

每個msqid_ds結構包含一個ipc_perm結構和指向已經進入此隊列消息的指針。另外,Linux保留有關隊列修改時間信息,如上次系統向隊列中寫入的時間等。msqid_ds包含兩個等待隊列:一個為隊列寫入進程使用而另一個由隊列讀取進程使用。 

每次進程試圖向寫入隊列寫入消息時,系統將把其有效用戶和組標志符與此隊列的ipc_perm結構中的模式進行比較。如果允許寫入操作,則把此消息從此進程的地址空間拷貝到msg數據結構中,並放置到此消息隊列尾部。由於 Linux嚴格限制可寫入消息的個數和長度,隊列中可能容納不下這個消息。此時,此寫入進程將被添加到這個消息隊列的等待隊列中,同時調用調度管理器選擇新進程運行。當由消息從此隊列中釋放時,該進程將被喚醒。 

從隊列中讀的過程與之類似。進程對這個寫入隊列的訪問權限將被再次檢驗。讀取進程將選擇隊列中第一個消息(不管是什麼類型)或者第一個某特定類型的消息。如果沒有消息可以滿足此要求,讀取進程將被添加 到消息隊列的讀取等待隊列中,然後系統運行調度管理器。當有新消息寫入隊列時,進程將被喚醒繼續執行。 


5.3.3  信號燈
信號燈最簡單的形式是某個可以被多個進程檢驗和設置(test&set)的內存單元。這個檢驗與設置操作對每個進程而言是不可中斷或者說是一個原子性操作;一旦啟動誰也終止不了。檢驗與設置操作的結果是信號燈當前值加1, 這個值可以是正數也可以是負數。根據這個操作的結果,進程可能可以一直睡眠到此信號燈的值被另一個進程更改為止。信號燈可用來實現臨界區(critical region):某一時刻在此區域內的代碼只能被一個進程執行。 

如果你有多個協作進程從一個數據文件中讀取與寫入記錄。有時你可能需要這些文件訪問遵循嚴格的訪問次序。 那麼可在文件操作代碼上使用一個初始值為1的信號燈,它帶有兩個信號燈操作,一個檢驗並對信號燈 值減1,而另一個檢驗並加1。第一個訪問文件的進程將試圖將信號燈值減1,如果獲得成功則信號燈值變成了 0。此進程於是開始使用這個數據文件,但是此時如果另一進程也想將信號燈值減1,則信號燈值將為-1,這次操作將會失敗。它將掛起執行直到第一個進程完成對此數據文件的使用。此時這個等待進程將被喚醒,這次它對信號燈的操作將成功。 




圖5.3 系統V IPC信號燈 


每個系統V IPC信號燈對象對應一個信號燈數組,Linux使用semid_ds結構來表示。系統中所有semid_ds結構由一組semary指針來指示。在每個信號燈數組中有一個sem_nsems,它表示一個由sem_base指向的sem結構。授權的進程可以使用系統調用來操縱這些包含系統V IPC信號燈對象的信號燈數組。這個系統調用可以定義許多種操作,每個操作用三個輸入來描敘:信號燈索引、操作值和一組標志。信號燈索引是一個信號燈數組的索引,而操作值是將被加到信號燈上的數值。首先Linux將檢查是否所有操作已經成功。如果操作值與信號燈當前數值相加大於0,或者操作值與信號燈當前值都是0,操作將會成功。如果所有信號燈操作失敗,Linux僅僅會把那些操作標志沒有要求系統調用為非阻塞類型的進程掛起。進程掛起後,Linux必須保存信號燈操作的執行狀態並將當前進程放入等待隊列。系統還在堆棧上建立sem_queue結構並填充各個域。這個sem_queue結構將被放到此信號燈對象等待隊列的尾部(使用 sem_pending和sem_pending_last指針)。系統把當前進程置入sem_queue結構中的等待隊列(sleeper)中,然後啟動調度管理器選擇其它進程運行。 

如果所有這些信號燈操作都成功則無需掛起當前進程,Linux將對信號燈數組中的其他成員進行相同操作,然後檢查那些處於等待或者掛起狀態的進程。首先,Linux將依次檢查掛起隊列(sem_pending) 中的每個成員,看信號燈操作能否繼續。如果可以則將其sem_queue結構從掛起鏈表中刪除並對信號燈數組發出信號燈操作。Linux還將喚醒處於睡眠狀態的進程並使之成為下一個運行的進程。如果在對掛起隊列的遍歷過程中有的信號燈操作不能完成則Linux將一直重復此過程,直到所有信號燈操作完成且沒有進程需要繼續睡眠。 

但是信號燈的使用可能產生一個嚴重的問題:死鎖。當一個進程進入臨界區時它改變了信號燈的值而離開臨界區時由於運行失敗或者被kill而沒有改回信號燈時,死鎖將會發生。Linux通過維護一組描敘信號燈數組變化的鏈表來防止該現象的發生。它的具體做法是讓Linux將把此信號燈設置為進程對其進行操作前的狀態。這些狀態值被保存在使用該信號燈數組進程的semid_ds和task_struct結構的sem_undo結構中。 

信號燈操作將迫使系統對它引起的狀態變化進行維護。Linux為每個進程維護至少一個對應於信號燈數組的sem_undo結構。如果請求進行信號燈操作的進程沒有該結構,則必要時Linux會為其創建一個。這個sem_undo 結構將同時放入此進程的task_struct結構和此信號燈數組的semid_ds結構中。當對信號燈進行操作時,信號燈變化值的負數被置入進程的sem_undo結構中該信號的入口中。所以當操作值為2時,則此信號燈的調整入口中將加入一個-2。 

象正常退出一樣,當進程被刪除時,Linux將遍歷該進程的sem_undo集合對信號燈數組使用調整值。如果信號燈集合被刪除而sem_undo數據結構還在進程的task_struct結構中則此信號燈數組標志符將被置為無效。此時 信號燈清除代碼只需丟棄sem_undo結構即可。 


5.3.4  共享內存
共享內存允許一個或多個進程通過同時出現在它們虛擬地址空間中的內存來通訊。此虛擬內存的頁面出現在每個共享進程頁表中。但此頁面並不一定位於所有共享進程虛擬內存的相同位置。和其它系統V IPC對象的使用方法一樣,對共享內存區域的訪問是通過鍵和訪問權限檢驗來控制的。一旦內存被共享,則再不會檢驗進程對對象的使用方式。它依賴於其它機制,如系統V信號燈,來同步對共享內存的訪問。 






圖5.4 系統V IPC共享內存 

每個新創建的共享內存區域由一個shmid_ds數據結構來表示。它們被保存再shm_segs數組中。 shmid_ds數據結構描敘共享內存的大小,進程如何使用以及共享內存映射到其各自地址空間的方式。由共享內存創建者控制對此內存的存取權限以及其鍵是公有還是私有。如果它由足夠權限,它還可以將此共享內存加載到物理內存中。 

每個使用此共享內存的進程必須通過系統調用將其連接到虛擬內存上。這時進程創建新的vm_area_struct來描敘此共享內存。進程可以決定此共享內存在其虛擬地址空間的位置,或者讓Linux選擇一塊足夠大的區域。 新的vm_area_struct結構將被放到由shmid_ds指向的vm_area_struct鏈表中。通過vm_next_shared和vm_prev_shared 指針將它們連接起來。虛擬內存在連接時並沒有創建;進程訪問它時才創建。 

當進程首次訪問共享虛擬內存中的頁面時將產生頁面錯。當取回此頁面後,Linux找到了描敘此頁面的vm_area_struct數據結構。它包含指向使用此種類型虛擬內存的處理函數地址指針。共享內存頁面錯誤處理 代碼將在此shmid_ds對應的頁表入口鏈表中尋找是否存在此共享虛擬內存頁面。如果不存在,則它將分配物理頁面並為其創建頁表入口。同時還將它放入當前進程的頁表中,此入口被保存在shmid_ds結構中。這意味著下個試圖訪問此內存的進程還會產生頁面錯誤,共享內存錯誤處理函數將為此進程使用其新創建的物理頁面。這樣,第一個訪問虛擬內存頁面的進程創建這塊內存,隨後的進程把此頁面加入到各自的虛擬地址空間中。 

當進程不再共享此虛擬內存時,進程和共享內存的連接將被斷開。如果其它進程還在使用這個內存,則此操作只影響當前進程。其對應的vm_area_struct結構將從shmid_ds結構中刪除並回收。當前進程對應此共享內存地址的頁表入口也將被更新並置為無效。當最後一個進程斷開與共享內存的連接時,當前位於物理內存中的共享內存頁面將被釋放,同時還有此共享內存的shmid_ds結構。 

當共享內存沒有被鎖入物理內存時,情況將更加復雜。此時共享內存頁面可能會在內存使用高峰期,被交換到系統的交換磁盤上。共享內存如何被交換與調入物理內存將在mm一章中詳細描敘。

Copyright © Linux教程網 All Rights Reserved