《Linux原理與結(jié)構(gòu)》課件第10章_第1頁
《Linux原理與結(jié)構(gòu)》課件第10章_第2頁
《Linux原理與結(jié)構(gòu)》課件第10章_第3頁
《Linux原理與結(jié)構(gòu)》課件第10章_第4頁
《Linux原理與結(jié)構(gòu)》課件第10章_第5頁
已閱讀5頁,還剩149頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認(rèn)領(lǐng)

文檔簡介

第十章進?程?間?通?信10.1信號10.2管道10.3消息隊列10.4共享內(nèi)存利用鎖、信號量、RCU等可以協(xié)調(diào)進程的動作,實現(xiàn)進程間的互斥與同步,但所傳遞的信息量極少,難以用于進程間通信。為了使相互協(xié)作的進程能夠更好地工作,除了互斥與同步之外,還需要提供一些進程間的通信機制,如通知機制、信息傳遞機制、信息共享機制等。

早期的Unix僅提供了兩種進程間通信機制,分別是用于通知的信號和用于傳遞數(shù)據(jù)的管道。1983年,AT&T在UnixSystemV中引入了三種新的進程間通信機制,分別是信息隊列、共享內(nèi)存和信號量集合。在隨后的發(fā)展中,SystemV的三種通信機制被吸收進POSIX標(biāo)準(zhǔn),但兩者使用了完全不同的API,因而具有完全不同的實現(xiàn)方式。

Linux繼承了Unix的傳統(tǒng),提供了多種進程間的通信機制,如用于通知的信號,用于交換信息的管道和消息隊列,用于共享信息的共享內(nèi)存等。在所有的通信手段中,信號是最基本的,管道與消息隊列是最直觀的,共享內(nèi)存是最快速的。當(dāng)然,通過網(wǎng)絡(luò)協(xié)議既可實現(xiàn)不同計算機間的進程通信,也可實現(xiàn)同一計算機內(nèi)部的進程通信。事實上,網(wǎng)絡(luò)協(xié)議是一種最復(fù)雜的進程間通信機制。

信號(Signal)是用于通知事件的一種機制。當(dāng)內(nèi)核有事情需要通知進程時,它可以發(fā)送信號。當(dāng)一個進程有事情需要通知另一個進程時,它也可以發(fā)送信號。信號還是一種原始的進程間同步機制,一個進程可以暫停工作等待被其它進程的信號喚醒。與其它通信機制不同,信號是Linux必備的一種通信機制,是Linux內(nèi)核不可分割的一部分。10.1信號最早的信號機制是由UnixSystemV引入的。BSD4.2解決了信號中的許多問題,BSD4.3又對其做了進一步的加強和改善。POSIX.1定義了一個標(biāo)準(zhǔn)的信號接口,POSIX.4又對其進行了擴充。目前幾乎所有的Unix變種,包括Linux,都提供了和POSIX標(biāo)準(zhǔn)兼容的信號機制。10.1.1信號定義

信號的格式和意義都是預(yù)先約定好的,一個信號表示一種類型的事件。能產(chǎn)生信號的實體稱為信號源,主要的信號源是內(nèi)核和進程。內(nèi)核通過信號將系統(tǒng)內(nèi)發(fā)生的事件通知進程,如進程執(zhí)行了非法操作、進程使用的資源超限、用戶輸入了Ctrl-C、進程啟動的定時器已到期等。一個進程也可以向另一個或一組進程發(fā)送信號,用來通知事件、控制作業(yè)等,如通過向一個進程發(fā)送SIGKILL信號來將其殺死等。

Unix通常用一個無符號長整數(shù)作為位圖來表示信號,其中的一位對應(yīng)一種信號。Linux用一個整數(shù)數(shù)組作為位圖來表示信號,數(shù)量可以更多。缺省情況下,Linux支持的信號有64個,其中1到31為普通信號,32到63為實時信號,第0個信號保留未用。普通信號的意義是固定的,實時信號的意義由用戶自定義,且可傳遞附加信息。表10.1是Linux的普通信號。

表10.1普通信號及其意義Linux系統(tǒng)中的每個進程都可能收到信號,而且可以用不同的方式處理信號。進程對信號的處理方式有下列幾種:

(1)阻塞。將到來的信號記錄下來但不處理,直到阻塞被解除。

(2)忽略(SIG_IGN)。不接收或不處理信號,直接將其丟棄。

(3)缺省(SIG_DFL)。由內(nèi)核按缺省方式處理信號。

(4)自定義。由進程自己注冊的用戶態(tài)信號處理程序處理信號。

Linux內(nèi)核提供了五種缺省的信號處理方式,分別是:

(1)夭折(abort)。把進程虛擬地址空間中的內(nèi)容存入文件core后終止進程。

(2)終止(exit)。直接終止進程,不生成core文件。

(3)忽略(ignore)。忽略或丟棄收到的信號。

(4)停止(stop)。讓進程停止運行。

(5)繼續(xù)(continue)。如果進程已被停止,則讓其恢復(fù)運行;否則忽略信號。

信號必須由接收者進程自己處理,處理方法由接收者進程自己決定。如果信號的接收者當(dāng)前未在運行態(tài),那么它對信號的處理就不會很及時。

另外,信號沒有優(yōu)先級,信號處理的先后順序完全取決于系統(tǒng)的設(shè)計。信號可能被接收者忽略或阻塞,因而接收者可能感覺不到某些信號的到來,也就是說信號可能會丟失。

10.1.2信號管理結(jié)構(gòu)

在早期的版本中,Linux用定義在task_struct結(jié)構(gòu)中的位圖signal記錄進程已收到且未處理的信號,用位圖blocked記錄進程當(dāng)前要阻塞的信號,用sigqueue結(jié)構(gòu)隊列記錄信號的附加信息。當(dāng)位圖signal&(~blocked)不空時,說明進程收到了未被阻塞的信號,應(yīng)該在適當(dāng)?shù)臅r機執(zhí)行一下這些信號的處理程序。進程預(yù)定的信號處理程序記錄在它的signal_struct結(jié)構(gòu)中,其主要內(nèi)容是一個數(shù)組,每一種信號對應(yīng)其中的一項,用于記錄進程預(yù)定的信號處理程序及處理信號時的特殊要求。老的信號管理結(jié)構(gòu)十分直觀,但未照顧到線程組的特殊需求。事實上,線程組中的進程可以作為個體接收信號,也可以作為整體接收信號。作為個體接收的信號只需自己處理即可,但作為整體接收的信號卻可被線程組中的任一進程處理。為了區(qū)分整體與個體信號,新版本的Linux保留了task_struct結(jié)構(gòu)中的blocked位圖,但卻將signal位圖與信號隊列合并到了sigpending結(jié)構(gòu)中。進程作為個體所收到的信號記錄在task_struct結(jié)構(gòu)的pending隊列中,作為整體所收到的信號記錄在signal_struct結(jié)構(gòu)的shared_pending隊列中,一個線程組中的所有進程共享同一個signal_struct結(jié)構(gòu)。進程預(yù)定的信號處理方式被從結(jié)構(gòu)signal_struct中分離出來,形成了獨立的sighand_struct結(jié)構(gòu),其主要內(nèi)容是一個action數(shù)組(數(shù)組長度為64),記錄著進程對各信號的處理程序及處理時的特殊要求。新版本的信號管理結(jié)構(gòu)如圖10.1所示。

圖10.1進程的信號管理結(jié)構(gòu)注意,信號是從1開始編碼的,信號i(i>0)對應(yīng)位圖blocked和signal的第i-1位。

由于signal_struct結(jié)構(gòu)是線程組中所有進程共享的,因而除了可用它記錄收到的信號之外,還可在其中記錄一些組內(nèi)進程共享的其它信息,如:

(1)線程組的各類統(tǒng)計信息,包括:

①累計消耗的用戶態(tài)時間、核心態(tài)時間等。

②進程運行的實際總時間。

③累計產(chǎn)生的主(需讀磁盤)、次(不需讀磁盤)頁故障異常的次數(shù)。④駐留在內(nèi)存中的最大頁數(shù)。

⑤累計產(chǎn)生的輸入輸出量,如讀入的字節(jié)數(shù)、寫出的字節(jié)數(shù)、讀操作的次數(shù)、寫操作的次數(shù)等。

(2)線程組能夠使用的各類資源的上界,包括可消耗的處理器時間、可使用的優(yōu)先級、可創(chuàng)建文件的大小、可使用數(shù)據(jù)區(qū)的大小、可使用堆棧區(qū)的大小、可駐留內(nèi)存的頁數(shù)、可創(chuàng)建的進程數(shù)、可打開的文件數(shù)、可掛起的信號數(shù)、可掛起的消息長度等。

(3)三類時間間隔定時器的管理信息,包括用于實時定時的高精度定時器、三類間隔定時器的定時間隔、虛擬和概略定時器的當(dāng)前值等。

(4)與線程組關(guān)聯(lián)的終端。

(5)用于等待子進程終止的進程等待隊列等。

10.1.3信號處理程序注冊

