第 5 章 程序間通信的機制

程序之間、程序與核心之間互相通信,以協調它們的活動。Linux支援一系列程序間通信
機制,信號和管道是其中的兩種,此外還有SVR的程序間通信機制。
5.1 信號
信號是unix系統最早使用的程序間通信方法之一。它們用來對一個或多個程序發送非同步事
件。信號可以由鍵盤中斷產生,也可以由程序試圖讀取虛擬記憶體中不存在的位置而引
發。另外,信號也可以用於外殼程式向它們的子程序發送作業控制命令。
有一組預先定義的信號,核心可以產生,具有相應優先權的程序也可以產生。使用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平台而言,數字又有所不同。程序可以選擇忽略產生的大部份信號,除了兩個
特殊的之外:使程序停止運行的SIGSTOP和使程序退出的SIGKILL。對此外的信號,進
程可以任意選擇處理的方法。程序可以阻塞信號,或者由自己的程式碼處理信號,或者交由
核心來處理信號。
如果是核心處理信號,它將進行這個信號要求的預設處理。例如,程序收到SIGFPE(浮
點溢出)信號時,預設處理是core dump並退出。信號之間沒有天然的相對優先關系。如
果兩個信號同時為同一個程序產生,它們可以以任意順序交給程序處理。程序也沒有任何
方法區別自己收到的是一個還是四十二個SIGCONT信號。Linux用存儲在程序的
task_struct中的信息實現信號。所支援的信號數受到字長的限制,32位處理器可有32個信
號,64位處理器就可有64位信號。目前未處理的信號保存在signal域中,blocked中為阻
塞信號的遮罩。除了SIGSTOP和SIGKILL之外,一切信號都能夠被阻塞。如果一個被阻
塞的信號產生,除非解除其阻塞,否則它不會被處理。Linux持有每一個程序如何處理每
一個可能的信號的信息(放在每個程序的task_struct指向的一組sigaction資料結構中)。
在sigaction之中,要麼含有信號處理例程的位址,要麼放置標誌告訴系統:程序希望忽略
這個信號,或者程序希望核心處理這個信號。程序通過系統調用來修改信號處理方法,這
些系統調用把相應信號的sigaction或者blocked進行修改。
並不是任何一個程序都能夠發信號給所有程序,只有核心和超級用戶程序才有此權力。普
通程序只能發送信號給具有相同uid和gid的程序,或者同一個程序組中的程序。要產生
一個信號,只要把task_struct中signal域的相應比特設置一下。如果程序沒有阻塞信號,
並且處於可中斷的等待狀態,那麼它會被喚醒,轉變為運行態,並確認它在運行隊列中。
這樣,排程程式在下一次排程時將把它作為運行的候選程序之一。如果預設處理是需要
的,Linux能夠最佳化信號的處理。例如,如果產生了信號SIGWINCH(X-Window改變焦
點),而預設處理程式正在使用,那麼什麼也不會做。
信號並非在產生後立刻送給程序,而是要等到程序重新被運行。每當一個程序從系統調用
中退出,它的signal和blocked域都被檢查,如果發現未被阻塞的信號,則將送給程序。
這看起來似乎不太可靠,但是事實上每個程序總是在不斷進行系統調用,例如把字元寫到
終端。如果願意,程序可以選擇等待信號,這是它處於可被信號的來臨中斷的暫停狀態。
Linux的信號處理程式碼通過查看sigaction結構以便決定處理方法。
如果信號的處理被設置為預設,那麼核心將負責處理它。SIGSTOP信號的預設處理停止當
前程序的運行並且使用排程程式選擇下一個運行的程序。SIGFPE信號的預設處理使程序
core dump並且讓它退出。程序也可以選擇指定自己的處理程式碼。該程式碼為一個每當信號
產生時可調用的例程,sigaction結構保存有這個例程的位址。核心必須調用程序的信號處
理例程,如何實現這一點與具體的處理器相關,但是無論如何CPU必須注意到目前程序
正處於核心模式運行,並且即將返回使用者模式。通過對堆疊和暫存器的操作能解決這個問題。進
程的程式計數器被設置到信號處理例程的位址,調用參數被通過調用frame或是暫存器傳遞。
當程序得以繼續時,看來似乎信號處理例程是被正常調用的。
Linux是POSIX兼容的,所以程序能夠在信號處理例程調用時指定哪些信號被阻塞。這就
意味著在信號處理例程中改變blocked遮罩。例程結束時,blocked遮罩必須被恢復原有
值。所以Linux增加了一個清理程序,該程序負責把原始blocked遮罩恢復到接收信號進
程的調用堆疊的頂端。某些情況下,幾個信號處理例程需要被用堆疊方式調用,以便保証每
個例程退出時,立刻調用下一個例程,直至清理例程被調用。對此,Linux需要進行最佳
化。
5.2 管道
普通的Linux外殼都允許重定向。例如
$ ls | pr | lpr
把ls命令的輸出檔案名通過管道作為pr命令的標準輸入,後者對之進行分頁
($$paginate?$$),最後pr的標準輸出又通過管道送入lpr的標準輸入,lpr把結果打在
預設印表機上。所以,管道就是連接一個程序的標準輸出到另外一個程序的標準輸入的單
向位元流。程序無法知道這個重定向,仍然正常工作。負責在程序之間建立臨時管道的是
外殼。
在Linux中,管道是通過兩個指向同一個虛擬檔案系統i節點的file結構來實現的,i節點
本身則指向記憶體中的一個實體頁。圖5.1(略)顯示,每個file資料結構含有指向不同的文
件操作例程向量的指標,一個用於寫管道,另一個用於讀管道。
這就隱藏了與讀寫普通檔案的一般的系統調用之間的區別。當寫程序在寫管道時,位元被
拷到共享資料頁上,而當讀程序在當讀管道時,位元被從共享資料頁上拷出來。Linux 必
須對共享資料頁的存取進行同步,它使用鎖、等待隊列和信號來保証讀寫程序之間的輪
流。
當寫程序想寫管道時,它使用標準寫函式庫函數。這些函式庫函數都傳遞檔案描述器,而檔案描述
符是程序的file資料結構集合的索引,每一個代表一個打開的檔案或者一個打開的管道。
Linux系統調用使用描述這個管道的file資料結構指向的例程。那個寫例程使用表示管道的i
節點中保存的信息來管理寫要求。
如果有足夠的空間供所有的位元寫入管道,只要管道沒有為讀程序鎖住,Linux將會為寫
程序鎖住管道,並且把所有的待寫位元從程序的位址空間拷到共享資料頁中。如果管道為
讀程序鎖住,或者沒有足夠的資料空間,那麼將使目前程序睡眠在管道i節點的等待隊列,
調用排程程式運行另外一個程序。程序的狀態是可中斷的,所以它能收到信號,能在寫數
據空間變得足夠或是管道被解鎖之後被寫程序喚醒。寫完資料之後,管道的i節點被解鎖,
睡眠在i節點等待隊列的讀程序將被喚醒。
從管道讀資料與寫資料非常類似。
允許程序做非阻塞讀(依賴於打開檔案或管道的模式),在此情況下,如果沒有資料可讀
或者管道被鎖住,將返回一個錯誤。這意味著程序可以繼續運行。另一種方法是等在管道i
節點的等待隊列裡直到寫程序完成工作(即阻塞讀--
譯者注)。當兩個程序都完成了管道上的工作,管道i節點將被與共享資料頁一起丟棄。
Linux也支援“有名”管道,也被稱為FIFO,因為管道的工作方式是先入先出的。最早寫入
這種管道的資料也最早被讀出。與一般管道不同的是,FIFO不是臨時對像,而是檔案系統
中的實體,可以用mkfifo命令創建出來。只要程序有足夠的存取權限,就能自由地使用FIF
O。打開FIFO的方式和打開管道的方式也稍有不同。一個管道(包括它的兩個file資料結構
,它的虛擬檔案系統i節點和共享資料頁)是一次性產生的,而FIFO是已經存在的,由用
戶負責它的打開和關閉。如果在寫程序打開FIFO之前,讀程序先打開了它,或者讀程序去
讀一個沒有被寫入資料的管道,Linux必須加以處理。除此之外,FIFO與管道完全相同,
因為它們採用的資料結構和操作是一致的。
5.3 socket
注:在網路章完成後增加。
5.3.1 SVR的程序間通信機制
Linux支援三類SVR首創的程序間通信機制:消息隊列、信號燈和共享記憶體。這些SVR程序
間通信機制都共用相同的認証方法。程序只能通過系統調用向核心傳送一個唯一的引用標
識,才能存取這些資源。這些SVR程序間通信對像的存取通過存取權限來控制,與檔案存
取的權限控制非常類似。由對像的創造者通過系統調用來設置對像的存取權限。在每一種
機制之中,對像的引用標識被用作資源表的索引。當然,索引本身並不簡單,還需要一些
操作來產生。
所有表示SVR程序間通信對像的Linux資料結構都包含一個ipc_perm結構,該結構包含了所
有者和創造者程序的用戶和組標識。對像的存取模式(所有者、組和其它)以及對像的ke
y(這句似乎少了些什麼,但是原文如此--
譯者注)。這個key只是用於定位對像的引用標識的一種方法。支援兩類key:公開key和秘
密key。如果key是公開的,那麼系統中的任何程序,只要有足夠的存取權限,都能找到對
像的引用標識。SVR程序間通信對像絕對不能用key來引用,而只能用引用標識來引用。
5.3.2 消息隊列
消息隊列允許一個或者多個程序讀/寫消息。Linux維護一個消息隊列表--
msgque向量,其中每一個元素指向一個msqid_ds資料結構,該資料結構將完整地描述消息
隊列。當創建一個消息隊列時,從系統記憶體中分配出一個新的msqid_ds資料結構,插入向
量之中。
每一個msqid_ds資料結構包含了一個ipc_perm資料結構和指向隊列中消息的一批指標。
另外,Linux保存有隊列修改時間,例如最後一次寫隊列的時間等等。msqid_ds也包含兩
個等待隊列,一個用於隊列的寫者程序,另一個用於隊列的讀者程序。
每當程序試圖寫消息到寫隊列中時,它的有效用戶標識和組標識將與隊列的ipc_perm資料
結構中的存取模式進行比較。如果程序能夠寫隊列,那麼消息將被從程序的位址空間中拷
到一個msg資料結構中,並且把該資料結構放到消息隊列的尾部。根據應用程序之間的約
定,每個消息被用一個類型標記出來,這裡的類型劃分是與應用有關的。然而,由於
Linux限制了能向隊列中寫的消息的長度和數量,隊列中剩餘的空間可能不足容納這次要
寫的消息。這是,程序將被加入消息隊列的寫等待隊列之中,調用排程程式來選擇一個新
的程序運行。當有消息從隊列中讀出後,寫等待隊列中的程序將被喚醒。
讀消息隊列也類似。同樣,需要檢查程序對於寫隊列的存取權限。讀程序可以選擇讀取隊
列中的第一個消息,而不計其類型,或者選擇只讀特定類型的消息。如果沒有消息滿足讀
程序的標準,那麼它將被加入消息隊列的讀等待隊列,然後運行排程程式。當一個新消息
寫入隊列時,程序將被喚醒,重新運行。
5.3.3 信號燈
最簡單類型的信號燈是記憶體中一個可以被一個或者多個程序測試並設置的位置。就程序而
言,測試並設置操作是不可中斷的,或者說是最基本的。測試並設置操作的結果是信號燈當
前值加上了所設置的數值,這個數值可以隨便是正的或負的。根據測試並設置操作的結
果,程序可能會被迫睡眠,直到另一個程序改變信號燈的值為止。信號燈可以用於實現關
鍵區--一次只能有一個程序進入運行的關鍵程式碼區域。
例如,假設你有很多程序在同時讀寫一個資料檔案的記錄,你想對檔案的存取進行嚴格的
協調。你可以用一個初始值是1的信號燈,在檔案操作程式碼的前後,放上兩個信號燈操
作。第一個信號燈操作是測試並且減少信號燈的值,第二個信號燈操作是測試並且增加信
號燈的值。實際運行時,存取檔案的第一個程序將試圖減少信號燈的值,它當然會成功,
這時信號燈的值變成了0。於是程序能夠繼續下去,使用資料檔案。這時,如果另外一個
程序也想使用檔案,當它試圖減少信號燈的值的時候,它會失敗,返回結果-1。該程序將
會被暫停,直到第一個程序完成該資料檔案的操作。第一個程序完成資料檔案操作時,它
增加信號燈的值,使之回到1。這時等待程序可以被喚醒,它增加信號燈數值的嘗試將會
成功。
每一個SVR信號燈對像描述一個信號燈序列,Linux使用semid_ds資料結構來代表之。系
統中所有的semid_ds資料結構都被semary向量中的一組指標所指引。每一個信號燈序列
中含有sem_nsems個信號燈,每一個信號燈用sem_base指向的一個sem 資料結構描述。
所有有權操作信號燈序列的程序可以通過系統調用來對它們進行操作。系統調用可以指定
很多操作,每個操作用三個輸入來描述:信號燈索引,操作值和一組標誌。信號燈索引是
信號燈序列中的索引,操作值是將被加到信號燈目前值上的數值。首先Linux測試是否所
有的操作都能成功。操作能夠成功當且僅當操作值加到目前值上之後結果大於0,或者操
作值與目前值都是0。如果其中的任何信號燈操作失敗,Linux將暫停程序,除非操作標
志要求系統調用是非阻塞的。如果需要暫停程序,Linux將保存信號燈操作的狀態,並把
目前程序送入等待隊列。實現的方法是創立並填寫一個sem_queue資料結構,放在信號燈
對像的等待隊列之中(使用sem_pending和sem_pending_last指標),並調用排程程式運
行另外一個程序。(這句話是譯者根據自己理解翻譯的,未必確切--譯者注)
如果所有的信號燈操作都成功並且目前程序不需要被暫停,那麼Linux繼續下去,對信號
燈序列中適當的成員進行操作。現在Linux必須檢查所有的等待、懸掛的程序能否進行它
們的操作了。它依次看每一個信號燈操作等待隊列sem_pending,測試這些操作這一次是
否會成功。如果能成功,則從隊列中刪除sem_queue資料結構,進行信號燈操作,並喚醒
睡眠程序,使它在排程程式下一次運行時具備候選資格。Linux從頭檢查等待隊列,直到
發現無法再進行任何信號燈操作,也不可能有更多程序被喚醒。
信號燈還有一個死鎖的問題。當一個程序進入關鍵區,改變信號燈的數值之後,由於癱瘓
或者被殺而無法離開關鍵區,就會發生死鎖。Linux防止死鎖的方法是維護信號燈序列的
矯正表。這裡的思想是用這些矯正值把信號燈恢復到操作之前的原有狀態。矯正值放在
sem_undo資料結構裡,同時在信號燈序列的semid_ds資料結構和程序的task_struct資料
結構之中排隊。
每一個信號燈操作都要求有一個矯正值。Linux為每個程序對每個信號燈序列的操作最多
保存一個sem_undo資料結構。如果需要的程序沒有此資料結構,則在需要時創建一個。
新的sem_undo資料結構同時排在程序的task_struct資料結構和信號燈序列的semid_ds數
據結構之中。當對信號燈序列進行操作時,操作值的相反數會被加在程序的sem_undo數
據結構的矯正值序列中相應於這個信號燈的那個。所以,如果操作值是2,矯正值加上的
就是-2。程序被刪除時,Linux處理它們的sem_undo資料結構,對信號燈進行矯正。如果
刪除一組信號燈,那麼sem_undo資料結構仍然排在程序的task_struct之中,但是信號燈
序列的標識被置為無效。遇到這種情況,信號燈清除程式碼就只要丟棄sem_undo資料結構
即可。
5.3.4 共享記憶體
共享記憶體允許一個或者多個程序通過同時出現在它們的虛擬位址空間內的記憶體進行通信。虛
存的頁面由各個程序的頁表的入口指引,並不需要共享記憶體在每一個程序的虛擬記憶體的地
址都相同。與所有的SVR程序間通信對像一樣,共享記憶體的存取由key和存取權限檢查
來控制。一旦記憶體被共享,無法對程序如何使用它進行檢查。必須依賴其它機制,如信號
燈,來對記憶體的存取進行同步。
每一個新創建的共享記憶體區域由一個shmid_ds資料結構代表。這些資料接被保存在
shm_segs向量之中。shmid_ds資料結構描述了共享記憶體區域的大小,使用共享記憶體區域的
程序的數量,和關於共享記憶體如何映射到程序的位址空間的信息。正是共享記憶體的創建者
控制著其存取權限和其key是否公開。如果創建者具有足夠的存取權限,它可以把共享
記憶體鎖定在實體記憶體之中。
每一個希望共享記憶體的程序必須通過系統調用與虛擬記憶體相聯,從而產生一個
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資料結構也釋放。
如果共享虛擬記憶體沒有鎖定在實體記憶體,就更復雜一些。這時,共享記憶體的頁面在記憶體使
用頻繁時可以被換出到系統的交換磁碟上。共享記憶體如何換入和換出虛擬記憶體,見“記憶體
管理”一章。