原著: David A Rusling 翻譯: Banyan & FIFA 第五章 進程間通訊機制 進程在核心的協調下進行相互間的通訊。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維護著一個msgq