每個進程的task_struct結(jié)構(gòu)中都有一個指向sighand_struct結(jié)構(gòu)的指針,其中記錄著進程對各種信號的處理方式,主要是各種信號的處理程序。當(dāng)然,多個進程(如同一線程組中的進程)可以共用一個sighand_struct結(jié)構(gòu)。結(jié)構(gòu)sighand_struct的定義如下:

structsigaction{

_sighandler_t sa_handler; //信號處理程序

unsignedlong sa_flags; //信號的特殊處理需求

_sigrestore_t sa_restorer; //善后處理程序

sigset_t sa_mask; //處理信號時的新增阻塞位

};

structk_sigaction{

structsigaction sa;

};

structsighand_struct{

atomic_t count; //引用計數(shù)

structk_sigaction action[_NSIG]; //信號處理程序列表,_NSIG等于64

spinlock_t siglock; //保護用自旋鎖

wait_queue_head_t signalfd_wqh; //等待接收該信號的進程隊列

};第0號進程的sighand_struct結(jié)構(gòu)是靜態(tài)建立的,它的所有信號處理程序都是缺省(SIG_DFL)的。其它進程的sighand_struct結(jié)構(gòu)都是動態(tài)建立的。在創(chuàng)建之初,進程要么與創(chuàng)建者共用同一個sighand_struct結(jié)構(gòu),要么從創(chuàng)建者復(fù)制一個sighand_struct結(jié)構(gòu),因而在創(chuàng)建之初,進程處理信號的方式與創(chuàng)建者進程完全相同。在為進程加載新的執(zhí)行程序之前,加載程序會為進程建立獨立的sighand_struct結(jié)構(gòu),并對其進行清理。在清理后的sighand_struct結(jié)構(gòu)中,用戶自定義的所有處理程序都被換成了缺省的SIG_DFL,所有的sa_mask和sa_flags也都被清空。

運行中的進程可以通過系統(tǒng)調(diào)用(如signal()、sigaction()、rt_sigaction()等)更改自己對各信號的處理方式,包括注冊自己定義的信號處理程序。函數(shù)signal()是較老的一個系統(tǒng)調(diào)用(即將被淘汰),僅能設(shè)置信號處理程序。函數(shù)sigaction()可以設(shè)置一個信號的處理程序及阻塞掩碼、特殊標(biāo)志等,但由于結(jié)構(gòu)的變化,也將被淘汰。函數(shù)rt_sigaction()是目前建議的系統(tǒng)調(diào)用,可用于設(shè)置一個信號處理的所有部分。進程設(shè)置的信號處理程序可以是缺省(SIG_DFL)、忽略(SIG_IGN)或自定義的函數(shù)。如果進程設(shè)置的信號處理程序是一個自定義的用戶空間函數(shù),那么sigaction結(jié)構(gòu)中的sa_handler將指向該函數(shù)。如果進程將某信號的處理程序換成了忽略,那么它此前收到的該種信號會被清除。由于信號SIGCONT、SIGCHLD、SIGWINCH和SIGURG的缺省處理是忽略,因而若進程將這四個信號的處理程序換成了缺省,那么它此前收到的這些信號也會被清除。

SIGKILL和SIGSTOP是內(nèi)核專用的信號,它們的處理方式不能被更改。

系統(tǒng)調(diào)用sigprocmask()專門用于設(shè)置或獲取進程的阻塞掩碼blocked。10.1.4信號發(fā)送

與其它形式的通信機制不同,信號是由發(fā)送者直接送給接收者的,接收者不需要采取任何接收動作。也就是說,信號的發(fā)送和接收都是由發(fā)送者進程負責(zé)的,操作系統(tǒng)只需要提供信號的發(fā)送操作即可。發(fā)送信號的進程必須在運行狀態(tài),但接收信號的進程卻可以在任意狀態(tài)。處于可中斷等待狀態(tài)或停止?fàn)顟B(tài)的進程可能會被收到的信號喚醒。

在早期的Unix系統(tǒng)中,發(fā)送信號實際就是在接收者進程的signal位圖中設(shè)置一個標(biāo)志。信號的接收者只知道收到了某類信號,卻不知道信號的來源和數(shù)量。在Linux的早期實現(xiàn)中,實時信號可以帶一個附加信息,這些附加信息被掛在接收者進程的信號隊列中,因而實時信號有了數(shù)量的概念。新版本的Linux給普通信號也準(zhǔn)備了附加信息,可用于通報信號的來源,但為了與老版本兼容,普通信號仍沒有數(shù)量的概念。

信號的附加信息被包裝在結(jié)構(gòu)sigqueue中,其中內(nèi)嵌的結(jié)構(gòu)siginfo用于描述附加信息本身,主要內(nèi)容如下:

(1)域si_signo中記錄著信號的編碼,從1開始。

(2)域si_code中記錄著信號的來源,如內(nèi)核、用戶、定時器等。

(3)聯(lián)合_sifields中記錄著各信號特定的附加信息,如發(fā)送者的pid、uid等。

信號的接收者可以是一個特定的進程,也可以是一個線程組。作為線程組所收到的信號可以被組中的所有進程看到,也可以被組中的任意一個進程處理,但只能被處理一次。不管被哪個進程處理,作為線程組收到的信號都會影響到組中所有的進程。

發(fā)送信號的進程至少需要提供四個參數(shù),分別是信號編號、附加信息、接收者進程、目標(biāo)類別(線程組或單個進程)等。發(fā)送信號時完成的工作主要有如下幾件:

(1)解決停止與繼續(xù)信號的矛盾問題,防止它們在接收者進程中共存。①如果要發(fā)送的是停止信號(SIGSTOP、SIGTSTP、SIGTTOU、SIGTTIN),則應(yīng)將接收者及其線程組此前已收到的繼續(xù)信號全部清除。

②如果要發(fā)送的是繼續(xù)信號(SIGCONT),則應(yīng)將接收者及其線程組此前已收到的停止信號全部清除,并喚醒接收者線程組中所有停止態(tài)的進程。

(2)丟棄接收者進程要忽略的信號。在接收者進程中,處理程序為SIG_IGN的信號是被忽略的,處理程序為SIG_DFL的SIGCHLD、SIGCONT、SIGURG或SIGWINCH信號也是被忽略的。接收者進程當(dāng)前阻塞的信號不能被忽略。

(3)確定新信號將要進駐的接收隊列。一個信號只能被加入到一個隊列中,信號所在隊列由參數(shù)中的目標(biāo)類別決定,可能是task_struct結(jié)構(gòu)中的pending或signal_struct結(jié)構(gòu)中的shared_pending隊列。

(4)丟棄重復(fù)收到的普通信號。根據(jù)傳統(tǒng)的信號語義,不需要記錄普通信號的接收次數(shù)。因而,如果要發(fā)送的普通信號已在選定的接收隊列中,可將其直接丟棄。

(5)發(fā)送信號。

①向?qū)ο髢?nèi)存管理器申請一個空閑的sigqueue結(jié)構(gòu),用發(fā)送者提供的參數(shù)填寫該結(jié)構(gòu),并將其掛在接收隊列的隊尾。為了防止拒絕服務(wù)類攻擊,Linux限制了一個進程可用的sigqueue結(jié)構(gòu)數(shù)。②如果有等待該信號的進程(signalfd_wqh隊列不空),則將其中的一個喚醒。

③將信號在隊列位圖signal中的標(biāo)志位置1,表示收到了該信號。

(6)善后處理。

①如果接收者是單個進程,在下列情況下,不需要特別的善后處理:

●信號正被接收者阻塞。

●接收者進程正處于停止?fàn)顟B(tài)。

●接收者進程當(dāng)前未在處理器上運行且其上已有待處理的信號。②如果接收者是一個線程組,則在其中選一個滿足下列條件之一的進程用來處理該

信號:

●未阻塞該信號且正在運行的進程。

●未阻塞該信號、不在停止?fàn)顟B(tài)且無其它待處理信號的進程。

如果組中的所有進程都不能滿足上述條件,則不需特別處理。③如果信號(包括SIGKILL)對接收者是致命的(處理動作是終止進程),則向線程組中的所有進程發(fā)送SIGKILL信號,并將處于可中斷等待狀態(tài)或醒后終止?fàn)顟B(tài)的進程全部喚醒,以便讓它們盡快終止。

④如果信號對接收者不是致命的但為其選定的處理者進程處于可中斷等待狀態(tài),則將其喚醒。

大部分信號都是由內(nèi)核發(fā)送給進程或線程組的,只有一小部分信號是在進程之間互相發(fā)送的。系統(tǒng)會對進程之間互相發(fā)送的信號進行嚴(yán)格的權(quán)限檢查。Linux提供了多個系統(tǒng)調(diào)用以便于在進程之間互發(fā)信號,主要有以下幾個:

(1)函數(shù)kill()。在未引入線程組時,該函數(shù)用于向一個進程或進程組發(fā)送信號。在引入線程組之后,該函數(shù)的意義取決于接收者進程的pid:

pid>0,用于向一個線程組發(fā)送信號,pid是接收者線程組的ID號。

pid=0,用于向發(fā)送者進程所在的線程組發(fā)送信號。

pid=-1,用于向當(dāng)前線程組之外的所有其它線程組發(fā)送信號。

pid<-1,用于向進程組中的所有進程發(fā)送信號,-pid是進程組的ID號。

(2)函數(shù)tgkill()用于向一個特定的進程(由TGID和PID標(biāo)識)發(fā)送信號。

(3)函數(shù)tkill()是tgkill()的特例,僅指定了PID而未指定TGID,接收者進程可位于任意一個線程組中。

(4)函數(shù)rt_sigqueueinfo()用于向一個線程組發(fā)送一個帶附加信息的信號。

(5)函數(shù)rt_tgsigqueueinfo()用于向一個特定的進程發(fā)送一個帶附加信息的信號。10.1.5信號處理

進程可以在任何狀態(tài)下接收信號,但只能在從核心態(tài)返回用戶態(tài)之前處理信號,見圖4.3。也就是說,信號是由接收者進程在特定時刻自己處理的,有可能不夠及時。由于內(nèi)核守護線程不會返回用戶態(tài),因而它們不會處理信號。

進程先處理發(fā)給自己的信號(在task_struct結(jié)構(gòu)的pending隊列中),再處理發(fā)給線程組的信號(在signal_struct結(jié)構(gòu)的shared_pending隊列中)。但進程并不按到達的順序處理信號。一般情況下,進程會按編號從小到大的順序處理隊列中未被阻塞的信號。然而有些信號是由進程在執(zhí)行過程中的異常操作引起的,屬于同步信號,如SIGSEGV、SIGBUS、SIGILL、SIGTRAP、SIGFPE等,應(yīng)該優(yōu)先處理。對同一種信號來說,先收到的先被處理。處理過的信號會被從隊列中摘除。當(dāng)隊列中的一種信號被徹底處理完之后,它在位圖signal中的標(biāo)志也會被清除。

進程處理信號的方法由它的sighand_struct結(jié)構(gòu)決定。如果信號的處理程序是忽略(SIG_IGN),那么簡單地將其丟棄即可。如果信號的處理程序是缺省(SIG_DFL),那么由內(nèi)核處理即可。內(nèi)核對不同信號的缺省處理方法也不同,大致可分為如下幾類:

(1)忽略。信號SIGCONT、SIGCHLD、SIGWINCH和SIGURG的缺省處理動作是忽略,即將信號直接丟棄。

(2)停止。信號SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU的缺省處理動作是停止,即將進程所在線程組中的所有進程(包括自己)都設(shè)為停止?fàn)顟B(tài)。

(3)終止。其它信號的缺省處理動作都是終止,即終止進程所在線程組中的所有進程(包括自己)。終止線程組中其它進程的方法是向它們發(fā)送SIGKILL信號,終止自己的方法是執(zhí)行函數(shù)exit()。如果信號的處理程序是用戶自定義的,則需要進入用戶空間執(zhí)行一次這一用戶態(tài)處理程序。由于進程還未返回到用戶空間,按照Intel處理器的約定,不能從高特權(quán)級向低特權(quán)級轉(zhuǎn)移控制,因而不能直接調(diào)用處理程序。但由于進程正處在返回用戶空間的過程中,其返回狀態(tài),包括用戶堆棧的棧頂、用戶空間下一條指令的地址等,都已記錄在進程的系統(tǒng)堆棧中(由pt_regs結(jié)構(gòu)描述,見圖4.2),因而只要將pt_regs結(jié)構(gòu)中的ip換成信號處理程序的入口地址(sighand_struct結(jié)構(gòu)中的sa_handler),而后讓進程正常返回,它就會立刻轉(zhuǎn)去執(zhí)行自己定義的信號處理程序。然而,對ip域的簡單修改破壞了進程的原有返回狀態(tài),使得進程在執(zhí)行完信號處理程序之后無法再返回它在用戶空間的應(yīng)有位置。因而,Linux將自定義信號處理程序的執(zhí)行過程分成了如下六步:

(1)將系統(tǒng)堆棧棧頂?shù)膒t_regs結(jié)構(gòu)暫存起來。

(2)修改進程的用戶堆棧,在其中壓入必須的參數(shù),如信號編號、附加信息等。

(3)將pt_regs結(jié)構(gòu)中的ip改為自定義信號處理程序的入口地址。

(4)返回用戶態(tài),讓進程執(zhí)行自定義的信號處理程序。

(5)在完成處理工作之后,讓信號處理程序通過系統(tǒng)調(diào)用再次進入內(nèi)核。

(6)在內(nèi)核中將進程的系統(tǒng)堆?;謴?fù)到修改之前的狀態(tài),而后讓進程再次返回。

可以將進程系統(tǒng)堆棧棧頂?shù)膒t_regs結(jié)構(gòu)暫存在內(nèi)核中,也可以將其暫存在用戶堆棧中。按照Intel處理器的約定,內(nèi)核程序可以修改用戶空間的數(shù)據(jù),因而可以修改用戶堆棧。Linux將進程的pt_regs結(jié)構(gòu)暫存在用戶堆棧中。

較困難的是讓信號處理程序在完成工作之后再次進入內(nèi)核。強行要求信號處理程序調(diào)用一個特定的函數(shù)不是一個好辦法,因為這會引起程序員的反感,萬一遺忘還會導(dǎo)致嚴(yán)重的進程錯誤。一種解決辦法是在用戶堆棧的棧頂壓入一段善后程序,并通過修改用戶堆棧將信號處理程序的返回地址改成善后程序的入口,由這段善后程序負責(zé)執(zhí)行系統(tǒng)調(diào)用,以再次進入內(nèi)核。Linux壓到用戶堆棧中的善后程序由三條指令組成,如下:

popl%eax //彈出棧頂?shù)膮?shù)

movl$_NR_sigreturn,%eax //將系統(tǒng)調(diào)用號存入EAX寄存器中

int$0x80 //再次進入內(nèi)核,執(zhí)行恢復(fù)程序sys_sigreturn()

然而,在堆棧中壓入代碼是一種非正常的程序執(zhí)行手段。當(dāng)堆棧頁被禁止執(zhí)行時(如Intel64中的NXB被置1),這種手段將無法使用。因而,新版本的Linux將上述善后程序移到了vsyscall頁(見4.4.4節(jié))中。由于vsyscall頁總在進程的虛擬地址空間中,且以共享庫(linux-gate.so)的面目出現(xiàn),對它的調(diào)用總是可行的。既然可以通過修改用戶堆棧讓信號處理程序返回到預(yù)定的善后程序,當(dāng)然也可以讓它返回到正常的返回地址。但由于在執(zhí)行信號處理程序之前需要修改進程的阻塞掩碼blocked(阻塞不希望收到的新信號),因而在執(zhí)行完信號處理程序之后還應(yīng)恢復(fù)阻塞掩碼。阻塞掩碼的修改必須在內(nèi)核中進行,所以再次進入內(nèi)核是必須的。

由此可見,執(zhí)行自定義信號處理程序需要使用到用戶堆棧。如果進程的用戶堆棧出現(xiàn)了問題(如堆棧溢出),上述處理方法必將無法工作。為此,Linux允許進程通過系統(tǒng)調(diào)用sigaltstack()定義自己的替換堆棧,專門用于執(zhí)行用戶自定義的信號處理程序。替換堆棧的位置記錄在task_struct中,開始位置為sas_ss_sp,大小為sas_ss_size。

為了操作方便,Linux將用戶堆棧(或替換堆棧)的棧頂定義成了一個結(jié)構(gòu),稱為rt_sigframe,如圖10.2所示。

進程系統(tǒng)堆棧的棧頂(pt_regs結(jié)構(gòu))被保存在用戶堆棧的uc_mcontext域中,阻塞掩碼被保存在用戶堆棧的uc_sigmask域中。信號處理程序的返回地址被保存在pretcode域中,通常就是vsyscall頁中的善后程序入口地址。信號的附加信息被壓入用戶堆棧的棧頂(info域)。Linux內(nèi)核通過寄存器向信號處理程序傳遞參數(shù),包括EAX中的信號編號、EDX中的附加信息(info域的地址)、ECX中的ucontext結(jié)構(gòu)等。

進程的新阻塞掩碼是老阻塞掩碼與位圖sa_mask的按位或,有時還需要加上當(dāng)前處理的信號。如此以來,在信號處理程序的執(zhí)行過程中,雖然進程可以被中斷,可以收到新的信號,但不會被不希望見到的信號打擾。圖10.2用戶堆棧和系統(tǒng)堆棧的棧頂結(jié)構(gòu)經(jīng)過上述修改之后,返回到用戶態(tài)的進程會自然轉(zhuǎn)入自定義的信號處理程序,且可看到需要的參數(shù)。當(dāng)信號處理程序執(zhí)行完后,最后一條ret指令會將控制轉(zhuǎn)移到vsyscall頁中的善后程序。善后程序通過int$0x80再次進入內(nèi)核,轉(zhuǎn)去執(zhí)行函數(shù)sys_rt_sigreturn()。

函數(shù)sys_rt_sigreturn()用保存在用戶堆棧中的信息恢復(fù)進程的blocked位圖和系統(tǒng)堆棧?;謴?fù)之后,進程系統(tǒng)堆棧中的sp又指向了用戶堆棧的老棧頂(相當(dāng)于彈出了棧頂?shù)膔t_sigframe結(jié)構(gòu)),ip又指向了正常的返回地址。當(dāng)函數(shù)sys_rt_sigreturn()執(zhí)行完畢之后,進程又一次從核心態(tài)返回用戶態(tài),會再次檢查進程上是否有待處理的信號。如果還有待處理的信號,進程會繼續(xù)處理它們。特別地,若信號的處理程序仍然是用戶自定義的,系統(tǒng)會再次修改進程的用戶堆棧、系統(tǒng)堆棧,再次進入用戶空間執(zhí)行自定義信號處理程序,并在處理完后再次進入內(nèi)核完成清理工作。

如果進程上已沒有待處理的信號,則進程會返回到正常的用戶態(tài),從此前被中斷的位置恢復(fù)正常運行。整個信號處理過程附著在中斷處理之后,相當(dāng)于前一次中斷處理或系統(tǒng)調(diào)用的延續(xù)。

如果進程在執(zhí)行自定義信號處理程序的過程中再次進入內(nèi)核(如執(zhí)行系統(tǒng)調(diào)用或被中斷),那么信號處理程序的執(zhí)行就會被打斷。如果進程上還有其它的待處理信號,那么當(dāng)進程從核心態(tài)返回用戶態(tài)時,就會執(zhí)行新的信號處理程序。也就是說信號處理的過程可能是嵌套的。如果不希望發(fā)生這種嵌套現(xiàn)象,應(yīng)在執(zhí)行信號處理程序時阻塞其它的信號,如在sigaction結(jié)構(gòu)的sa_mask位圖中聲明需阻塞的信號等。

值得注意的是Linux對信號SIGCHLD的處理。正常情況下,當(dāng)線程組中最后一個進程終止時,它應(yīng)該將自己的退出狀態(tài)設(shè)為EXIT_ZOMBIE并向父進程發(fā)送SIGCHLD信號(見7.4.1節(jié))。如果父進程為SIGCHLD自定義了處理程序,該信號會被父進程正常處理,如init進程。然而通常情況下,進程為SIGCHLD指定的處理程序都是忽略。按照POSIX約定,進程對SIGCHLD的忽略處理是反復(fù)執(zhí)行函數(shù)wait4(),以回收所有處于僵死態(tài)的子進程。事實上,早期的Linux就是這樣處理SIGCHLD信號的。這種約定雖然有效,但顯得十分古怪(進程對信號SIGCHLD的缺省處理是忽略、忽略處理是回收子進程)。在新版本中,Linux修正了信號SIGCHLD的發(fā)送和處理方法,如下:

(1)如果父進程為SIGCHLD指定了自定義的處理程序,則正常發(fā)送,正常處理。

(2)如果父進程為SIGCHLD指定的處理程序是缺省,則直接將其丟棄。

(3)如果父進程為SIGCHLD指定的處理程序是忽略,則將子進程的退出狀態(tài)改為EXIT_DEAD,并將它的task_struct結(jié)構(gòu)的引用計數(shù)減1。如此以來,調(diào)度程序就會直接將子進程釋放,不再需要麻煩父進程回收了。10.1.6信號接收

正常情況下,信號是異步的。進程不知道什么時候會收到信號,也不知道什么時候會處理信號。異步的信號雖然簡單,但卻讓人覺得難以掌控。為此,新版本的Linux又提供了同步信號處理方式。

如果進程想用同步方式處理自己收到的信號,它可以采用如下方法:

(1)通過系統(tǒng)調(diào)用signalfd()或signalfd4()創(chuàng)建一個文件描述符,并在其中指定想要接收的信號種類(不包括SIGKILL和SIGSTOP)。

(2)通過系統(tǒng)調(diào)用sigprocmask()阻塞想要同步接收的信號。

(3)在需要時,直接通過系統(tǒng)調(diào)用read()讀新建的文件描述符。如果進程已收到了指定的信號,read()會返回信號的描述信息,包括信號的編號和附加信息。如果進程還未收到指定的信號,進程將被掛在sighand_struct結(jié)構(gòu)的signalfd_wgh隊列中等待,直到指定的信號到來。

(4)當(dāng)不再需要同步接收信號時,可通過系統(tǒng)調(diào)用close()關(guān)閉信號的描述符。

進程通過read()操作讀出的信號會自動從進程的信號隊列中刪除。由signalfd()或signalfd4()創(chuàng)建的文件描述符也可用在通用的poll()或select()中,以查詢或監(jiān)視想要同步接收的信號。執(zhí)行poll()或select()的進程會被阻塞,直到收到需要的信號或等待超時。

利用系統(tǒng)調(diào)用sigsuspend()或rt_sigsuspend()也可以實現(xiàn)信號的同步處理。想同步處理信號的進程可以定義一個阻塞位圖,將希望接收信號的阻塞位清0,而后通過函數(shù)sigsuspend()或rt_sigsuspend()將進程的阻塞位圖換成新定義的阻塞位圖,并將自己掛起。如此以來,只有當(dāng)期望的信號到來時,進程才會被喚醒。進程被喚醒后,其上的阻塞位圖會被恢復(fù),進程此前執(zhí)行的函數(shù)sigsuspend()或rt_sigsuspend()會正常返回。

利用系統(tǒng)調(diào)用sigpending()或rt_sigpending()可以查詢進程已收到、未被阻塞、未被處理的信號位圖。

雖然可以帶一個附加信息,但信號所傳遞的信息量畢竟十分有限,因而信號較適合于通知,卻難以勝任大量信息的傳遞。要實現(xiàn)進程之間的大數(shù)據(jù)量通信,還必須提供其它的通信手段,如管道(Pipe)。管道機制最早由AT&T的M.D.Mcllroy提出,于1973年被引入Unix操作系統(tǒng)。管道是一項重要的發(fā)明,它使Unix具有了將小程序組合成大工具的能力,使Unix的優(yōu)雅哲學(xué)(小的就是美的)得以充分體現(xiàn)。10.2管道10.2.1管道的意義

正常情況下,進程的虛擬地址空間是相互獨立的,除內(nèi)核之外,在不同進程的虛擬地址空間中沒有重疊的部分,進程之間沒有自然的通信渠道,無法進行直接通信。然而,若抽象掉程序、數(shù)據(jù)等的實際含義,可以將進程的虛擬地址空間看成一個字節(jié)的容器。只要在兩個容器之間建立一條管道(Pipe),一個進程中的字節(jié)(數(shù)據(jù))就可以自然地流動到另一個進程中,如圖10.3(a)所示。因而,管道是進程間一種最自然的通信方式。

圖10.3基于管道的進程間通信在Linux的Shell命令中,管道由‘|’標(biāo)識。由‘|’鏈接起來的多個命令會創(chuàng)建多個進程(每個命令對應(yīng)一個進程),進程間建立有自然的管道,前一個命令(或進程)的標(biāo)準(zhǔn)輸出會被管道轉(zhuǎn)化為后一個命令(或進程)的標(biāo)準(zhǔn)輸入。如在命令“l(fā)s-l|wc-l”中,ls的輸出(當(dāng)前目錄的內(nèi)容)被管道直接遞交給命令wc,用以統(tǒng)計其中的行數(shù)。

與現(xiàn)實世界中的管道不同,用于通信的管道具有如下特點:

(1)管道是單向的,數(shù)據(jù)只能從入口進程流向出口進程,不能逆向流動。

(2)管道中流動的數(shù)據(jù)是字節(jié)流,沒有結(jié)構(gòu),通信的格式需要雙方自己約定。

(3)管道是可靠的、有序的、先進先出的,流出的數(shù)據(jù)與流入的數(shù)據(jù)完全一致。

(4)管道有容量限制,當(dāng)管道滿時,發(fā)送端無法再發(fā)送字節(jié);當(dāng)管道空時,接收端無法再取出字節(jié)。10.2.2匿名管道

在多年的發(fā)展過程中,人們給出了多種管道實現(xiàn)方法。在兩個進程之間建立Socket鏈接之后可以實現(xiàn)它們之間的通信。同一系統(tǒng)中的兩個進程也可以利用普通文件實現(xiàn)通信(一個進程向文件尾部寫數(shù)據(jù),另一個進程從文件頭部讀數(shù)據(jù))。然而,利用Socket實現(xiàn)的通信需要經(jīng)過網(wǎng)絡(luò)協(xié)議層的轉(zhuǎn)換,其中有許多無謂的打包、拆包操作,開銷較大。利用普通文件進行的通信不夠靈活、速度較慢且浪費外存空間。因而,實用的管道應(yīng)是對Socket或普通文件的簡化。目前的Linux利用偽文件系統(tǒng)(稱為pipefs)實現(xiàn)管道,該文件系統(tǒng)已在系統(tǒng)初始化時注冊并安裝。既然管道僅用作通信,那么管道文件就應(yīng)該是一種臨時文件,沒有必要在外存設(shè)備上真正將其建立起來。如果用一塊內(nèi)存空間來模擬管道文件(如圖10.3(b)所示),那么對它的讀寫操作就可直接在內(nèi)存中進行,從而可大大提升通信速度?;谏鲜隹紤],Linux用內(nèi)存中的臨時文件實現(xiàn)其經(jīng)典的管道。由于這種管道是動態(tài)建立和撤銷的,在文件系統(tǒng)中沒有體現(xiàn),也沒有名稱,故被稱為匿名管道。

匿名管道的建立由系統(tǒng)調(diào)用pipe()或pipe2()完成。兩個系統(tǒng)調(diào)用的功能一樣,只是后者比前者多一個標(biāo)志,可用于設(shè)置管道文件的屬性,如非阻塞讀寫等。函數(shù)pipe()或pipe2()會在pipefs的根目錄中創(chuàng)建一個管道文件,并將其分別按只寫和只讀方式打開,而后返回兩個文件描述符(兩個整數(shù),分別代表兩個打開的文件),其中的第0個描述符表示管道文件的讀端口或出口,只能用于從中讀數(shù)據(jù),第1個描述符表示管道文件的寫端口或入口,只能用于向其中寫數(shù)據(jù)。也就是說,執(zhí)行函數(shù)pipe()或pipe2()的進程會動態(tài)地創(chuàng)建一個管道文件,并得到它的兩個端口的描述符,此后該進程可以用普通的write()操作向入口端寫數(shù)據(jù)并用read()操作從出口端讀數(shù)據(jù),以實現(xiàn)基于管道的通信。當(dāng)然,單個進程的自我通信是沒有意義的。要想利用匿名管道實現(xiàn)雙進程間的通信,就必須將管道的一端交給另一個進程。然而,文件描述符是進程私有的,在一個進程中使用另一個進程的文件描述符是非法的,因而不能直接將管道文件描述符交給另一個進程,除非這兩個進程是父子關(guān)系。如果進程A在執(zhí)行完pipe()或pipe2()之后創(chuàng)建進程B,那么A的整個文件描述符表都會被B繼承,包括其中的兩個管道文件描述符。此后,只要進程A關(guān)閉管道文件的一端,進程B關(guān)閉管道文件的另一端,即可在兩進程之間建立起一個單向的管道,實現(xiàn)父子進程之間的通信。如果進程A在執(zhí)行完pipe()或pipe2()之后創(chuàng)建兩個進程B和C,那么B和C都會繼承A的文件描述符表,包括其中的兩個管道文件描述符。此后,只要進程B關(guān)閉管道文件的一端,進程C關(guān)閉管道文件的另一端,即可在兩進程之間建立起一個單向的管道,實現(xiàn)兄弟進程之間的通信。事實上,利用匿名管道只能實現(xiàn)父子進程或兄弟進程之間的通信,如圖10.4所示。

圖10.4父子進程之間的通信管道為了提高內(nèi)存的利用率,管道所用的內(nèi)存空間也是動態(tài)分配的,每次至少1頁,缺省情況下管道的大小可達16頁,如圖10.5所示。

在圖10.5中,buffers是管道緩沖區(qū)的最大允許頁數(shù),nrbufs是管道中的當(dāng)前頁數(shù),curbuf是位于管道首部的物理頁。物理頁的分配由寫操作負責(zé)。當(dāng)寫操作發(fā)現(xiàn)管道尾部的剩余空間不夠用時,它向物理內(nèi)存管理器申請1個高端頁,并將其鏈接在管道的尾部。當(dāng)讀操作將管道頭部的物理頁讀空后,它直接將其釋放。管道是典型的生產(chǎn)者/消費者問題,其操作具有自然的同步能力。當(dāng)寫操作發(fā)現(xiàn)管道滿時,寫進程被掛在wait隊列上等待,直到被讀進程喚醒。當(dāng)讀操作發(fā)現(xiàn)管道中的內(nèi)容不夠讀時,讀進程被掛在wait隊列上等待,直到被寫進程喚醒。

圖10.5匿名管道的管理結(jié)構(gòu)生產(chǎn)者在發(fā)送完畢之后可以直接通過close()操作關(guān)閉自己一方的管道。當(dāng)讀完管道中的數(shù)據(jù)后,消費者會得到一個EOF(EndofFile)標(biāo)志,此時,它可以關(guān)閉自己一方的管道。當(dāng)兩方都關(guān)閉以后,管道才會被釋放。如果消費者因為某種原因而提前關(guān)閉管道,那么生產(chǎn)者會在寫操作中發(fā)現(xiàn)管道已經(jīng)斷裂(讀方已不存在),會收到一個SIGPIPE信號。生產(chǎn)者可以在SIGPIPE信號的處理程序中關(guān)閉管道。10.2.3命名管道

匿名管道簡單但能力有限,只能用于父子、兄弟進程之間的通信,不能成為一種通用的進程間通信機制,原因是匿名管道“無名”、“無形”,只能被隱式地繼承而不能被顯式地聲明。只有當(dāng)管道“有名”、“有形”時,它才可能被任意兩個進程打開,從而實現(xiàn)任意兩個進程之間的通信。稱這種“有名”、“有形”的管道為命名管道(FIFO)。

命名管道是外存設(shè)備上的一種特殊類型的文件,類型為FIFO,且有一個永久性的名字。與普通文件不同,由于不需要在其中真正保存數(shù)據(jù),因而命名管道僅需要一個管理結(jié)構(gòu)(文件控制塊inode),不需要占用外存設(shè)備的其它存儲空間。只要知道命名管道的名稱,所有進程都可按需要的方式打開命名管道,并通過它實現(xiàn)進程間的通信。

使用命名管道的方法如下:

(1)讀進程按只讀方式打開命名管道文件,獲得命名管道的文件描述符。

(2)寫進程按只寫方式打開命名管道文件,獲得命名管道的文件描述符。

(3)寫進程向管道中寫數(shù)據(jù),讀進程從管道中讀數(shù)據(jù),實現(xiàn)相互通信。

(4)通信完成之后,各自關(guān)閉命名管道文件。

命名管道的管理結(jié)構(gòu)與匿名管道的相同。第一個打開命名管道的進程創(chuàng)建管理結(jié)構(gòu)。通常情況下,執(zhí)行打開操作的進程會被阻塞直到命名管道的另一端也被打開。按讀方式打開的命名管道不能寫,按寫方式打開的命名管道不能讀,但命名管道允許按讀寫方式打開。按讀寫方式打開的命名管道既允許讀又允許寫,是一種雙向的管道。與匿名管道不同,一個命名管道可以被多個進程打開。也就是說,一個命名管道可以同時有多個寫者和多個讀者。寫入命名管道的數(shù)據(jù)被按序保存在緩沖區(qū)中,不區(qū)分寫者;讀者從緩沖區(qū)的頭部按序讀出數(shù)據(jù),也不區(qū)分讀者。數(shù)據(jù)被讀出后即從命名管道中消失。顯然,命名管道比匿名管道功能更強,也更加靈活。

不管是命名管道還是匿名管道,在其中傳遞的都是沒有經(jīng)過任何包裝的裸數(shù)據(jù)。這種通信方式的效率雖然很高,但卻丟失了通信所需要的許多信息,如發(fā)送者、接收者、時間、類型、長度、邊界等。因而管道通信的適用范圍十分有限,有必要提供更符合人類習(xí)慣的進程間通信機制,如消息隊列(MessageQueue)。10.3消息隊列與面向連接的管道通信不同,消息隊列是一種面向消息的、無連接的異步通信機制,更像是基于郵箱(Mailbox)的通信。發(fā)送者將包裝后的消息放到郵箱中,接收者在方便的時候從郵箱中取走自己需要的整條消息。消息的發(fā)送者和接收者不需要同時存在。通過消息隊列,發(fā)送者和接收者之間可以建立起多種形式的通信關(guān)系,如一對一、一對多、多對一、多對多等。

早期的Linux僅實現(xiàn)了符合UnixSystemV規(guī)范的消息隊列,所采用的管理機制與信號量集合相似(見9.6)。新版本的Linux增加了符合POSIX標(biāo)準(zhǔn)的消息隊列,所采用的管理機制與文件系統(tǒng)相同。10.3.1SystemV消息隊列

1970年,為了支持?jǐn)?shù)據(jù)庫和事務(wù)處理,Bell實驗室在自己內(nèi)部的Unix版本中首次引入了三種進程間通信(InterprocessCommunication,IPC)機制,包括消息隊列、信號量集合和共享內(nèi)存。1983年,在SystemV發(fā)布之時,這三種IPC機制被正式集成到了Unix操作系統(tǒng)中,遂被統(tǒng)稱為UnixSystemV的IPC機制。

SystemV的三種IPC機制具有相似的編程接口和使用方法。與信號量集合相似,Linux為它的每個消息隊列都定義了一個證書(結(jié)構(gòu)kern_ipc_perm),用于描述消息隊列的基本屬性,如鍵值(外部名稱)key、內(nèi)部標(biāo)識id、創(chuàng)建者的uid和gid、擁有者的uid和gid、訪問權(quán)限mode、序列號seq、安全證書security等。其中鍵值key是一個整數(shù)或者魔數(shù),是用戶為消息隊列起的名稱。消息隊列由鍵值命名,由id號標(biāo)識。進程使用消息隊列的過程如下:

(1)通過其它途徑(如預(yù)先約定等)獲得消息隊列的鍵值。

(2)打開消息隊列,核對訪問權(quán)限,獲得id號。第一個打開者創(chuàng)建消息隊列。

(3)通過id號對消息隊列進行初始化,而后在其上執(zhí)行發(fā)送、接收操作。

(4)由最后一個使用者釋放并銷毀消息隊列。

消息隊列是動態(tài)創(chuàng)建的。Linux用結(jié)構(gòu)msg_queue描述消息隊列,主要內(nèi)容如下:

(1)證書q_perm是一個kern_ipc_perm結(jié)構(gòu),描述消息隊列的基本屬性。

(2)字節(jié)數(shù)q_cbytes是一個無符號長整數(shù),表示隊列中當(dāng)前的消息總長度。

(3)消息數(shù)q_qnum是一個無符號長整數(shù),表示隊列中當(dāng)前的消息條數(shù)。

(4)容量q_qbytes是一個無符號長整數(shù),表示隊列的最大容量(字節(jié)數(shù))。

(5)隊列q_messages是通用鏈表的表頭,用于組織隊列中的所有消息。

(6)隊列q_receivers是通用鏈表的表頭,用于組織等待從隊列中接收消息的進程。

(7)隊列q_senders是通用鏈表的表頭,用于組織等待向隊列中發(fā)送消息的進程。圖10.6SystemV的消息隊列管理結(jié)構(gòu)發(fā)送到隊列中的消息由正文和消息頭構(gòu)成,消息頭由結(jié)構(gòu)msg_msg描述,如下:

structmsg_msg{

structlist_head m_list; //隊列節(jié)點

long m_type; //消息類型

int m_ts; //消息正文的長度

structmsg_msgseg *next; //消息正文的附加段

void *security; //消息的安全標(biāo)識

};正常情況下,消息正文緊跟著消息頭。如果消息較短(包裝后的長度不超過一頁),整條消息會被集中存放在一塊連續(xù)的內(nèi)存空間中。如果消息較長(包裝后的長度超過一頁),消息正文會被分成幾部分,每一部分的長度都不超過一頁。長消息的第一部分帶著消息頭,其余部分(稱為附加段)的前面帶一個指針,用于將長消息的所有附加段串成一個單向隊列。消息頭中的next是附加段隊列的隊頭。

消息隊列具有同步能力。當(dāng)消息隊列達到或接近容量極限,無法再容納新的消息時,發(fā)送進程被掛在q_senders中等待;當(dāng)消息隊列中沒有需要類型的消息時,接收進程被掛在q_receivers中等待。早期的Linux用一個靜態(tài)的全局指針數(shù)組(長度為128)組織系統(tǒng)中的消息隊列,雖然簡單但不夠靈活。新版本的Linux改用IDR(IDRadix)樹來組織消息隊列,每個IPC名字空間一棵,IDR樹的樹根記錄在IPC名字空間中,如圖10.7所示。消息隊列的ID號id、在IDR樹中的索引號idx與序列號seq(記錄在證書結(jié)構(gòu)kern_ipc_perm中)之間的關(guān)系為id=idx+32768

×

seq,idx=id%32768。

打開消息隊列的操作是msgget(),需要的參數(shù)有兩個,一是消息隊列的鍵值key,二是消息隊列的訪問權(quán)限。打開操作搜索申請者進程的名字空間(實際是它的IDR樹),以確定鍵值為key的消息隊列是否已在其中。如果在,說明該消息隊列已被其它進程創(chuàng)建,只要申請者能通過它的權(quán)限檢查,即可返回它的id號。如果不在,說明該消息隊列還未被建立,需要創(chuàng)建一個新的消息隊列,并返回它的id號。

圖10.7基于IDR樹的消息隊列管理結(jié)構(gòu)創(chuàng)建新消息隊列的工作如下:

(1)在當(dāng)前IDR樹中為新消息隊列找一個空槽位,確定其索引號和id號。

(2)創(chuàng)建一個msg_queue結(jié)構(gòu),設(shè)置其中的key、id、seq、uid、gid等域,并將其插入到IDR樹中。

在獲得消息隊列的id號之后,通過函數(shù)msgctl()可對其進行管理操作,如:

(1)通過操作MSG_INFO獲取當(dāng)前名字空間中消息隊列的狀態(tài)信息,包括允許創(chuàng)建的最大消息隊列數(shù)、單個消息的最大長度(字節(jié))、單個消息隊列的最大容量(字節(jié))、單個消息中的最大附加段數(shù)、當(dāng)前已創(chuàng)建的消息隊列數(shù)等。

(2)通過操作MSG_STAT獲取特定消息隊列的狀態(tài)信息,包括隊列的證書、隊列中的消息數(shù)、隊列中的字節(jié)數(shù)、最近一次發(fā)送時間、最近一次接收時間等。

(3)通過操作IPC_SET設(shè)置特定消息隊列的屬性,包括uid、gid、訪問權(quán)限、隊列的最大容量等。消息隊列有容量限制,雖然容量是可調(diào)的。

(4)通過操作IPC_RMID銷毀特定的消息隊列,包括其中的所有消息,同時喚醒在該消息隊列上等待的所有進程。

向消息隊列發(fā)送消息的操作是msgsnd(),需要的參數(shù)有消息隊列的id號、用戶空間中的消息緩沖區(qū)、消息正文的長度和特殊的發(fā)送要求(如非阻塞發(fā)送)等。每個消息都有一個類型,其含義由發(fā)送者和接收者自己約定(見表10.2)。消息類型與消息正文一起記錄在消息緩沖區(qū)中,類型在前(占四個字節(jié))正文在后。消息發(fā)送的過程如下:

(1)根據(jù)id號找到消息隊列(msg_queue結(jié)構(gòu)),進行必要的權(quán)限檢查。

(2)如果消息隊列已沒有足夠的空間來接納新的消息,且發(fā)送者未聲明非阻塞發(fā)送,則將發(fā)送者進程掛在隊列q_senders中等待,直到接收者為其騰出足夠的空間。

(3)如果可以接收新消息,則將用戶緩沖區(qū)中的消息正文讀入內(nèi)核,將其加上消息頭(msg_msg結(jié)構(gòu))后掛在隊列q_messages中。如果消息正文較長,要將其拆分成附加段。

(4)如果隊列q_receivers中有等待接收消息的進程,且新到來的消息能滿足其中某個等待者的需求,則將該等待進程喚醒。

從消息隊列中接收消息的操作是msgrcv(),需要的參數(shù)有消息隊列的id號、用戶空間中的消息緩沖區(qū)、消息正文的長度、期望接收的消息類型和特殊的接收要求(如非阻塞接收)等。與管道不同,接收者可以通過消息類型指定自己期望接收的消息(不一定是隊列中的第一個消息)。消息類型的含義如表10.2所示。

表10.2消息類型的含義消息接收的過程如下:

(1)根據(jù)id號找到消息隊列(msg_queue結(jié)構(gòu)),進行必要的權(quán)限檢查。

(2)如果消息隊列中沒有滿足要求的消息,且接收者未明確聲明非阻塞接收,則將接收者進程掛在隊列q_receivers中等待,直到被發(fā)送者喚醒。

(3)如果消息隊列中有滿足要求的消息,則將其從隊列中摘下,將其內(nèi)容拷貝到用戶空間的緩沖區(qū)中,并釋放消息所占用的內(nèi)存空間。

(4)如果隊列q_senders中有等待發(fā)送消息的進程,則喚醒其中的第一個等待者。

值得注意的是,隊列中的每條消息都是完整的實體,消息與消息之間有嚴(yán)格的邊界,消息只能被整條發(fā)送和接收,不能僅發(fā)送或接收部分消息。另外,等待發(fā)送或接收消息的進程處于可中斷等待狀態(tài),可被信號喚醒。被信號喚醒的發(fā)送或接收者進程將錯誤返回,表示發(fā)送或接收失敗。

SystemV的消息隊列雖然靈活,但存在著一些問題,如:

(1)與Linux的其它I/O機制不兼容。在Linux中,所有的輸入/輸出都已被統(tǒng)一在虛擬文件系統(tǒng)的框架之內(nèi),但消息隊列是一個例外。雖然消息隊列借用了文件系統(tǒng)的實現(xiàn)思想,但其標(biāo)識方法(鍵值而不是文件名)、使用方法(不是打開、讀寫、關(guān)閉)等都與標(biāo)準(zhǔn)的文件操作不同,需要為其提供專門的管理工具。

(2)難以確定銷毀時機。消息隊列是一種無鏈接的通信機制,不需要通信各方同時存在。也就是說消息隊列可以離開發(fā)送者和接收者進程獨立存在,通過消息隊列可以向未來某個時刻啟動的進程傳遞信息。消息隊列的這一特性使內(nèi)核無法確定其銷毀時機,過早銷毀會丟失隊列中的消息,忘記銷毀則會使隊列及其中的消息成為內(nèi)存垃圾。

事實上,SystemV的三種IPC機制中都存在上述問題。所以有人建議應(yīng)盡量避免使用SystemV的消息隊列,而代之以命名管道或POSIX消息隊列。10.3.2POSIX消息隊列

POSIX的IPC是模仿SystemV的IPC制定的,目的是為了解決SystemV的上述問題。POSIX的IPC也包含三種機制,分別是消息隊列、信號量集合和共享內(nèi)存。POSIX稱IPC機制中的單個個體為對象,如一個POSIX消息隊列被稱為一個消息隊列對象。

與SystemV的IPC不同,POSIX將自己的三種IPC機制全都集成在了虛擬文件系統(tǒng)框架之內(nèi),或者說POSIX將它的IPC對象也都看成是文件,采用與普通文件相同或相似的方式來操作和使用它們。在POSIX中,IPC對象的標(biāo)識方式不再是鍵值而是文件名,如“/myqueue”。在使用IPC對象之前需要用open()操作將其打開并獲得描述符。如果指定名稱的IPC對象不存在,open()操作會為其創(chuàng)建一個新的對象。在獲得IPC對象的描述符后,可以對其進行需要的操作,如在消息隊列對象上進行發(fā)送、接收操作,在信號量對象上進行P、V操作等。用完后的IPC對象需要用close()操作關(guān)閉。

POSIX在每個IPC對象上都關(guān)聯(lián)了一個引用計數(shù),用于記錄該對象被打開的次數(shù)。對象每被打開一次,它的引用計數(shù)就被加1,每被關(guān)閉一次,它的引用計數(shù)就被減1。引用計數(shù)為0的對象可被銷毀。在對象被銷毀之后,新的open()操作會再次創(chuàng)建指定名稱的IPC對象。

為了管理POSIX的消息隊列,Linux為它的每個IPC名字空間都專門建立了一個名為mqueue的偽文件系統(tǒng)(用戶不可見)。與常見的文件系統(tǒng)不同,mqueue中僅有一個根目錄,每個動態(tài)創(chuàng)建的消息隊列對象都是根目錄中的普通文件。在系統(tǒng)初始化時,mqueue已被注冊并安裝,它的安裝結(jié)構(gòu)已被記錄在IPC名字空間中。與SystemV的消息隊列不同,POSIX的消息隊列具有雙重身份。在mqueue文件系統(tǒng)中,消息隊列是普通文件,其描述結(jié)構(gòu)是inode和目錄項(dentry結(jié)構(gòu));在IPC中,消息隊列就是消息隊列,其描述結(jié)構(gòu)中定義有消息的等待隊列和進程的等待隊列。因而POSIX的消息隊列描述結(jié)構(gòu)是兩種身份的組合。Linux為POSIX消息隊列所定義的管理結(jié)構(gòu)是mqueue_inode_info,如圖10.8所示。

圖10.8POSIX單個消息隊列的管理結(jié)構(gòu)

POSIX消息隊列的隊列屬性由域attr描述,其中的主要內(nèi)容包括mq_maxmsg(消息隊列容量,即最多可容納的消息數(shù))、mq_msgsize(單個消息的最大長度,單位為字節(jié))、mq_

curmsgs(隊列中當(dāng)前的消息數(shù))和mq_flags(隊列的操作標(biāo)志,如是否允許非阻塞發(fā)送和接收等)。消息隊列的容量是在創(chuàng)建時指定的,在使用過程中不可再改變。

在POSIX消息隊列中,用于暫存消息的不是一個鏈表而是一個指針數(shù)組messages,其大小與隊列屬性中的最大消息數(shù)mq_maxmsg相等。與SystemV的消息隊列相同,POSIX的消息也被包裝在結(jié)構(gòu)msg_msg中,但消息類型m_type被重新解釋成了優(yōu)先級。在數(shù)組messages中的消息按優(yōu)先級排序,優(yōu)先級低的在前,優(yōu)先級高的在后。messages[0]所指消息的優(yōu)先級最低,messages[mq_curmsgs-1]所指消息的優(yōu)先級最高。進程接收的總是優(yōu)先級最高的消息。

如果消息隊列已滿,發(fā)送者應(yīng)該等待,e_wait_q[0]是發(fā)送者進程等待隊列;如果消息隊列已空,接收者應(yīng)該等待,e_wait_q[1]是接收者進程等待隊列。與SystemV的等待隊列不同,POSIX的等待隊列是有序的,高優(yōu)先級的進程在前,低優(yōu)先級的進程在后。與SystemV的消息隊列不同,POSIX允許為消息隊列選擇一種通知方式,如信號,以支持異步接收。當(dāng)空消息隊列首次收到新消息時,如果e_wait_q[1]中沒有等待的接收者,系統(tǒng)將向預(yù)定消息的接收者發(fā)送一個信號。信號的編號記錄在mqueue_inode_info結(jié)構(gòu)的notify域中,信號的接收者記錄在notify_owner域中。進程可以通過系統(tǒng)調(diào)用mq_notify()預(yù)定消息。

POSIX消息隊列的文件屬性由域vfs_inode描述,其主要內(nèi)容包括訪問權(quán)限i_mode、inode_operations操作集、file_operations操作集等。每個inode結(jié)構(gòu)都關(guān)聯(lián)著一個目錄項結(jié)構(gòu)dentry,其中記錄著消息隊列的名稱和組織關(guān)系,如父、子關(guān)系等。系統(tǒng)中所有的目錄項,包括消息隊列目錄項,都被組織在一個名為dentry_hashtable的全局Hash表中。消息隊列目錄項的Hash值是根據(jù)隊列的名稱和根目錄的地址算出的。給出一個消息隊列名,查IPC名字空間可獲得mqueue文件系統(tǒng)的根目錄,查表dentry_hashtable可找到消息隊列的dentry結(jié)構(gòu)和與之關(guān)聯(lián)的inode結(jié)構(gòu),進而可獲得該消息隊列的管理結(jié)構(gòu)mqueue_inode_info。圖10.9是IPC名字空間中消息隊列的組織結(jié)構(gòu)。

圖10.9IPC名字空間中消息隊列的組織結(jié)構(gòu)當(dāng)進程打開消息隊列時,Linux會根據(jù)名稱查Hash表dentry_hashtable。如果指定名稱的消息隊列不在該Hash表中,則要為其創(chuàng)建一個新對象。進程在創(chuàng)建新消息隊列時需要指定訪問權(quán)限和屬性。新消息隊列的創(chuàng)建過程如下:

(1)創(chuàng)建一個dentry結(jié)構(gòu),將其d_name設(shè)為新消息隊列的名稱。

(2)將新建的dentry插入到mqueue文件系統(tǒng)的根目錄(作為根目錄中的普通文件)和Hash表dentry_hashtable中。

(3)創(chuàng)建一個mqueue_inode_info結(jié)構(gòu)(包括messages數(shù)組)并對其進行初始化,包括設(shè)置其中的vfs_inode部分(i_mode、i_uid、i_fop等)和屬性部分attr,而后將新建的dentry與vfs_inode關(guān)聯(lián)起來。

(4)創(chuàng)建一個file結(jié)構(gòu),設(shè)置它的f_mode、f_path、f_pos、f_op等,并將其插入到進程的文件描述符表中。結(jié)構(gòu)file在文件描述符表中的索引就是新消息隊列的描述符。如果進程要打開的消息隊列在表dentry_hashtable中,說明它已被其它進程創(chuàng)建,其dentry和mqueue_inode_info結(jié)構(gòu)已經(jīng)存在。此時的打開操作僅需完成兩件事,一是核對進程的訪問權(quán)限,二是創(chuàng)建一個新的file結(jié)構(gòu)并將其插入到進程的文件描述符表中。

在獲得消息隊列的描述符之后,可以向其發(fā)送消息。向消息隊列發(fā)送消息的操作是mq_timedsend(),需要的參數(shù)有消息隊列的描述符、用戶空間的消息緩沖區(qū)、消息正文的長度、消息的優(yōu)先級、最長等待時間等。消息的發(fā)送過程如下:

(1)用消息隊列的描述符查進程的描述符表,找到與之對應(yīng)的file結(jié)構(gòu),進而得到它的dentry結(jié)構(gòu)、inode結(jié)構(gòu)和mqueue_inode_info結(jié)構(gòu),進行必要的權(quán)限檢查。

(2)將用戶空間的消息拷貝到內(nèi)核,將其包裝在msg_msg結(jié)構(gòu)中,將其類型m_type設(shè)為消息的優(yōu)先級。

(3)如果隊列已滿且不允許非阻塞發(fā)送,則啟動一個高精度定時器,而后按優(yōu)先級從大到小的順序?qū)l(fā)送者進程掛在e_wait_q[0]中等待。進程醒來的原因有三:

①定時器到期,說明一直沒有接收者讀取消息,發(fā)送失敗。②收到信號,說明進程遇到了需要緊急處理的事件,發(fā)送失敗。

③隊列中出現(xiàn)空缺,說明已有消息被用戶取走,可以再次嘗試發(fā)送。

(4)如果隊列未滿或已出現(xiàn)空缺,則將消息按優(yōu)先級從小到大的順序插入到數(shù)組messages中。

(5)如果隊列上有等待的接收者,則喚醒e_wait_q[1]中的第一個進程(優(yōu)先級最高)。如果沒有等待的接收者,且隊列中沒有其它的消息,則通知預(yù)定的接收者。從消息隊列中接收消息的操作是mq_timedreceive(),需要的參數(shù)有消息隊列的描述符、用戶空間的消息緩沖區(qū)、緩沖區(qū)的長度、消息的優(yōu)先級、最長等待時間等。消息的接收過程如下:

(1)用消息隊列的描述符查進程的描述符表,找到與之對應(yīng)的file結(jié)構(gòu),進而得到它的dentry結(jié)構(gòu)、inode結(jié)構(gòu)和mqueue_inode_info結(jié)構(gòu),進行必要的權(quán)限檢查。

(2)如果隊列已空且不允許非阻塞接收,則啟動一個高精度定時器,而后按優(yōu)先級從大到小的順序?qū)⒔邮照哌M程掛在e_wait_q[1]中等待。進程醒來的原因有三:①定時器到期,說明一直沒有消息到來,接收失敗。

②收到信號,說明進程遇到了需要緊急處理的事件,接收失敗。

③隊列中出現(xiàn)新消息,可以再次嘗試接收。

(3)如果隊列非空,則將優(yōu)先級最高的消息從messages數(shù)組中摘下,將其內(nèi)容和優(yōu)先級拷貝到用戶空間的消息緩沖區(qū)中,而后釋放消息所占用的內(nèi)核內(nèi)存空間。

(4)如果有等待發(fā)送的進程,則喚醒e_wait_q[0]中的第一個等待者。當(dāng)不再需要向消息隊列發(fā)送消息或從消息隊列接收消息時,進程可以將打開的消息隊列關(guān)閉。關(guān)閉操作是close(),所需的參數(shù)是消息隊列的描述符,所完成的工作是釋放描述符所對應(yīng)的file結(jié)構(gòu)。與普通文件的關(guān)閉操作一樣,消息隊列的關(guān)閉操作也不會將隊列對象銷毀。

徹底銷毀消息隊列的操作是mq_unlink(),需要提供的參數(shù)是消息隊列的名稱。操作mq_unlink()所完成的工作與創(chuàng)建相反,包括:

(1)根據(jù)名稱查Hash表dentry_hashtable找到與之對應(yīng)的dentry并進行權(quán)限檢查。

(2)遞減dentry中的引用計數(shù)。如果引用計數(shù)大于0,說明該消息隊列還有用戶,不能將其銷毀。如果引用計數(shù)為0,則銷毀消息隊列:

①釋放隊列中的所有消息。

②釋放隊列的mqueue_inode_info結(jié)構(gòu),包括其中的massages數(shù)組。

③將dentry結(jié)構(gòu)從dentry_hashtable表和目錄樹中刪除并釋放。

由此可見,POSIX消息隊列本身并未定義引用計數(shù),它使用的引用計數(shù)實際是從目錄項結(jié)構(gòu)dentry中借用的。

雖然用管道和消息隊列可以實現(xiàn)進程之間的通信,但它們的通信代價都比較高。在通信時,發(fā)送方需要將數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間,接收方需要將數(shù)據(jù)從內(nèi)核空間再拷貝回用戶空間。數(shù)據(jù)的來回拷貝既浪費了內(nèi)存又浪費了時間,應(yīng)該盡量避免。10.4共享內(nèi)存事實上,如果能在兩個進程之間建立一塊共用的物理內(nèi)存空間,那么它們之間就可以直接交換信息。一個進程對共用內(nèi)存空間的修改可以立刻被另一個進程看到,數(shù)據(jù)不需要在進程之間來回拷貝,參與通信的進程不需要執(zhí)行專門的發(fā)送和接收操作,通信過程也不需要內(nèi)核介入,因而可極大地提升通信的速度。這種利用共用物理內(nèi)存空間實現(xiàn)的進程間通信稱為共享內(nèi)存(sharedmemory),是一種最快速的通信方式。共享內(nèi)存的問題是它可能被多個進程同時訪問。為了保證通信的可靠性,需要其它手段來實現(xiàn)進程間的互斥與同步。當(dāng)然,互斥與同步操作會降低通信的速度。由于正常情況下的進程之間沒有共享的虛擬內(nèi)存,因而共享內(nèi)存的建立需要操作系統(tǒng)內(nèi)核的支持。Linux提供了多種建立共享內(nèi)存的系統(tǒng)調(diào)用,如共享文件映射、SystemV方式的共享內(nèi)存、POSIX方式的共享內(nèi)存等。10.4.1共享文件映射

文件映射的作用是建立虛擬內(nèi)存區(qū)域,即在文件區(qū)間與進程虛擬地址空間之間建立映射關(guān)系。映射之后的文件可以直接訪問,就像它們已被讀入內(nèi)存一樣,不需要再執(zhí)行專門的read()、write()操作。若進程訪問的內(nèi)容未被讀入內(nèi)存,處理器會產(chǎn)生頁故障異常,虛擬內(nèi)存管理器會找到與之對應(yīng)的文件頁并將其讀入。因此,文件映射是進程使用文件的一種極為簡潔的方法。如果將一個文件區(qū)間同時映射到多個進程的虛擬地址空間中(在每個進程中建立一個虛擬內(nèi)存區(qū)域),那么該文件區(qū)間就會變成進程之間的公共內(nèi)存區(qū)間。也就是說,可以利用文件映射在進程之間建立共享內(nèi)存??蓤?zhí)行文件的加載操作(exec類操作)是一種隱式的文件映射操作,mmap()是一種顯式的文件映射操作。加載操作建立的是私有映射(對應(yīng)的虛擬內(nèi)存區(qū)域稱為私有區(qū)域),且一次會建立多個虛擬內(nèi)存區(qū)域。mmap()操作既可建立私有映射也可建立共享映射(對應(yīng)的虛擬內(nèi)存區(qū)域稱為共享區(qū)域),且一次僅會建立一個虛擬內(nèi)存區(qū)域。

如果進程僅在私有區(qū)域上執(zhí)行讀操作,那么系統(tǒng)僅會在物理內(nèi)存中保留一份文件內(nèi)容。用于保存文件內(nèi)容的物理頁出現(xiàn)在多個進程的頁表中,是它們的共享內(nèi)存區(qū)間。如果進程在私有區(qū)域上執(zhí)行了寫操作,虛擬內(nèi)存管理器會立刻為其建立一個私有的拷貝。私有拷貝不再是進程之間的共享區(qū)間,一個進程寫入其中的內(nèi)容無法被其它進程看到,也不會被寫入映射文件,因而利用私有映射無法實現(xiàn)進程之間的通信。與私有區(qū)域不同,不管進程在共享區(qū)域上執(zhí)行讀操作還是寫操作,虛擬內(nèi)存管理器僅會在物理內(nèi)存中保留一份文件內(nèi)容,用于保存文件內(nèi)容的物理頁可能被共享它的所有進程訪問。按共享方式映射同一文件區(qū)間的所有進程都可對其進行讀寫操作,一個進程寫入的內(nèi)容可以立刻被其它進程看到,進程對共享區(qū)間的修改結(jié)果也會被寫回映射文件。因而,利用共享文件映射可以實現(xiàn)進程之間的通信,如圖10.10所示。

圖10.10通過共享文件映射實現(xiàn)的共享內(nèi)存在圖10.10中,文件中的兩頁按共享方式被同時映射到進程1和進程2的虛擬地址空間中,這兩頁的內(nèi)容在物理內(nèi)存中僅有一個拷貝,進程1和進程2都可對這兩個物理頁進行讀寫操作,一個進程對它的修改可以立刻被另一個進程看到,修改結(jié)果也會被寫回映射文件。進程1和進程2通過共享文件映射建立起了共享內(nèi)存。

用于建立文件映射的操作是mmap(),該函數(shù)需要6個參數(shù),分別是文件描述符fd、區(qū)間在文件中的開始位置offset、區(qū)間的長度length、虛擬內(nèi)存區(qū)域的開始位置addr、映射方式flags(私有、共享等)和虛擬內(nèi)存區(qū)域的訪問權(quán)限prot(讀、

寫)。

如果申請者未指定參數(shù)addr,虛擬內(nèi)存管理器將為其選擇一個適當(dāng)?shù)挠成湮恢?。即使申請?/p>

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論