版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
第十章進(jìn)?程?間?通?信10.1信號(hào)10.2管道10.3消息隊(duì)列10.4共享內(nèi)存利用鎖、信號(hào)量、RCU等可以協(xié)調(diào)進(jìn)程的動(dòng)作,實(shí)現(xiàn)進(jìn)程間的互斥與同步,但所傳遞的信息量極少,難以用于進(jìn)程間通信。為了使相互協(xié)作的進(jìn)程能夠更好地工作,除了互斥與同步之外,還需要提供一些進(jìn)程間的通信機(jī)制,如通知機(jī)制、信息傳遞機(jī)制、信息共享機(jī)制等。
早期的Unix僅提供了兩種進(jìn)程間通信機(jī)制,分別是用于通知的信號(hào)和用于傳遞數(shù)據(jù)的管道。1983年,AT&T在UnixSystemV中引入了三種新的進(jìn)程間通信機(jī)制,分別是信息隊(duì)列、共享內(nèi)存和信號(hào)量集合。在隨后的發(fā)展中,SystemV的三種通信機(jī)制被吸收進(jìn)POSIX標(biāo)準(zhǔn),但兩者使用了完全不同的API,因而具有完全不同的實(shí)現(xiàn)方式。
Linux繼承了Unix的傳統(tǒng),提供了多種進(jìn)程間的通信機(jī)制,如用于通知的信號(hào),用于交換信息的管道和消息隊(duì)列,用于共享信息的共享內(nèi)存等。在所有的通信手段中,信號(hào)是最基本的,管道與消息隊(duì)列是最直觀的,共享內(nèi)存是最快速的。當(dāng)然,通過(guò)網(wǎng)絡(luò)協(xié)議既可實(shí)現(xiàn)不同計(jì)算機(jī)間的進(jìn)程通信,也可實(shí)現(xiàn)同一計(jì)算機(jī)內(nèi)部的進(jìn)程通信。事實(shí)上,網(wǎng)絡(luò)協(xié)議是一種最復(fù)雜的進(jìn)程間通信機(jī)制。
信號(hào)(Signal)是用于通知事件的一種機(jī)制。當(dāng)內(nèi)核有事情需要通知進(jìn)程時(shí),它可以發(fā)送信號(hào)。當(dāng)一個(gè)進(jìn)程有事情需要通知另一個(gè)進(jìn)程時(shí),它也可以發(fā)送信號(hào)。信號(hào)還是一種原始的進(jìn)程間同步機(jī)制,一個(gè)進(jìn)程可以暫停工作等待被其它進(jìn)程的信號(hào)喚醒。與其它通信機(jī)制不同,信號(hào)是Linux必備的一種通信機(jī)制,是Linux內(nèi)核不可分割的一部分。10.1信號(hào)最早的信號(hào)機(jī)制是由UnixSystemV引入的。BSD4.2解決了信號(hào)中的許多問(wèn)題,BSD4.3又對(duì)其做了進(jìn)一步的加強(qiáng)和改善。POSIX.1定義了一個(gè)標(biāo)準(zhǔn)的信號(hào)接口,POSIX.4又對(duì)其進(jìn)行了擴(kuò)充。目前幾乎所有的Unix變種,包括Linux,都提供了和POSIX標(biāo)準(zhǔn)兼容的信號(hào)機(jī)制。10.1.1信號(hào)定義
信號(hào)的格式和意義都是預(yù)先約定好的,一個(gè)信號(hào)表示一種類型的事件。能產(chǎn)生信號(hào)的實(shí)體稱為信號(hào)源,主要的信號(hào)源是內(nèi)核和進(jìn)程。內(nèi)核通過(guò)信號(hào)將系統(tǒng)內(nèi)發(fā)生的事件通知進(jìn)程,如進(jìn)程執(zhí)行了非法操作、進(jìn)程使用的資源超限、用戶輸入了Ctrl-C、進(jìn)程啟動(dòng)的定時(shí)器已到期等。一個(gè)進(jìn)程也可以向另一個(gè)或一組進(jìn)程發(fā)送信號(hào),用來(lái)通知事件、控制作業(yè)等,如通過(guò)向一個(gè)進(jìn)程發(fā)送SIGKILL信號(hào)來(lái)將其殺死等。
Unix通常用一個(gè)無(wú)符號(hào)長(zhǎng)整數(shù)作為位圖來(lái)表示信號(hào),其中的一位對(duì)應(yīng)一種信號(hào)。Linux用一個(gè)整數(shù)數(shù)組作為位圖來(lái)表示信號(hào),數(shù)量可以更多。缺省情況下,Linux支持的信號(hào)有64個(gè),其中1到31為普通信號(hào),32到63為實(shí)時(shí)信號(hào),第0個(gè)信號(hào)保留未用。普通信號(hào)的意義是固定的,實(shí)時(shí)信號(hào)的意義由用戶自定義,且可傳遞附加信息。表10.1是Linux的普通信號(hào)。
表10.1普通信號(hào)及其意義Linux系統(tǒng)中的每個(gè)進(jìn)程都可能收到信號(hào),而且可以用不同的方式處理信號(hào)。進(jìn)程對(duì)信號(hào)的處理方式有下列幾種:
(1)阻塞。將到來(lái)的信號(hào)記錄下來(lái)但不處理,直到阻塞被解除。
(2)忽略(SIG_IGN)。不接收或不處理信號(hào),直接將其丟棄。
(3)缺省(SIG_DFL)。由內(nèi)核按缺省方式處理信號(hào)。
(4)自定義。由進(jìn)程自己注冊(cè)的用戶態(tài)信號(hào)處理程序處理信號(hào)。
Linux內(nèi)核提供了五種缺省的信號(hào)處理方式,分別是:
(1)夭折(abort)。把進(jìn)程虛擬地址空間中的內(nèi)容存入文件core后終止進(jìn)程。
(2)終止(exit)。直接終止進(jìn)程,不生成core文件。
(3)忽略(ignore)。忽略或丟棄收到的信號(hào)。
(4)停止(stop)。讓進(jìn)程停止運(yùn)行。
(5)繼續(xù)(continue)。如果進(jìn)程已被停止,則讓其恢復(fù)運(yùn)行;否則忽略信號(hào)。
信號(hào)必須由接收者進(jìn)程自己處理,處理方法由接收者進(jìn)程自己決定。如果信號(hào)的接收者當(dāng)前未在運(yùn)行態(tài),那么它對(duì)信號(hào)的處理就不會(huì)很及時(shí)。
另外,信號(hào)沒有優(yōu)先級(jí),信號(hào)處理的先后順序完全取決于系統(tǒng)的設(shè)計(jì)。信號(hào)可能被接收者忽略或阻塞,因而接收者可能感覺不到某些信號(hào)的到來(lái),也就是說(shuō)信號(hào)可能會(huì)丟失。
10.1.2信號(hào)管理結(jié)構(gòu)
在早期的版本中,Linux用定義在task_struct結(jié)構(gòu)中的位圖signal記錄進(jìn)程已收到且未處理的信號(hào),用位圖blocked記錄進(jìn)程當(dāng)前要阻塞的信號(hào),用sigqueue結(jié)構(gòu)隊(duì)列記錄信號(hào)的附加信息。當(dāng)位圖signal&(~blocked)不空時(shí),說(shuō)明進(jìn)程收到了未被阻塞的信號(hào),應(yīng)該在適當(dāng)?shù)臅r(shí)機(jī)執(zhí)行一下這些信號(hào)的處理程序。進(jìn)程預(yù)定的信號(hào)處理程序記錄在它的signal_struct結(jié)構(gòu)中,其主要內(nèi)容是一個(gè)數(shù)組,每一種信號(hào)對(duì)應(yīng)其中的一項(xiàng),用于記錄進(jìn)程預(yù)定的信號(hào)處理程序及處理信號(hào)時(shí)的特殊要求。老的信號(hào)管理結(jié)構(gòu)十分直觀,但未照顧到線程組的特殊需求。事實(shí)上,線程組中的進(jìn)程可以作為個(gè)體接收信號(hào),也可以作為整體接收信號(hào)。作為個(gè)體接收的信號(hào)只需自己處理即可,但作為整體接收的信號(hào)卻可被線程組中的任一進(jìn)程處理。為了區(qū)分整體與個(gè)體信號(hào),新版本的Linux保留了task_struct結(jié)構(gòu)中的blocked位圖,但卻將signal位圖與信號(hào)隊(duì)列合并到了sigpending結(jié)構(gòu)中。進(jìn)程作為個(gè)體所收到的信號(hào)記錄在task_struct結(jié)構(gòu)的pending隊(duì)列中,作為整體所收到的信號(hào)記錄在signal_struct結(jié)構(gòu)的shared_pending隊(duì)列中,一個(gè)線程組中的所有進(jìn)程共享同一個(gè)signal_struct結(jié)構(gòu)。進(jìn)程預(yù)定的信號(hào)處理方式被從結(jié)構(gòu)signal_struct中分離出來(lái),形成了獨(dú)立的sighand_struct結(jié)構(gòu),其主要內(nèi)容是一個(gè)action數(shù)組(數(shù)組長(zhǎng)度為64),記錄著進(jìn)程對(duì)各信號(hào)的處理程序及處理時(shí)的特殊要求。新版本的信號(hào)管理結(jié)構(gòu)如圖10.1所示。
圖10.1進(jìn)程的信號(hào)管理結(jié)構(gòu)注意,信號(hào)是從1開始編碼的,信號(hào)i(i>0)對(duì)應(yīng)位圖blocked和signal的第i-1位。
由于signal_struct結(jié)構(gòu)是線程組中所有進(jìn)程共享的,因而除了可用它記錄收到的信號(hào)之外,還可在其中記錄一些組內(nèi)進(jìn)程共享的其它信息,如:
(1)線程組的各類統(tǒng)計(jì)信息,包括:
①累計(jì)消耗的用戶態(tài)時(shí)間、核心態(tài)時(shí)間等。
②進(jìn)程運(yùn)行的實(shí)際總時(shí)間。
③累計(jì)產(chǎn)生的主(需讀磁盤)、次(不需讀磁盤)頁(yè)故障異常的次數(shù)。④駐留在內(nèi)存中的最大頁(yè)數(shù)。
⑤累計(jì)產(chǎn)生的輸入輸出量,如讀入的字節(jié)數(shù)、寫出的字節(jié)數(shù)、讀操作的次數(shù)、寫操作的次數(shù)等。
(2)線程組能夠使用的各類資源的上界,包括可消耗的處理器時(shí)間、可使用的優(yōu)先級(jí)、可創(chuàng)建文件的大小、可使用數(shù)據(jù)區(qū)的大小、可使用堆棧區(qū)的大小、可駐留內(nèi)存的頁(yè)數(shù)、可創(chuàng)建的進(jìn)程數(shù)、可打開的文件數(shù)、可掛起的信號(hào)數(shù)、可掛起的消息長(zhǎng)度等。
(3)三類時(shí)間間隔定時(shí)器的管理信息,包括用于實(shí)時(shí)定時(shí)的高精度定時(shí)器、三類間隔定時(shí)器的定時(shí)間隔、虛擬和概略定時(shí)器的當(dāng)前值等。
(4)與線程組關(guān)聯(lián)的終端。
(5)用于等待子進(jìn)程終止的進(jìn)程等待隊(duì)列等。
10.1.3信號(hào)處理程序注冊(cè)
每個(gè)進(jìn)程的task_struct結(jié)構(gòu)中都有一個(gè)指向sighand_struct結(jié)構(gòu)的指針,其中記錄著進(jìn)程對(duì)各種信號(hào)的處理方式,主要是各種信號(hào)的處理程序。當(dāng)然,多個(gè)進(jìn)程(如同一線程組中的進(jìn)程)可以共用一個(gè)sighand_struct結(jié)構(gòu)。結(jié)構(gòu)sighand_struct的定義如下:
structsigaction{
_sighandler_t sa_handler; //信號(hào)處理程序
unsignedlong sa_flags; //信號(hào)的特殊處理需求
_sigrestore_t sa_restorer; //善后處理程序
sigset_t sa_mask; //處理信號(hào)時(shí)的新增阻塞位
};
structk_sigaction{
structsigaction sa;
};
structsighand_struct{
atomic_t count; //引用計(jì)數(shù)
structk_sigaction action[_NSIG]; //信號(hào)處理程序列表,_NSIG等于64
spinlock_t siglock; //保護(hù)用自旋鎖
wait_queue_head_t signalfd_wqh; //等待接收該信號(hào)的進(jìn)程隊(duì)列
};第0號(hào)進(jìn)程的sighand_struct結(jié)構(gòu)是靜態(tài)建立的,它的所有信號(hào)處理程序都是缺省(SIG_DFL)的。其它進(jìn)程的sighand_struct結(jié)構(gòu)都是動(dòng)態(tài)建立的。在創(chuàng)建之初,進(jìn)程要么與創(chuàng)建者共用同一個(gè)sighand_struct結(jié)構(gòu),要么從創(chuàng)建者復(fù)制一個(gè)sighand_struct結(jié)構(gòu),因而在創(chuàng)建之初,進(jìn)程處理信號(hào)的方式與創(chuàng)建者進(jìn)程完全相同。在為進(jìn)程加載新的執(zhí)行程序之前,加載程序會(huì)為進(jìn)程建立獨(dú)立的sighand_struct結(jié)構(gòu),并對(duì)其進(jìn)行清理。在清理后的sighand_struct結(jié)構(gòu)中,用戶自定義的所有處理程序都被換成了缺省的SIG_DFL,所有的sa_mask和sa_flags也都被清空。
運(yùn)行中的進(jìn)程可以通過(guò)系統(tǒng)調(diào)用(如signal()、sigaction()、rt_sigaction()等)更改自己對(duì)各信號(hào)的處理方式,包括注冊(cè)自己定義的信號(hào)處理程序。函數(shù)signal()是較老的一個(gè)系統(tǒng)調(diào)用(即將被淘汰),僅能設(shè)置信號(hào)處理程序。函數(shù)sigaction()可以設(shè)置一個(gè)信號(hào)的處理程序及阻塞掩碼、特殊標(biāo)志等,但由于結(jié)構(gòu)的變化,也將被淘汰。函數(shù)rt_sigaction()是目前建議的系統(tǒng)調(diào)用,可用于設(shè)置一個(gè)信號(hào)處理的所有部分。進(jìn)程設(shè)置的信號(hào)處理程序可以是缺省(SIG_DFL)、忽略(SIG_IGN)或自定義的函數(shù)。如果進(jìn)程設(shè)置的信號(hào)處理程序是一個(gè)自定義的用戶空間函數(shù),那么sigaction結(jié)構(gòu)中的sa_handler將指向該函數(shù)。如果進(jìn)程將某信號(hào)的處理程序換成了忽略,那么它此前收到的該種信號(hào)會(huì)被清除。由于信號(hào)SIGCONT、SIGCHLD、SIGWINCH和SIGURG的缺省處理是忽略,因而若進(jìn)程將這四個(gè)信號(hào)的處理程序換成了缺省,那么它此前收到的這些信號(hào)也會(huì)被清除。
SIGKILL和SIGSTOP是內(nèi)核專用的信號(hào),它們的處理方式不能被更改。
系統(tǒng)調(diào)用sigprocmask()專門用于設(shè)置或獲取進(jìn)程的阻塞掩碼blocked。10.1.4信號(hào)發(fā)送
與其它形式的通信機(jī)制不同,信號(hào)是由發(fā)送者直接送給接收者的,接收者不需要采取任何接收動(dòng)作。也就是說(shuō),信號(hào)的發(fā)送和接收都是由發(fā)送者進(jìn)程負(fù)責(zé)的,操作系統(tǒng)只需要提供信號(hào)的發(fā)送操作即可。發(fā)送信號(hào)的進(jìn)程必須在運(yùn)行狀態(tài),但接收信號(hào)的進(jìn)程卻可以在任意狀態(tài)。處于可中斷等待狀態(tài)或停止?fàn)顟B(tài)的進(jìn)程可能會(huì)被收到的信號(hào)喚醒。
在早期的Unix系統(tǒng)中,發(fā)送信號(hào)實(shí)際就是在接收者進(jìn)程的signal位圖中設(shè)置一個(gè)標(biāo)志。信號(hào)的接收者只知道收到了某類信號(hào),卻不知道信號(hào)的來(lái)源和數(shù)量。在Linux的早期實(shí)現(xiàn)中,實(shí)時(shí)信號(hào)可以帶一個(gè)附加信息,這些附加信息被掛在接收者進(jìn)程的信號(hào)隊(duì)列中,因而實(shí)時(shí)信號(hào)有了數(shù)量的概念。新版本的Linux給普通信號(hào)也準(zhǔn)備了附加信息,可用于通報(bào)信號(hào)的來(lái)源,但為了與老版本兼容,普通信號(hào)仍沒有數(shù)量的概念。
信號(hào)的附加信息被包裝在結(jié)構(gòu)sigqueue中,其中內(nèi)嵌的結(jié)構(gòu)siginfo用于描述附加信息本身,主要內(nèi)容如下:
(1)域si_signo中記錄著信號(hào)的編碼,從1開始。
(2)域si_code中記錄著信號(hào)的來(lái)源,如內(nèi)核、用戶、定時(shí)器等。
(3)聯(lián)合_sifields中記錄著各信號(hào)特定的附加信息,如發(fā)送者的pid、uid等。
信號(hào)的接收者可以是一個(gè)特定的進(jìn)程,也可以是一個(gè)線程組。作為線程組所收到的信號(hào)可以被組中的所有進(jìn)程看到,也可以被組中的任意一個(gè)進(jìn)程處理,但只能被處理一次。不管被哪個(gè)進(jìn)程處理,作為線程組收到的信號(hào)都會(huì)影響到組中所有的進(jìn)程。
發(fā)送信號(hào)的進(jìn)程至少需要提供四個(gè)參數(shù),分別是信號(hào)編號(hào)、附加信息、接收者進(jìn)程、目標(biāo)類別(線程組或單個(gè)進(jìn)程)等。發(fā)送信號(hào)時(shí)完成的工作主要有如下幾件:
(1)解決停止與繼續(xù)信號(hào)的矛盾問(wèn)題,防止它們?cè)诮邮照哌M(jìn)程中共存。①如果要發(fā)送的是停止信號(hào)(SIGSTOP、SIGTSTP、SIGTTOU、SIGTTIN),則應(yīng)將接收者及其線程組此前已收到的繼續(xù)信號(hào)全部清除。
②如果要發(fā)送的是繼續(xù)信號(hào)(SIGCONT),則應(yīng)將接收者及其線程組此前已收到的停止信號(hào)全部清除,并喚醒接收者線程組中所有停止態(tài)的進(jìn)程。
(2)丟棄接收者進(jìn)程要忽略的信號(hào)。在接收者進(jìn)程中,處理程序?yàn)镾IG_IGN的信號(hào)是被忽略的,處理程序?yàn)镾IG_DFL的SIGCHLD、SIGCONT、SIGURG或SIGWINCH信號(hào)也是被忽略的。接收者進(jìn)程當(dāng)前阻塞的信號(hào)不能被忽略。
(3)確定新信號(hào)將要進(jìn)駐的接收隊(duì)列。一個(gè)信號(hào)只能被加入到一個(gè)隊(duì)列中,信號(hào)所在隊(duì)列由參數(shù)中的目標(biāo)類別決定,可能是task_struct結(jié)構(gòu)中的pending或signal_struct結(jié)構(gòu)中的shared_pending隊(duì)列。
(4)丟棄重復(fù)收到的普通信號(hào)。根據(jù)傳統(tǒng)的信號(hào)語(yǔ)義,不需要記錄普通信號(hào)的接收次數(shù)。因而,如果要發(fā)送的普通信號(hào)已在選定的接收隊(duì)列中,可將其直接丟棄。
(5)發(fā)送信號(hào)。
①向?qū)ο髢?nèi)存管理器申請(qǐng)一個(gè)空閑的sigqueue結(jié)構(gòu),用發(fā)送者提供的參數(shù)填寫該結(jié)構(gòu),并將其掛在接收隊(duì)列的隊(duì)尾。為了防止拒絕服務(wù)類攻擊,Linux限制了一個(gè)進(jìn)程可用的sigqueue結(jié)構(gòu)數(shù)。②如果有等待該信號(hào)的進(jìn)程(signalfd_wqh隊(duì)列不空),則將其中的一個(gè)喚醒。
③將信號(hào)在隊(duì)列位圖signal中的標(biāo)志位置1,表示收到了該信號(hào)。
(6)善后處理。
①如果接收者是單個(gè)進(jìn)程,在下列情況下,不需要特別的善后處理:
●信號(hào)正被接收者阻塞。
●接收者進(jìn)程正處于停止?fàn)顟B(tài)。
●接收者進(jìn)程當(dāng)前未在處理器上運(yùn)行且其上已有待處理的信號(hào)。②如果接收者是一個(gè)線程組,則在其中選一個(gè)滿足下列條件之一的進(jìn)程用來(lái)處理該
信號(hào):
●未阻塞該信號(hào)且正在運(yùn)行的進(jìn)程。
●未阻塞該信號(hào)、不在停止?fàn)顟B(tài)且無(wú)其它待處理信號(hào)的進(jìn)程。
如果組中的所有進(jìn)程都不能滿足上述條件,則不需特別處理。③如果信號(hào)(包括SIGKILL)對(duì)接收者是致命的(處理動(dòng)作是終止進(jìn)程),則向線程組中的所有進(jìn)程發(fā)送SIGKILL信號(hào),并將處于可中斷等待狀態(tài)或醒后終止?fàn)顟B(tài)的進(jìn)程全部喚醒,以便讓它們盡快終止。
④如果信號(hào)對(duì)接收者不是致命的但為其選定的處理者進(jìn)程處于可中斷等待狀態(tài),則將其喚醒。
大部分信號(hào)都是由內(nèi)核發(fā)送給進(jìn)程或線程組的,只有一小部分信號(hào)是在進(jìn)程之間互相發(fā)送的。系統(tǒng)會(huì)對(duì)進(jìn)程之間互相發(fā)送的信號(hào)進(jìn)行嚴(yán)格的權(quán)限檢查。Linux提供了多個(gè)系統(tǒng)調(diào)用以便于在進(jìn)程之間互發(fā)信號(hào),主要有以下幾個(gè):
(1)函數(shù)kill()。在未引入線程組時(shí),該函數(shù)用于向一個(gè)進(jìn)程或進(jìn)程組發(fā)送信號(hào)。在引入線程組之后,該函數(shù)的意義取決于接收者進(jìn)程的pid:
①
pid>0,用于向一個(gè)線程組發(fā)送信號(hào),pid是接收者線程組的ID號(hào)。
②
pid=0,用于向發(fā)送者進(jìn)程所在的線程組發(fā)送信號(hào)。
③
pid=-1,用于向當(dāng)前線程組之外的所有其它線程組發(fā)送信號(hào)。
④
pid<-1,用于向進(jìn)程組中的所有進(jìn)程發(fā)送信號(hào),-pid是進(jìn)程組的ID號(hào)。
(2)函數(shù)tgkill()用于向一個(gè)特定的進(jìn)程(由TGID和PID標(biāo)識(shí))發(fā)送信號(hào)。
(3)函數(shù)tkill()是tgkill()的特例,僅指定了PID而未指定TGID,接收者進(jìn)程可位于任意一個(gè)線程組中。
(4)函數(shù)rt_sigqueueinfo()用于向一個(gè)線程組發(fā)送一個(gè)帶附加信息的信號(hào)。
(5)函數(shù)rt_tgsigqueueinfo()用于向一個(gè)特定的進(jìn)程發(fā)送一個(gè)帶附加信息的信號(hào)。10.1.5信號(hào)處理
進(jìn)程可以在任何狀態(tài)下接收信號(hào),但只能在從核心態(tài)返回用戶態(tài)之前處理信號(hào),見圖4.3。也就是說(shuō),信號(hào)是由接收者進(jìn)程在特定時(shí)刻自己處理的,有可能不夠及時(shí)。由于內(nèi)核守護(hù)線程不會(huì)返回用戶態(tài),因而它們不會(huì)處理信號(hào)。
進(jìn)程先處理發(fā)給自己的信號(hào)(在task_struct結(jié)構(gòu)的pending隊(duì)列中),再處理發(fā)給線程組的信號(hào)(在signal_struct結(jié)構(gòu)的shared_pending隊(duì)列中)。但進(jìn)程并不按到達(dá)的順序處理信號(hào)。一般情況下,進(jìn)程會(huì)按編號(hào)從小到大的順序處理隊(duì)列中未被阻塞的信號(hào)。然而有些信號(hào)是由進(jìn)程在執(zhí)行過(guò)程中的異常操作引起的,屬于同步信號(hào),如SIGSEGV、SIGBUS、SIGILL、SIGTRAP、SIGFPE等,應(yīng)該優(yōu)先處理。對(duì)同一種信號(hào)來(lái)說(shuō),先收到的先被處理。處理過(guò)的信號(hào)會(huì)被從隊(duì)列中摘除。當(dāng)隊(duì)列中的一種信號(hào)被徹底處理完之后,它在位圖signal中的標(biāo)志也會(huì)被清除。
進(jìn)程處理信號(hào)的方法由它的sighand_struct結(jié)構(gòu)決定。如果信號(hào)的處理程序是忽略(SIG_IGN),那么簡(jiǎn)單地將其丟棄即可。如果信號(hào)的處理程序是缺省(SIG_DFL),那么由內(nèi)核處理即可。內(nèi)核對(duì)不同信號(hào)的缺省處理方法也不同,大致可分為如下幾類:
(1)忽略。信號(hào)SIGCONT、SIGCHLD、SIGWINCH和SIGURG的缺省處理動(dòng)作是忽略,即將信號(hào)直接丟棄。
(2)停止。信號(hào)SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU的缺省處理動(dòng)作是停止,即將進(jìn)程所在線程組中的所有進(jìn)程(包括自己)都設(shè)為停止?fàn)顟B(tài)。
(3)終止。其它信號(hào)的缺省處理動(dòng)作都是終止,即終止進(jìn)程所在線程組中的所有進(jìn)程(包括自己)。終止線程組中其它進(jìn)程的方法是向它們發(fā)送SIGKILL信號(hào),終止自己的方法是執(zhí)行函數(shù)exit()。如果信號(hào)的處理程序是用戶自定義的,則需要進(jìn)入用戶空間執(zhí)行一次這一用戶態(tài)處理程序。由于進(jìn)程還未返回到用戶空間,按照Intel處理器的約定,不能從高特權(quán)級(jí)向低特權(quán)級(jí)轉(zhuǎn)移控制,因而不能直接調(diào)用處理程序。但由于進(jìn)程正處在返回用戶空間的過(guò)程中,其返回狀態(tài),包括用戶堆棧的棧頂、用戶空間下一條指令的地址等,都已記錄在進(jìn)程的系統(tǒng)堆棧中(由pt_regs結(jié)構(gòu)描述,見圖4.2),因而只要將pt_regs結(jié)構(gòu)中的ip換成信號(hào)處理程序的入口地址(sighand_struct結(jié)構(gòu)中的sa_handler),而后讓進(jìn)程正常返回,它就會(huì)立刻轉(zhuǎn)去執(zhí)行自己定義的信號(hào)處理程序。然而,對(duì)ip域的簡(jiǎn)單修改破壞了進(jìn)程的原有返回狀態(tài),使得進(jìn)程在執(zhí)行完信號(hào)處理程序之后無(wú)法再返回它在用戶空間的應(yīng)有位置。因而,Linux將自定義信號(hào)處理程序的執(zhí)行過(guò)程分成了如下六步:
(1)將系統(tǒng)堆棧棧頂?shù)膒t_regs結(jié)構(gòu)暫存起來(lái)。
(2)修改進(jìn)程的用戶堆棧,在其中壓入必須的參數(shù),如信號(hào)編號(hào)、附加信息等。
(3)將pt_regs結(jié)構(gòu)中的ip改為自定義信號(hào)處理程序的入口地址。
(4)返回用戶態(tài),讓進(jìn)程執(zhí)行自定義的信號(hào)處理程序。
(5)在完成處理工作之后,讓信號(hào)處理程序通過(guò)系統(tǒng)調(diào)用再次進(jìn)入內(nèi)核。
(6)在內(nèi)核中將進(jìn)程的系統(tǒng)堆棧恢復(fù)到修改之前的狀態(tài),而后讓進(jìn)程再次返回。
可以將進(jìn)程系統(tǒng)堆棧棧頂?shù)膒t_regs結(jié)構(gòu)暫存在內(nèi)核中,也可以將其暫存在用戶堆棧中。按照Intel處理器的約定,內(nèi)核程序可以修改用戶空間的數(shù)據(jù),因而可以修改用戶堆棧。Linux將進(jìn)程的pt_regs結(jié)構(gòu)暫存在用戶堆棧中。
較困難的是讓信號(hào)處理程序在完成工作之后再次進(jìn)入內(nèi)核。強(qiáng)行要求信號(hào)處理程序調(diào)用一個(gè)特定的函數(shù)不是一個(gè)好辦法,因?yàn)檫@會(huì)引起程序員的反感,萬(wàn)一遺忘還會(huì)導(dǎo)致嚴(yán)重的進(jìn)程錯(cuò)誤。一種解決辦法是在用戶堆棧的棧頂壓入一段善后程序,并通過(guò)修改用戶堆棧將信號(hào)處理程序的返回地址改成善后程序的入口,由這段善后程序負(fù)責(zé)執(zhí)行系統(tǒng)調(diào)用,以再次進(jìn)入內(nèi)核。Linux壓到用戶堆棧中的善后程序由三條指令組成,如下:
popl%eax //彈出棧頂?shù)膮?shù)
movl$_NR_sigreturn,%eax //將系統(tǒng)調(diào)用號(hào)存入EAX寄存器中
int$0x80 //再次進(jìn)入內(nèi)核,執(zhí)行恢復(fù)程序sys_sigreturn()
然而,在堆棧中壓入代碼是一種非正常的程序執(zhí)行手段。當(dāng)堆棧頁(yè)被禁止執(zhí)行時(shí)(如Intel64中的NXB被置1),這種手段將無(wú)法使用。因而,新版本的Linux將上述善后程序移到了vsyscall頁(yè)(見4.4.4節(jié))中。由于vsyscall頁(yè)總在進(jìn)程的虛擬地址空間中,且以共享庫(kù)(linux-gate.so)的面目出現(xiàn),對(duì)它的調(diào)用總是可行的。既然可以通過(guò)修改用戶堆棧讓信號(hào)處理程序返回到預(yù)定的善后程序,當(dāng)然也可以讓它返回到正常的返回地址。但由于在執(zhí)行信號(hào)處理程序之前需要修改進(jìn)程的阻塞掩碼blocked(阻塞不希望收到的新信號(hào)),因而在執(zhí)行完信號(hào)處理程序之后還應(yīng)恢復(fù)阻塞掩碼。阻塞掩碼的修改必須在內(nèi)核中進(jìn)行,所以再次進(jìn)入內(nèi)核是必須的。
由此可見,執(zhí)行自定義信號(hào)處理程序需要使用到用戶堆棧。如果進(jìn)程的用戶堆棧出現(xiàn)了問(wèn)題(如堆棧溢出),上述處理方法必將無(wú)法工作。為此,Linux允許進(jìn)程通過(guò)系統(tǒng)調(diào)用sigaltstack()定義自己的替換堆棧,專門用于執(zhí)行用戶自定義的信號(hào)處理程序。替換堆棧的位置記錄在task_struct中,開始位置為sas_ss_sp,大小為sas_ss_size。
為了操作方便,Linux將用戶堆棧(或替換堆棧)的棧頂定義成了一個(gè)結(jié)構(gòu),稱為rt_sigframe,如圖10.2所示。
進(jìn)程系統(tǒng)堆棧的棧頂(pt_regs結(jié)構(gòu))被保存在用戶堆棧的uc_mcontext域中,阻塞掩碼被保存在用戶堆棧的uc_sigmask域中。信號(hào)處理程序的返回地址被保存在pretcode域中,通常就是vsyscall頁(yè)中的善后程序入口地址。信號(hào)的附加信息被壓入用戶堆棧的棧頂(info域)。Linux內(nèi)核通過(guò)寄存器向信號(hào)處理程序傳遞參數(shù),包括EAX中的信號(hào)編號(hào)、EDX中的附加信息(info域的地址)、ECX中的ucontext結(jié)構(gòu)等。
進(jìn)程的新阻塞掩碼是老阻塞掩碼與位圖sa_mask的按位或,有時(shí)還需要加上當(dāng)前處理的信號(hào)。如此以來(lái),在信號(hào)處理程序的執(zhí)行過(guò)程中,雖然進(jìn)程可以被中斷,可以收到新的信號(hào),但不會(huì)被不希望見到的信號(hào)打擾。圖10.2用戶堆棧和系統(tǒng)堆棧的棧頂結(jié)構(gòu)經(jīng)過(guò)上述修改之后,返回到用戶態(tài)的進(jìn)程會(huì)自然轉(zhuǎn)入自定義的信號(hào)處理程序,且可看到需要的參數(shù)。當(dāng)信號(hào)處理程序執(zhí)行完后,最后一條ret指令會(huì)將控制轉(zhuǎn)移到vsyscall頁(yè)中的善后程序。善后程序通過(guò)int$0x80再次進(jìn)入內(nèi)核,轉(zhuǎn)去執(zhí)行函數(shù)sys_rt_sigreturn()。
函數(shù)sys_rt_sigreturn()用保存在用戶堆棧中的信息恢復(fù)進(jìn)程的blocked位圖和系統(tǒng)堆棧?;謴?fù)之后,進(jìn)程系統(tǒng)堆棧中的sp又指向了用戶堆棧的老棧頂(相當(dāng)于彈出了棧頂?shù)膔t_sigframe結(jié)構(gòu)),ip又指向了正常的返回地址。當(dāng)函數(shù)sys_rt_sigreturn()執(zhí)行完畢之后,進(jìn)程又一次從核心態(tài)返回用戶態(tài),會(huì)再次檢查進(jìn)程上是否有待處理的信號(hào)。如果還有待處理的信號(hào),進(jìn)程會(huì)繼續(xù)處理它們。特別地,若信號(hào)的處理程序仍然是用戶自定義的,系統(tǒng)會(huì)再次修改進(jìn)程的用戶堆棧、系統(tǒng)堆棧,再次進(jìn)入用戶空間執(zhí)行自定義信號(hào)處理程序,并在處理完后再次進(jìn)入內(nèi)核完成清理工作。
如果進(jìn)程上已沒有待處理的信號(hào),則進(jìn)程會(huì)返回到正常的用戶態(tài),從此前被中斷的位置恢復(fù)正常運(yùn)行。整個(gè)信號(hào)處理過(guò)程附著在中斷處理之后,相當(dāng)于前一次中斷處理或系統(tǒng)調(diào)用的延續(xù)。
如果進(jìn)程在執(zhí)行自定義信號(hào)處理程序的過(guò)程中再次進(jìn)入內(nèi)核(如執(zhí)行系統(tǒng)調(diào)用或被中斷),那么信號(hào)處理程序的執(zhí)行就會(huì)被打斷。如果進(jìn)程上還有其它的待處理信號(hào),那么當(dāng)進(jìn)程從核心態(tài)返回用戶態(tài)時(shí),就會(huì)執(zhí)行新的信號(hào)處理程序。也就是說(shuō)信號(hào)處理的過(guò)程可能是嵌套的。如果不希望發(fā)生這種嵌套現(xiàn)象,應(yīng)在執(zhí)行信號(hào)處理程序時(shí)阻塞其它的信號(hào),如在sigaction結(jié)構(gòu)的sa_mask位圖中聲明需阻塞的信號(hào)等。
值得注意的是Linux對(duì)信號(hào)SIGCHLD的處理。正常情況下,當(dāng)線程組中最后一個(gè)進(jìn)程終止時(shí),它應(yīng)該將自己的退出狀態(tài)設(shè)為EXIT_ZOMBIE并向父進(jìn)程發(fā)送SIGCHLD信號(hào)(見7.4.1節(jié))。如果父進(jìn)程為SIGCHLD自定義了處理程序,該信號(hào)會(huì)被父進(jìn)程正常處理,如init進(jìn)程。然而通常情況下,進(jìn)程為SIGCHLD指定的處理程序都是忽略。按照POSIX約定,進(jìn)程對(duì)SIGCHLD的忽略處理是反復(fù)執(zhí)行函數(shù)wait4(),以回收所有處于僵死態(tài)的子進(jìn)程。事實(shí)上,早期的Linux就是這樣處理SIGCHLD信號(hào)的。這種約定雖然有效,但顯得十分古怪(進(jìn)程對(duì)信號(hào)SIGCHLD的缺省處理是忽略、忽略處理是回收子進(jìn)程)。在新版本中,Linux修正了信號(hào)SIGCHLD的發(fā)送和處理方法,如下:
(1)如果父進(jìn)程為SIGCHLD指定了自定義的處理程序,則正常發(fā)送,正常處理。
(2)如果父進(jìn)程為SIGCHLD指定的處理程序是缺省,則直接將其丟棄。
(3)如果父進(jìn)程為SIGCHLD指定的處理程序是忽略,則將子進(jìn)程的退出狀態(tài)改為EXIT_DEAD,并將它的task_struct結(jié)構(gòu)的引用計(jì)數(shù)減1。如此以來(lái),調(diào)度程序就會(huì)直接將子進(jìn)程釋放,不再需要麻煩父進(jìn)程回收了。10.1.6信號(hào)接收
正常情況下,信號(hào)是異步的。進(jìn)程不知道什么時(shí)候會(huì)收到信號(hào),也不知道什么時(shí)候會(huì)處理信號(hào)。異步的信號(hào)雖然簡(jiǎn)單,但卻讓人覺得難以掌控。為此,新版本的Linux又提供了同步信號(hào)處理方式。
如果進(jìn)程想用同步方式處理自己收到的信號(hào),它可以采用如下方法:
(1)通過(guò)系統(tǒng)調(diào)用signalfd()或signalfd4()創(chuàng)建一個(gè)文件描述符,并在其中指定想要接收的信號(hào)種類(不包括SIGKILL和SIGSTOP)。
(2)通過(guò)系統(tǒng)調(diào)用sigprocmask()阻塞想要同步接收的信號(hào)。
(3)在需要時(shí),直接通過(guò)系統(tǒng)調(diào)用read()讀新建的文件描述符。如果進(jìn)程已收到了指定的信號(hào),read()會(huì)返回信號(hào)的描述信息,包括信號(hào)的編號(hào)和附加信息。如果進(jìn)程還未收到指定的信號(hào),進(jìn)程將被掛在sighand_struct結(jié)構(gòu)的signalfd_wgh隊(duì)列中等待,直到指定的信號(hào)到來(lái)。
(4)當(dāng)不再需要同步接收信號(hào)時(shí),可通過(guò)系統(tǒng)調(diào)用close()關(guān)閉信號(hào)的描述符。
進(jìn)程通過(guò)read()操作讀出的信號(hào)會(huì)自動(dòng)從進(jìn)程的信號(hào)隊(duì)列中刪除。由signalfd()或signalfd4()創(chuàng)建的文件描述符也可用在通用的poll()或select()中,以查詢或監(jiān)視想要同步接收的信號(hào)。執(zhí)行poll()或select()的進(jìn)程會(huì)被阻塞,直到收到需要的信號(hào)或等待超時(shí)。
利用系統(tǒng)調(diào)用sigsuspend()或rt_sigsuspend()也可以實(shí)現(xiàn)信號(hào)的同步處理。想同步處理信號(hào)的進(jìn)程可以定義一個(gè)阻塞位圖,將希望接收信號(hào)的阻塞位清0,而后通過(guò)函數(shù)sigsuspend()或rt_sigsuspend()將進(jìn)程的阻塞位圖換成新定義的阻塞位圖,并將自己掛起。如此以來(lái),只有當(dāng)期望的信號(hào)到來(lái)時(shí),進(jìn)程才會(huì)被喚醒。進(jìn)程被喚醒后,其上的阻塞位圖會(huì)被恢復(fù),進(jìn)程此前執(zhí)行的函數(shù)sigsuspend()或rt_sigsuspend()會(huì)正常返回。
利用系統(tǒng)調(diào)用sigpending()或rt_sigpending()可以查詢進(jìn)程已收到、未被阻塞、未被處理的信號(hào)位圖。
雖然可以帶一個(gè)附加信息,但信號(hào)所傳遞的信息量畢竟十分有限,因而信號(hào)較適合于通知,卻難以勝任大量信息的傳遞。要實(shí)現(xiàn)進(jìn)程之間的大數(shù)據(jù)量通信,還必須提供其它的通信手段,如管道(Pipe)。管道機(jī)制最早由AT&T的M.D.Mcllroy提出,于1973年被引入U(xiǎn)nix操作系統(tǒng)。管道是一項(xiàng)重要的發(fā)明,它使Unix具有了將小程序組合成大工具的能力,使Unix的優(yōu)雅哲學(xué)(小的就是美的)得以充分體現(xiàn)。10.2管道10.2.1管道的意義
正常情況下,進(jìn)程的虛擬地址空間是相互獨(dú)立的,除內(nèi)核之外,在不同進(jìn)程的虛擬地址空間中沒有重疊的部分,進(jìn)程之間沒有自然的通信渠道,無(wú)法進(jìn)行直接通信。然而,若抽象掉程序、數(shù)據(jù)等的實(shí)際含義,可以將進(jìn)程的虛擬地址空間看成一個(gè)字節(jié)的容器。只要在兩個(gè)容器之間建立一條管道(Pipe),一個(gè)進(jìn)程中的字節(jié)(數(shù)據(jù))就可以自然地流動(dòng)到另一個(gè)進(jìn)程中,如圖10.3(a)所示。因而,管道是進(jìn)程間一種最自然的通信方式。
圖10.3基于管道的進(jìn)程間通信在Linux的Shell命令中,管道由‘|’標(biāo)識(shí)。由‘|’鏈接起來(lái)的多個(gè)命令會(huì)創(chuàng)建多個(gè)進(jìn)程(每個(gè)命令對(duì)應(yīng)一個(gè)進(jìn)程),進(jìn)程間建立有自然的管道,前一個(gè)命令(或進(jìn)程)的標(biāo)準(zhǔn)輸出會(huì)被管道轉(zhuǎn)化為后一個(gè)命令(或進(jìn)程)的標(biāo)準(zhǔn)輸入。如在命令“l(fā)s-l|wc-l”中,ls的輸出(當(dāng)前目錄的內(nèi)容)被管道直接遞交給命令wc,用以統(tǒng)計(jì)其中的行數(shù)。
與現(xiàn)實(shí)世界中的管道不同,用于通信的管道具有如下特點(diǎn):
(1)管道是單向的,數(shù)據(jù)只能從入口進(jìn)程流向出口進(jìn)程,不能逆向流動(dòng)。
(2)管道中流動(dòng)的數(shù)據(jù)是字節(jié)流,沒有結(jié)構(gòu),通信的格式需要雙方自己約定。
(3)管道是可靠的、有序的、先進(jìn)先出的,流出的數(shù)據(jù)與流入的數(shù)據(jù)完全一致。
(4)管道有容量限制,當(dāng)管道滿時(shí),發(fā)送端無(wú)法再發(fā)送字節(jié);當(dāng)管道空時(shí),接收端無(wú)法再取出字節(jié)。10.2.2匿名管道
在多年的發(fā)展過(guò)程中,人們給出了多種管道實(shí)現(xiàn)方法。在兩個(gè)進(jìn)程之間建立Socket鏈接之后可以實(shí)現(xiàn)它們之間的通信。同一系統(tǒng)中的兩個(gè)進(jìn)程也可以利用普通文件實(shí)現(xiàn)通信(一個(gè)進(jìn)程向文件尾部寫數(shù)據(jù),另一個(gè)進(jìn)程從文件頭部讀數(shù)據(jù))。然而,利用Socket實(shí)現(xiàn)的通信需要經(jīng)過(guò)網(wǎng)絡(luò)協(xié)議層的轉(zhuǎn)換,其中有許多無(wú)謂的打包、拆包操作,開銷較大。利用普通文件進(jìn)行的通信不夠靈活、速度較慢且浪費(fèi)外存空間。因而,實(shí)用的管道應(yīng)是對(duì)Socket或普通文件的簡(jiǎn)化。目前的Linux利用偽文件系統(tǒng)(稱為pipefs)實(shí)現(xiàn)管道,該文件系統(tǒng)已在系統(tǒng)初始化時(shí)注冊(cè)并安裝。既然管道僅用作通信,那么管道文件就應(yīng)該是一種臨時(shí)文件,沒有必要在外存設(shè)備上真正將其建立起來(lái)。如果用一塊內(nèi)存空間來(lái)模擬管道文件(如圖10.3(b)所示),那么對(duì)它的讀寫操作就可直接在內(nèi)存中進(jìn)行,從而可大大提升通信速度。基于上述考慮,Linux用內(nèi)存中的臨時(shí)文件實(shí)現(xiàn)其經(jīng)典的管道。由于這種管道是動(dòng)態(tài)建立和撤銷的,在文件系統(tǒng)中沒有體現(xiàn),也沒有名稱,故被稱為匿名管道。
匿名管道的建立由系統(tǒng)調(diào)用pipe()或pipe2()完成。兩個(gè)系統(tǒng)調(diào)用的功能一樣,只是后者比前者多一個(gè)標(biāo)志,可用于設(shè)置管道文件的屬性,如非阻塞讀寫等。函數(shù)pipe()或pipe2()會(huì)在pipefs的根目錄中創(chuàng)建一個(gè)管道文件,并將其分別按只寫和只讀方式打開,而后返回兩個(gè)文件描述符(兩個(gè)整數(shù),分別代表兩個(gè)打開的文件),其中的第0個(gè)描述符表示管道文件的讀端口或出口,只能用于從中讀數(shù)據(jù),第1個(gè)描述符表示管道文件的寫端口或入口,只能用于向其中寫數(shù)據(jù)。也就是說(shuō),執(zhí)行函數(shù)pipe()或pipe2()的進(jìn)程會(huì)動(dòng)態(tài)地創(chuàng)建一個(gè)管道文件,并得到它的兩個(gè)端口的描述符,此后該進(jìn)程可以用普通的write()操作向入口端寫數(shù)據(jù)并用read()操作從出口端讀數(shù)據(jù),以實(shí)現(xiàn)基于管道的通信。當(dāng)然,單個(gè)進(jìn)程的自我通信是沒有意義的。要想利用匿名管道實(shí)現(xiàn)雙進(jìn)程間的通信,就必須將管道的一端交給另一個(gè)進(jìn)程。然而,文件描述符是進(jìn)程私有的,在一個(gè)進(jìn)程中使用另一個(gè)進(jìn)程的文件描述符是非法的,因而不能直接將管道文件描述符交給另一個(gè)進(jìn)程,除非這兩個(gè)進(jìn)程是父子關(guān)系。如果進(jìn)程A在執(zhí)行完pipe()或pipe2()之后創(chuàng)建進(jìn)程B,那么A的整個(gè)文件描述符表都會(huì)被B繼承,包括其中的兩個(gè)管道文件描述符。此后,只要進(jìn)程A關(guān)閉管道文件的一端,進(jìn)程B關(guān)閉管道文件的另一端,即可在兩進(jìn)程之間建立起一個(gè)單向的管道,實(shí)現(xiàn)父子進(jìn)程之間的通信。如果進(jìn)程A在執(zhí)行完pipe()或pipe2()之后創(chuàng)建兩個(gè)進(jìn)程B和C,那么B和C都會(huì)繼承A的文件描述符表,包括其中的兩個(gè)管道文件描述符。此后,只要進(jìn)程B關(guān)閉管道文件的一端,進(jìn)程C關(guān)閉管道文件的另一端,即可在兩進(jìn)程之間建立起一個(gè)單向的管道,實(shí)現(xiàn)兄弟進(jìn)程之間的通信。事實(shí)上,利用匿名管道只能實(shí)現(xiàn)父子進(jìn)程或兄弟進(jìn)程之間的通信,如圖10.4所示。
圖10.4父子進(jìn)程之間的通信管道為了提高內(nèi)存的利用率,管道所用的內(nèi)存空間也是動(dòng)態(tài)分配的,每次至少1頁(yè),缺省情況下管道的大小可達(dá)16頁(yè),如圖10.5所示。
在圖10.5中,buffers是管道緩沖區(qū)的最大允許頁(yè)數(shù),nrbufs是管道中的當(dāng)前頁(yè)數(shù),curbuf是位于管道首部的物理頁(yè)。物理頁(yè)的分配由寫操作負(fù)責(zé)。當(dāng)寫操作發(fā)現(xiàn)管道尾部的剩余空間不夠用時(shí),它向物理內(nèi)存管理器申請(qǐng)1個(gè)高端頁(yè),并將其鏈接在管道的尾部。當(dāng)讀操作將管道頭部的物理頁(yè)讀空后,它直接將其釋放。管道是典型的生產(chǎn)者/消費(fèi)者問(wèn)題,其操作具有自然的同步能力。當(dāng)寫操作發(fā)現(xiàn)管道滿時(shí),寫進(jìn)程被掛在wait隊(duì)列上等待,直到被讀進(jìn)程喚醒。當(dāng)讀操作發(fā)現(xiàn)管道中的內(nèi)容不夠讀時(shí),讀進(jìn)程被掛在wait隊(duì)列上等待,直到被寫進(jìn)程喚醒。
圖10.5匿名管道的管理結(jié)構(gòu)生產(chǎn)者在發(fā)送完畢之后可以直接通過(guò)close()操作關(guān)閉自己一方的管道。當(dāng)讀完管道中的數(shù)據(jù)后,消費(fèi)者會(huì)得到一個(gè)EOF(EndofFile)標(biāo)志,此時(shí),它可以關(guān)閉自己一方的管道。當(dāng)兩方都關(guān)閉以后,管道才會(huì)被釋放。如果消費(fèi)者因?yàn)槟撤N原因而提前關(guān)閉管道,那么生產(chǎn)者會(huì)在寫操作中發(fā)現(xiàn)管道已經(jīng)斷裂(讀方已不存在),會(huì)收到一個(gè)SIGPIPE信號(hào)。生產(chǎn)者可以在SIGPIPE信號(hào)的處理程序中關(guān)閉管道。10.2.3命名管道
匿名管道簡(jiǎn)單但能力有限,只能用于父子、兄弟進(jìn)程之間的通信,不能成為一種通用的進(jìn)程間通信機(jī)制,原因是匿名管道“無(wú)名”、“無(wú)形”,只能被隱式地繼承而不能被顯式地聲明。只有當(dāng)管道“有名”、“有形”時(shí),它才可能被任意兩個(gè)進(jìn)程打開,從而實(shí)現(xiàn)任意兩個(gè)進(jìn)程之間的通信。稱這種“有名”、“有形”的管道為命名管道(FIFO)。
命名管道是外存設(shè)備上的一種特殊類型的文件,類型為FIFO,且有一個(gè)永久性的名字。與普通文件不同,由于不需要在其中真正保存數(shù)據(jù),因而命名管道僅需要一個(gè)管理結(jié)構(gòu)(文件控制塊inode),不需要占用外存設(shè)備的其它存儲(chǔ)空間。只要知道命名管道的名稱,所有進(jìn)程都可按需要的方式打開命名管道,并通過(guò)它實(shí)現(xiàn)進(jìn)程間的通信。
使用命名管道的方法如下:
(1)讀進(jìn)程按只讀方式打開命名管道文件,獲得命名管道的文件描述符。
(2)寫進(jìn)程按只寫方式打開命名管道文件,獲得命名管道的文件描述符。
(3)寫進(jìn)程向管道中寫數(shù)據(jù),讀進(jìn)程從管道中讀數(shù)據(jù),實(shí)現(xiàn)相互通信。
(4)通信完成之后,各自關(guān)閉命名管道文件。
命名管道的管理結(jié)構(gòu)與匿名管道的相同。第一個(gè)打開命名管道的進(jìn)程創(chuàng)建管理結(jié)構(gòu)。通常情況下,執(zhí)行打開操作的進(jìn)程會(huì)被阻塞直到命名管道的另一端也被打開。按讀方式打開的命名管道不能寫,按寫方式打開的命名管道不能讀,但命名管道允許按讀寫方式打開。按讀寫方式打開的命名管道既允許讀又允許寫,是一種雙向的管道。與匿名管道不同,一個(gè)命名管道可以被多個(gè)進(jìn)程打開。也就是說(shuō),一個(gè)命名管道可以同時(shí)有多個(gè)寫者和多個(gè)讀者。寫入命名管道的數(shù)據(jù)被按序保存在緩沖區(qū)中,不區(qū)分寫者;讀者從緩沖區(qū)的頭部按序讀出數(shù)據(jù),也不區(qū)分讀者。數(shù)據(jù)被讀出后即從命名管道中消失。顯然,命名管道比匿名管道功能更強(qiáng),也更加靈活。
不管是命名管道還是匿名管道,在其中傳遞的都是沒有經(jīng)過(guò)任何包裝的裸數(shù)據(jù)。這種通信方式的效率雖然很高,但卻丟失了通信所需要的許多信息,如發(fā)送者、接收者、時(shí)間、類型、長(zhǎng)度、邊界等。因而管道通信的適用范圍十分有限,有必要提供更符合人類習(xí)慣的進(jìn)程間通信機(jī)制,如消息隊(duì)列(MessageQueue)。10.3消息隊(duì)列與面向連接的管道通信不同,消息隊(duì)列是一種面向消息的、無(wú)連接的異步通信機(jī)制,更像是基于郵箱(Mailbox)的通信。發(fā)送者將包裝后的消息放到郵箱中,接收者在方便的時(shí)候從郵箱中取走自己需要的整條消息。消息的發(fā)送者和接收者不需要同時(shí)存在。通過(guò)消息隊(duì)列,發(fā)送者和接收者之間可以建立起多種形式的通信關(guān)系,如一對(duì)一、一對(duì)多、多對(duì)一、多對(duì)多等。
早期的Linux僅實(shí)現(xiàn)了符合UnixSystemV規(guī)范的消息隊(duì)列,所采用的管理機(jī)制與信號(hào)量集合相似(見9.6)。新版本的Linux增加了符合POSIX標(biāo)準(zhǔn)的消息隊(duì)列,所采用的管理機(jī)制與文件系統(tǒng)相同。10.3.1SystemV消息隊(duì)列
1970年,為了支持?jǐn)?shù)據(jù)庫(kù)和事務(wù)處理,Bell實(shí)驗(yàn)室在自己內(nèi)部的Unix版本中首次引入了三種進(jìn)程間通信(InterprocessCommunication,IPC)機(jī)制,包括消息隊(duì)列、信號(hào)量集合和共享內(nèi)存。1983年,在SystemV發(fā)布之時(shí),這三種IPC機(jī)制被正式集成到了Unix操作系統(tǒng)中,遂被統(tǒng)稱為UnixSystemV的IPC機(jī)制。
SystemV的三種IPC機(jī)制具有相似的編程接口和使用方法。與信號(hào)量集合相似,Linux為它的每個(gè)消息隊(duì)列都定義了一個(gè)證書(結(jié)構(gòu)kern_ipc_perm),用于描述消息隊(duì)列的基本屬性,如鍵值(外部名稱)key、內(nèi)部標(biāo)識(shí)id、創(chuàng)建者的uid和gid、擁有者的uid和gid、訪問(wèn)權(quán)限mode、序列號(hào)seq、安全證書security等。其中鍵值key是一個(gè)整數(shù)或者魔數(shù),是用戶為消息隊(duì)列起的名稱。消息隊(duì)列由鍵值命名,由id號(hào)標(biāo)識(shí)。進(jìn)程使用消息隊(duì)列的過(guò)程如下:
(1)通過(guò)其它途徑(如預(yù)先約定等)獲得消息隊(duì)列的鍵值。
(2)打開消息隊(duì)列,核對(duì)訪問(wèn)權(quán)限,獲得id號(hào)。第一個(gè)打開者創(chuàng)建消息隊(duì)列。
(3)通過(guò)id號(hào)對(duì)消息隊(duì)列進(jìn)行初始化,而后在其上執(zhí)行發(fā)送、接收操作。
(4)由最后一個(gè)使用者釋放并銷毀消息隊(duì)列。
消息隊(duì)列是動(dòng)態(tài)創(chuàng)建的。Linux用結(jié)構(gòu)msg_queue描述消息隊(duì)列,主要內(nèi)容如下:
(1)證書q_perm是一個(gè)kern_ipc_perm結(jié)構(gòu),描述消息隊(duì)列的基本屬性。
(2)字節(jié)數(shù)q_cbytes是一個(gè)無(wú)符號(hào)長(zhǎng)整數(shù),表示隊(duì)列中當(dāng)前的消息總長(zhǎng)度。
(3)消息數(shù)q_qnum是一個(gè)無(wú)符號(hào)長(zhǎng)整數(shù),表示隊(duì)列中當(dāng)前的消息條數(shù)。
(4)容量q_qbytes是一個(gè)無(wú)符號(hào)長(zhǎng)整數(shù),表示隊(duì)列的最大容量(字節(jié)數(shù))。
(5)隊(duì)列q_messages是通用鏈表的表頭,用于組織隊(duì)列中的所有消息。
(6)隊(duì)列q_receivers是通用鏈表的表頭,用于組織等待從隊(duì)列中接收消息的進(jìn)程。
(7)隊(duì)列q_senders是通用鏈表的表頭,用于組織等待向隊(duì)列中發(fā)送消息的進(jìn)程。圖10.6SystemV的消息隊(duì)列管理結(jié)構(gòu)發(fā)送到隊(duì)列中的消息由正文和消息頭構(gòu)成,消息頭由結(jié)構(gòu)msg_msg描述,如下:
structmsg_msg{
structlist_head m_list; //隊(duì)列節(jié)點(diǎn)
long m_type; //消息類型
int m_ts; //消息正文的長(zhǎng)度
structmsg_msgseg *next; //消息正文的附加段
void *security; //消息的安全標(biāo)識(shí)
};正常情況下,消息正文緊跟著消息頭。如果消息較短(包裝后的長(zhǎng)度不超過(guò)一頁(yè)),整條消息會(huì)被集中存放在一塊連續(xù)的內(nèi)存空間中。如果消息較長(zhǎng)(包裝后的長(zhǎng)度超過(guò)一頁(yè)),消息正文會(huì)被分成幾部分,每一部分的長(zhǎng)度都不超過(guò)一頁(yè)。長(zhǎng)消息的第一部分帶著消息頭,其余部分(稱為附加段)的前面帶一個(gè)指針,用于將長(zhǎng)消息的所有附加段串成一個(gè)單向隊(duì)列。消息頭中的next是附加段隊(duì)列的隊(duì)頭。
消息隊(duì)列具有同步能力。當(dāng)消息隊(duì)列達(dá)到或接近容量極限,無(wú)法再容納新的消息時(shí),發(fā)送進(jìn)程被掛在q_senders中等待;當(dāng)消息隊(duì)列中沒有需要類型的消息時(shí),接收進(jìn)程被掛在q_receivers中等待。早期的Linux用一個(gè)靜態(tài)的全局指針數(shù)組(長(zhǎng)度為128)組織系統(tǒng)中的消息隊(duì)列,雖然簡(jiǎn)單但不夠靈活。新版本的Linux改用IDR(IDRadix)樹來(lái)組織消息隊(duì)列,每個(gè)IPC名字空間一棵,IDR樹的樹根記錄在IPC名字空間中,如圖10.7所示。消息隊(duì)列的ID號(hào)id、在IDR樹中的索引號(hào)idx與序列號(hào)seq(記錄在證書結(jié)構(gòu)kern_ipc_perm中)之間的關(guān)系為id=idx+32768
×
seq,idx=id%32768。
打開消息隊(duì)列的操作是msgget(),需要的參數(shù)有兩個(gè),一是消息隊(duì)列的鍵值key,二是消息隊(duì)列的訪問(wèn)權(quán)限。打開操作搜索申請(qǐng)者進(jìn)程的名字空間(實(shí)際是它的IDR樹),以確定鍵值為key的消息隊(duì)列是否已在其中。如果在,說(shuō)明該消息隊(duì)列已被其它進(jìn)程創(chuàng)建,只要申請(qǐng)者能通過(guò)它的權(quán)限檢查,即可返回它的id號(hào)。如果不在,說(shuō)明該消息隊(duì)列還未被建立,需要?jiǎng)?chuàng)建一個(gè)新的消息隊(duì)列,并返回它的id號(hào)。
圖10.7基于IDR樹的消息隊(duì)列管理結(jié)構(gòu)創(chuàng)建新消息隊(duì)列的工作如下:
(1)在當(dāng)前IDR樹中為新消息隊(duì)列找一個(gè)空槽位,確定其索引號(hào)和id號(hào)。
(2)創(chuàng)建一個(gè)msg_queue結(jié)構(gòu),設(shè)置其中的key、id、seq、uid、gid等域,并將其插入到IDR樹中。
在獲得消息隊(duì)列的id號(hào)之后,通過(guò)函數(shù)msgctl()可對(duì)其進(jìn)行管理操作,如:
(1)通過(guò)操作MSG_INFO獲取當(dāng)前名字空間中消息隊(duì)列的狀態(tài)信息,包括允許創(chuàng)建的最大消息隊(duì)列數(shù)、單個(gè)消息的最大長(zhǎng)度(字節(jié))、單個(gè)消息隊(duì)列的最大容量(字節(jié))、單個(gè)消息中的最大附加段數(shù)、當(dāng)前已創(chuàng)建的消息隊(duì)列數(shù)等。
(2)通過(guò)操作MSG_STAT獲取特定消息隊(duì)列的狀態(tài)信息,包括隊(duì)列的證書、隊(duì)列中的消息數(shù)、隊(duì)列中的字節(jié)數(shù)、最近一次發(fā)送時(shí)間、最近一次接收時(shí)間等。
(3)通過(guò)操作IPC_SET設(shè)置特定消息隊(duì)列的屬性,包括uid、gid、訪問(wèn)權(quán)限、隊(duì)列的最大容量等。消息隊(duì)列有容量限制,雖然容量是可調(diào)的。
(4)通過(guò)操作IPC_RMID銷毀特定的消息隊(duì)列,包括其中的所有消息,同時(shí)喚醒在該消息隊(duì)列上等待的所有進(jìn)程。
向消息隊(duì)列發(fā)送消息的操作是msgsnd(),需要的參數(shù)有消息隊(duì)列的id號(hào)、用戶空間中的消息緩沖區(qū)、消息正文的長(zhǎng)度和特殊的發(fā)送要求(如非阻塞發(fā)送)等。每個(gè)消息都有一個(gè)類型,其含義由發(fā)送者和接收者自己約定(見表10.2)。消息類型與消息正文一起記錄在消息緩沖區(qū)中,類型在前(占四個(gè)字節(jié))正文在后。消息發(fā)送的過(guò)程如下:
(1)根據(jù)id號(hào)找到消息隊(duì)列(msg_queue結(jié)構(gòu)),進(jìn)行必要的權(quán)限檢查。
(2)如果消息隊(duì)列已沒有足夠的空間來(lái)接納新的消息,且發(fā)送者未聲明非阻塞發(fā)送,則將發(fā)送者進(jìn)程掛在隊(duì)列q_senders中等待,直到接收者為其騰出足夠的空間。
(3)如果可以接收新消息,則將用戶緩沖區(qū)中的消息正文讀入內(nèi)核,將其加上消息頭(msg_msg結(jié)構(gòu))后掛在隊(duì)列q_messages中。如果消息正文較長(zhǎng),要將其拆分成附加段。
(4)如果隊(duì)列q_receivers中有等待接收消息的進(jìn)程,且新到來(lái)的消息能滿足其中某個(gè)等待者的需求,則將該等待進(jìn)程喚醒。
從消息隊(duì)列中接收消息的操作是msgrcv(),需要的參數(shù)有消息隊(duì)列的id號(hào)、用戶空間中的消息緩沖區(qū)、消息正文的長(zhǎng)度、期望接收的消息類型和特殊的接收要求(如非阻塞接收)等。與管道不同,接收者可以通過(guò)消息類型指定自己期望接收的消息(不一定是隊(duì)列中的第一個(gè)消息)。消息類型的含義如表10.2所示。
表10.2消息類型的含義消息接收的過(guò)程如下:
(1)根據(jù)id號(hào)找到消息隊(duì)列(msg_queue結(jié)構(gòu)),進(jìn)行必要的權(quán)限檢查。
(2)如果消息隊(duì)列中沒有滿足要求的消息,且接收者未明確聲明非阻塞接收,則將接收者進(jìn)程掛在隊(duì)列q_receivers中等待,直到被發(fā)送者喚醒。
(3)如果消息隊(duì)列中有滿足要求的消息,則將其從隊(duì)列中摘下,將其內(nèi)容拷貝到用戶空間的緩沖區(qū)中,并釋放消息所占用的內(nèi)存空間。
(4)如果隊(duì)列q_senders中有等待發(fā)送消息的進(jìn)程,則喚醒其中的第一個(gè)等待者。
值得注意的是,隊(duì)列中的每條消息都是完整的實(shí)體,消息與消息之間有嚴(yán)格的邊界,消息只能被整條發(fā)送和接收,不能僅發(fā)送或接收部分消息。另外,等待發(fā)送或接收消息的進(jìn)程處于可中斷等待狀態(tài),可被信號(hào)喚醒。被信號(hào)喚醒的發(fā)送或接收者進(jìn)程將錯(cuò)誤返回,表示發(fā)送或接收失敗。
SystemV的消息隊(duì)列雖然靈活,但存在著一些問(wèn)題,如:
(1)與Linux的其它I/O機(jī)制不兼容。在Linux中,所有的輸入/輸出都已被統(tǒng)一在虛擬文件系統(tǒng)的框架之內(nèi),但消息隊(duì)列是一個(gè)例外。雖然消息隊(duì)列借用了文件系統(tǒng)的實(shí)現(xiàn)思想,但其標(biāo)識(shí)方法(鍵值而不是文件名)、使用方法(不是打開、讀寫、關(guān)閉)等都與標(biāo)準(zhǔn)的文件操作不同,需要為其提供專門的管理工具。
(2)難以確定銷毀時(shí)機(jī)。消息隊(duì)列是一種無(wú)鏈接的通信機(jī)制,不需要通信各方同時(shí)存在。也就是說(shuō)消息隊(duì)列可以離開發(fā)送者和接收者進(jìn)程獨(dú)立存在,通過(guò)消息隊(duì)列可以向未來(lái)某個(gè)時(shí)刻啟動(dòng)的進(jìn)程傳遞信息。消息隊(duì)列的這一特性使內(nèi)核無(wú)法確定其銷毀時(shí)機(jī),過(guò)早銷毀會(huì)丟失隊(duì)列中的消息,忘記銷毀則會(huì)使隊(duì)列及其中的消息成為內(nèi)存垃圾。
事實(shí)上,SystemV的三種IPC機(jī)制中都存在上述問(wèn)題。所以有人建議應(yīng)盡量避免使用SystemV的消息隊(duì)列,而代之以命名管道或POSIX消息隊(duì)列。10.3.2POSIX消息隊(duì)列
POSIX的IPC是模仿SystemV的IPC制定的,目的是為了解決SystemV的上述問(wèn)題。POSIX的IPC也包含三種機(jī)制,分別是消息隊(duì)列、信號(hào)量集合和共享內(nèi)存。POSIX稱IPC機(jī)制中的單個(gè)個(gè)體為對(duì)象,如一個(gè)POSIX消息隊(duì)列被稱為一個(gè)消息隊(duì)列對(duì)象。
與SystemV的IPC不同,POSIX將自己的三種IPC機(jī)制全都集成在了虛擬文件系統(tǒng)框架之內(nèi),或者說(shuō)POSIX將它的IPC對(duì)象也都看成是文件,采用與普通文件相同或相似的方式來(lái)操作和使用它們。在POSIX中,IPC對(duì)象的標(biāo)識(shí)方式不再是鍵值而是文件名,如“/myqueue”。在使用IPC對(duì)象之前需要用open()操作將其打開并獲得描述符。如果指定名稱的IPC對(duì)象不存在,open()操作會(huì)為其創(chuàng)建一個(gè)新的對(duì)象。在獲得IPC對(duì)象的描述符后,可以對(duì)其進(jìn)行需要的操作,如在消息隊(duì)列對(duì)象上進(jìn)行發(fā)送、接收操作,在信號(hào)量對(duì)象上進(jìn)行P、V操作等。用完后的IPC對(duì)象需要用close()操作關(guān)閉。
POSIX在每個(gè)IPC對(duì)象上都關(guān)聯(lián)了一個(gè)引用計(jì)數(shù),用于記錄該對(duì)象被打開的次數(shù)。對(duì)象每被打開一次,它的引用計(jì)數(shù)就被加1,每被關(guān)閉一次,它的引用計(jì)數(shù)就被減1。引用計(jì)數(shù)為0的對(duì)象可被銷毀。在對(duì)象被銷毀之后,新的open()操作會(huì)再次創(chuàng)建指定名稱的IPC對(duì)象。
為了管理POSIX的消息隊(duì)列,Linux為它的每個(gè)IPC名字空間都專門建立了一個(gè)名為mqueue的偽文件系統(tǒng)(用戶不可見)。與常見的文件系統(tǒng)不同,mqueue中僅有一個(gè)根目錄,每個(gè)動(dòng)態(tài)創(chuàng)建的消息隊(duì)列對(duì)象都是根目錄中的普通文件。在系統(tǒng)初始化時(shí),mqueue已被注冊(cè)并安裝,它的安裝結(jié)構(gòu)已被記錄在IPC名字空間中。與SystemV的消息隊(duì)列不同,POSIX的消息隊(duì)列具有雙重身份。在mqueue文件系統(tǒng)中,消息隊(duì)列是普通文件,其描述結(jié)構(gòu)是inode和目錄項(xiàng)(dentry結(jié)構(gòu));在IPC中,消息隊(duì)列就是消息隊(duì)列,其描述結(jié)構(gòu)中定義有消息的等待隊(duì)列和進(jìn)程的等待隊(duì)列。因而POSIX的消息隊(duì)列描述結(jié)構(gòu)是兩種身份的組合。Linux為POSIX消息隊(duì)列所定義的管理結(jié)構(gòu)是mqueue_inode_info,如圖10.8所示。
圖10.8POSIX單個(gè)消息隊(duì)列的管理結(jié)構(gòu)
POSIX消息隊(duì)列的隊(duì)列屬性由域attr描述,其中的主要內(nèi)容包括mq_maxmsg(消息隊(duì)列容量,即最多可容納的消息數(shù))、mq_msgsize(單個(gè)消息的最大長(zhǎng)度,單位為字節(jié))、mq_
curmsgs(隊(duì)列中當(dāng)前的消息數(shù))和mq_flags(隊(duì)列的操作標(biāo)志,如是否允許非阻塞發(fā)送和接收等)。消息隊(duì)列的容量是在創(chuàng)建時(shí)指定的,在使用過(guò)程中不可再改變。
在POSIX消息隊(duì)列中,用于暫存消息的不是一個(gè)鏈表而是一個(gè)指針數(shù)組messages,其大小與隊(duì)列屬性中的最大消息數(shù)mq_maxmsg相等。與SystemV的消息隊(duì)列相同,POSIX的消息也被包裝在結(jié)構(gòu)msg_msg中,但消息類型m_type被重新解釋成了優(yōu)先級(jí)。在數(shù)組messages中的消息按優(yōu)先級(jí)排序,優(yōu)先級(jí)低的在前,優(yōu)先級(jí)高的在后。messages[0]所指消息的優(yōu)先級(jí)最低,messages[mq_curmsgs-1]所指消息的優(yōu)先級(jí)最高。進(jìn)程接收的總是優(yōu)先級(jí)最高的消息。
如果消息隊(duì)列已滿,發(fā)送者應(yīng)該等待,e_wait_q[0]是發(fā)送者進(jìn)程等待隊(duì)列;如果消息隊(duì)列已空,接收者應(yīng)該等待,e_wait_q[1]是接收者進(jìn)程等待隊(duì)列。與SystemV的等待隊(duì)列不同,POSIX的等待隊(duì)列是有序的,高優(yōu)先級(jí)的進(jìn)程在前,低優(yōu)先級(jí)的進(jìn)程在后。與SystemV的消息隊(duì)列不同,POSIX允許為消息隊(duì)列選擇一種通知方式,如信號(hào),以支持異步接收。當(dāng)空消息隊(duì)列首次收到新消息時(shí),如果e_wait_q[1]中沒有等待的接收者,系統(tǒng)將向預(yù)定消息的接收者發(fā)送一個(gè)信號(hào)。信號(hào)的編號(hào)記錄在mqueue_inode_info結(jié)構(gòu)的notify域中,信號(hào)的接收者記錄在notify_owner域中。進(jìn)程可以通過(guò)系統(tǒng)調(diào)用mq_notify()預(yù)定消息。
POSIX消息隊(duì)列的文件屬性由域vfs_inode描述,其主要內(nèi)容包括訪問(wèn)權(quán)限i_mode、inode_operations操作集、file_operations操作集等。每個(gè)inode結(jié)構(gòu)都關(guān)聯(lián)著一個(gè)目錄項(xiàng)結(jié)構(gòu)dentry,其中記錄著消息隊(duì)列的名稱和組織關(guān)系,如父、子關(guān)系等。系統(tǒng)中所有的目錄項(xiàng),包括消息隊(duì)列目錄項(xiàng),都被組織在一個(gè)名為dentry_hashtable的全局Hash表中。消息隊(duì)列目錄項(xiàng)的Hash值是根據(jù)隊(duì)列的名稱和根目錄的地址算出的。給出一個(gè)消息隊(duì)列名,查IPC名字空間可獲得mqueue文件系統(tǒng)的根目錄,查表dentry_hashtable可找到消息隊(duì)列的dentry結(jié)構(gòu)和與之關(guān)聯(lián)的inode結(jié)構(gòu),進(jìn)而可獲得該消息隊(duì)列的管理結(jié)構(gòu)mqueue_inode_info。圖10.9是IPC名字空間中消息隊(duì)列的組織結(jié)構(gòu)。
圖10.9IPC名字空間中消息隊(duì)列的組織結(jié)構(gòu)當(dāng)進(jìn)程打開消息隊(duì)列時(shí),Linux會(huì)根據(jù)名稱查Hash表dentry_hashtable。如果指定名稱的消息隊(duì)列不在該Hash表中,則要為其創(chuàng)建一個(gè)新對(duì)象。進(jìn)程在創(chuàng)建新消息隊(duì)列時(shí)需要指定訪問(wèn)權(quán)限和屬性。新消息隊(duì)列的創(chuàng)建過(guò)程如下:
(1)創(chuàng)建一個(gè)dentry結(jié)構(gòu),將其d_name設(shè)為新消息隊(duì)列的名稱。
(2)將新建的dentry插入到mqueue文件系統(tǒng)的根目錄(作為根目錄中的普通文件)和Hash表dentry_hashtable中。
(3)創(chuàng)建一個(gè)mqueue_inode_info結(jié)構(gòu)(包括messages數(shù)組)并對(duì)其進(jìn)行初始化,包括設(shè)置其中的vfs_inode部分(i_mode、i_uid、i_fop等)和屬性部分attr,而后將新建的dentry與vfs_inode關(guān)聯(lián)起來(lái)。
(4)創(chuàng)建一個(gè)file結(jié)構(gòu),設(shè)置它的f_mode、f_path、f_pos、f_op等,并將其插入到進(jìn)程的文件描述符表中。結(jié)構(gòu)file在文件描述符表中的索引就是新消息隊(duì)列的描述符。如果進(jìn)程要打開的消息隊(duì)列在表dentry_hashtable中,說(shuō)明它已被其它進(jìn)程創(chuàng)建,其dentry和mqueue_inode_info結(jié)構(gòu)已經(jīng)存在。此時(shí)的打開操作僅需完成兩件事,一是核對(duì)進(jìn)程的訪問(wèn)權(quán)限,二是創(chuàng)建一個(gè)新的file結(jié)構(gòu)并將其插入到進(jìn)程的文件描述符表中。
在獲得消息隊(duì)列的描述符之后,可以向其發(fā)送消息。向消息隊(duì)列發(fā)送消息的操作是mq_timedsend(),需要的參數(shù)有消息隊(duì)列的描述符、用戶空間的消息緩沖區(qū)、消息正文的長(zhǎng)度、消息的優(yōu)先級(jí)、最長(zhǎng)等待時(shí)間等。消息的發(fā)送過(guò)程如下:
(1)用消息隊(duì)列的描述符查進(jìn)程的描述符表,找到與之對(duì)應(yīng)的file結(jié)構(gòu),進(jìn)而得到它的dentry結(jié)構(gòu)、inode結(jié)構(gòu)和mqueue_inode_info結(jié)構(gòu),進(jìn)行必要的權(quán)限檢查。
(2)將用戶空間的消息拷貝到內(nèi)核,將其包裝在msg_msg結(jié)構(gòu)中,將其類型m_type設(shè)為消息的優(yōu)先級(jí)。
(3)如果隊(duì)列已滿且不允許非阻塞發(fā)送,則啟動(dòng)一個(gè)高精度定時(shí)器,而后按優(yōu)先級(jí)從大到小的順序?qū)l(fā)送者進(jìn)程掛在e_wait_q[0]中等待。進(jìn)程醒來(lái)的原因有三:
①定時(shí)器到期,說(shuō)明一直沒有接收者讀取消息,發(fā)送失敗。②收到信號(hào),說(shuō)明進(jìn)程遇到了需要緊急處理的事件,發(fā)送失敗。
③隊(duì)列中出現(xiàn)空缺,說(shuō)明已有消息被用戶取走,可以再次嘗試發(fā)送。
(4)如果隊(duì)列未滿或已出現(xiàn)空缺,則將消息按優(yōu)先級(jí)從小到大的順序插入到數(shù)組messages中。
(5)如果隊(duì)列上有等待的接收者,則喚醒e_wait_q[1]中的第一個(gè)進(jìn)程(優(yōu)先級(jí)最高)。如果沒有等待的接收者,且隊(duì)列中沒有其它的消息,則通知預(yù)定的接收者。從消息隊(duì)列中接收消息的操作是mq_timedreceive(),需要的參數(shù)有消息隊(duì)列的描述符、用戶空間的消息緩沖區(qū)、緩沖區(qū)的長(zhǎng)度、消息的優(yōu)先級(jí)、最長(zhǎng)等待時(shí)間等。消息的接收過(guò)程如下:
(1)用消息隊(duì)列的描述符查進(jìn)程的描述符表,找到與之對(duì)應(yīng)的file結(jié)構(gòu),進(jìn)而得到它的dentry結(jié)構(gòu)、inode結(jié)構(gòu)和mqueue_inode_info結(jié)構(gòu),進(jìn)行必要的權(quán)限檢查。
(2)如果隊(duì)列已空且不允許非阻塞接收,則啟動(dòng)一個(gè)高精度定時(shí)器,而后按優(yōu)先級(jí)從大到小的順序?qū)⒔邮照哌M(jìn)程掛在e_wait_q[1]中等待。進(jìn)程醒來(lái)的原因有三:①定時(shí)器到期,說(shuō)明一直沒有消息到來(lái),接收失敗。
②收到信號(hào),說(shuō)明進(jìn)程遇到了需要緊急處理的事件,接收失敗。
③隊(duì)列中出現(xiàn)新消息,可以再次嘗試接收。
(3)如果隊(duì)列非空,則將優(yōu)先級(jí)最高的消息從messages數(shù)組中摘下,將其內(nèi)容和優(yōu)先級(jí)拷貝到用戶空間的消息緩沖區(qū)中,而后釋放消息所占用的內(nèi)核內(nèi)存空間。
(4)如果有等待發(fā)送的進(jìn)程,則喚醒e_wait_q[0]中的第一個(gè)等待者。當(dāng)不再需要向消息隊(duì)列發(fā)送消息或從消息隊(duì)列接收消息時(shí),進(jìn)程可以將打開的消息隊(duì)列關(guān)閉。關(guān)閉操作是close(),所需的參數(shù)是消息隊(duì)列的描述符,所完成的工作是釋放描述符所對(duì)應(yīng)的file結(jié)構(gòu)。與普通文件的關(guān)閉操作一樣,消息隊(duì)列的關(guān)閉操作也不會(huì)將隊(duì)列對(duì)象銷毀。
徹底銷毀消息隊(duì)列的操作是mq_unlink(),需要提供的參數(shù)是消息隊(duì)列的名稱。操作mq_unlink()所完成的工作與創(chuàng)建相反,包括:
(1)根據(jù)名稱查Hash表dentry_hashtable找到與之對(duì)應(yīng)的dentry并進(jìn)行權(quán)限檢查。
(2)遞減dentry中的引用計(jì)數(shù)。如果引用計(jì)數(shù)大于0,說(shuō)明該消息隊(duì)列還有用戶,不能將其銷毀。如果引用計(jì)數(shù)為0,則銷毀消息隊(duì)列:
①釋放隊(duì)列中的所有消息。
②釋放隊(duì)列的mqueue_inode_info結(jié)構(gòu),包括其中的massages數(shù)組。
③將dentry結(jié)構(gòu)從dentry_hashtable表和目錄樹中刪除并釋放。
由此可見,POSIX消息隊(duì)列本身并未定義引用計(jì)數(shù),它使用的引用計(jì)數(shù)實(shí)際是從目錄項(xiàng)結(jié)構(gòu)dentry中借用的。
雖然用管道和消息隊(duì)列可以實(shí)現(xiàn)進(jìn)程之間的通信,但它們的通信代價(jià)都比較高。在通信時(shí),發(fā)送方需要將數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間,接收方需要將數(shù)據(jù)從內(nèi)核空間再拷貝回用戶空間。數(shù)據(jù)的來(lái)回拷貝既浪費(fèi)了內(nèi)存又浪費(fèi)了時(shí)間,應(yīng)該盡量避免。10.4共享內(nèi)存事實(shí)上,如果能在兩個(gè)進(jìn)程之間建立一塊共用的物理內(nèi)存空間,那么它們之間就可以直接交換信息。一個(gè)進(jìn)程對(duì)共用內(nèi)存空間的修改可以立刻被另一個(gè)進(jìn)程看到,數(shù)據(jù)不需要在進(jìn)程之間來(lái)回拷貝,參與通信的進(jìn)程不需要執(zhí)行專門的發(fā)送和接收操作,通信過(guò)程也不需要內(nèi)核介入,因而可極大地提升通信的速度。這種利用共用物理內(nèi)存空間實(shí)現(xiàn)的進(jìn)程間通信稱為共享內(nèi)存(sharedmemory),是一種最快速的通信方式。共享內(nèi)存的問(wèn)題是它可能被多個(gè)進(jìn)程同時(shí)訪問(wèn)。為了保證通信的可靠性,需要其它手段來(lái)實(shí)現(xiàn)進(jìn)程間的互斥與同步。當(dāng)然,互斥與同步操作會(huì)降低通信的速度。由于正常情況下的進(jìn)程之間沒有共享的虛擬內(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ū)間與進(jìn)程虛擬地址空間之間建立映射關(guān)系。映射之后的文件可以直接訪問(wèn),就像它們已被讀入內(nèi)存一樣,不需要再執(zhí)行專門的read()、write()操作。若進(jìn)程訪問(wèn)的內(nèi)容未被讀入內(nèi)存,處理器會(huì)產(chǎn)生頁(yè)故障異常,虛擬內(nèi)存管理器會(huì)找到與之對(duì)應(yīng)的文件頁(yè)并將其讀入。因此,文件映射是進(jìn)程使用文件的一種極為簡(jiǎn)潔的方法。如果將一個(gè)文件區(qū)間同時(shí)映射到多個(gè)進(jìn)程的虛擬地址空間中(在每個(gè)進(jìn)程中建立一個(gè)虛擬內(nèi)存區(qū)域),那么該文件區(qū)間就會(huì)變成進(jìn)程之間的公共內(nèi)存區(qū)間。也就是說(shuō),可以利用文件映射在進(jìn)程之間建立共享內(nèi)存??蓤?zhí)行文件的加載操作(exec類操作)是一種隱式的文件映射操作,mmap()是一種顯式的文件映射操作。加載操作建立的是私有映射(對(duì)應(yīng)的虛擬內(nèi)存區(qū)域稱為私有區(qū)域),且一次會(huì)建立多個(gè)虛擬內(nèi)存區(qū)域。mmap()操作既可建立私有映射也可建立共享映射(對(duì)應(yīng)的虛擬內(nèi)存區(qū)域稱為共享區(qū)域),且一次僅會(huì)建立一個(gè)虛擬內(nèi)存區(qū)域。
如果進(jìn)程僅在私有區(qū)域上執(zhí)行讀操作,那么系統(tǒng)僅會(huì)在物理內(nèi)存中保留一份文件內(nèi)容。用于保存文件內(nèi)容的物理頁(yè)出現(xiàn)在多個(gè)進(jìn)程的頁(yè)表中,是它們的共享內(nèi)存區(qū)間。如果進(jìn)程在私有區(qū)域上執(zhí)行了寫操作,虛擬內(nèi)存管理器會(huì)立刻為其建立一個(gè)私有的拷貝。私有拷貝不再是進(jìn)程之間的共享區(qū)間,一個(gè)進(jìn)程寫入其中的內(nèi)容無(wú)法被其它進(jìn)程看到,也不會(huì)被寫入映射文件,因而利用私有映射無(wú)法實(shí)現(xiàn)進(jìn)程之間的通信。與私有區(qū)域不同,不管進(jìn)程在共享區(qū)域上執(zhí)行讀操作還是寫操作,虛擬內(nèi)存管理器僅會(huì)在物理內(nèi)存中保留一份文件內(nèi)容,用于保存文件內(nèi)容的物理頁(yè)可能被共享它的所有進(jìn)程訪問(wèn)。按共享方式映射同一文件區(qū)間的所有進(jìn)程都可對(duì)其進(jìn)行讀寫操作,一個(gè)進(jìn)程寫入的內(nèi)容可以立刻被其它進(jìn)程看到,進(jìn)程對(duì)共享區(qū)間的修改結(jié)果也會(huì)被寫回映射文件。因而,利用共享文件映射可以實(shí)現(xiàn)進(jìn)程之間的通信,如圖10.10所示。
圖10.10通過(guò)共享文件映射實(shí)現(xiàn)的共享內(nèi)存在圖10.10中,文件中的兩頁(yè)按共享方式被同時(shí)映射到進(jìn)程1和進(jìn)程2的虛擬地址空間中,這兩頁(yè)的內(nèi)容在物理內(nèi)存中僅有一個(gè)拷貝,進(jìn)程1和進(jìn)程2都可對(duì)這兩個(gè)物理頁(yè)進(jìn)行讀寫操作,一個(gè)進(jìn)程對(duì)它的修改可以立刻被另一個(gè)進(jìn)程看到,修改結(jié)果也會(huì)被寫回映射文件。進(jìn)程1和進(jìn)程2通過(guò)共享文件映射建立起了共享內(nèi)存。
用于建立文件映射的操作是mmap(),該函數(shù)需要6個(gè)參數(shù),分別是文件描述符fd、區(qū)間在文件中的開始位置offset、區(qū)間的長(zhǎng)度length、虛擬內(nèi)存區(qū)域的開始位置addr、映射方式flags(私有、共享等)和虛擬內(nèi)存區(qū)域的訪問(wèn)權(quán)限prot(讀、
寫)。
如果申請(qǐng)者未指定參數(shù)addr,虛擬內(nèi)存管理器將為其選擇一個(gè)適當(dāng)?shù)挠成湮恢?。即使申?qǐng)者
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 二零二五年度護(hù)校與養(yǎng)老機(jī)構(gòu)合作服務(wù)合同3篇
- 女生節(jié)活動(dòng)策劃方案(3篇)
- 中小學(xué)校實(shí)驗(yàn)室內(nèi)部管理制度范文(二篇)
- 2025年度物流運(yùn)輸安全環(huán)保服務(wù)協(xié)議范本3篇
- 液壓銑床課程設(shè)計(jì)摘要
- 財(cái)務(wù)分析圖表課程設(shè)計(jì)
- 平路機(jī)安全操作規(guī)程范文(2篇)
- 二零二五年度房地產(chǎn)租賃權(quán)包銷合同3篇
- 2025年上半年安全員工作總結(jié)(3篇)
- 2024年滬教版高三歷史上冊(cè)階段測(cè)試試卷
- 虛擬貨幣地址分析技術(shù)的研究-洞察分析
- 綠色供應(yīng)鏈管理制度內(nèi)容
- 無(wú)錫市區(qū)2024-2025學(xué)年四年級(jí)上學(xué)期數(shù)學(xué)期末試題一(有答案)
- 血液凈化中心院內(nèi)感染控制課件
- 一年級(jí)數(shù)學(xué)(上)計(jì)算題專項(xiàng)練習(xí)集錦
- 年產(chǎn)1.5萬(wàn)噸長(zhǎng)鏈二元酸工程建設(shè)項(xiàng)目可研報(bào)告
- 《北航空氣動(dòng)力學(xué)》課件
- 紡織廠消防管道安裝協(xié)議
- 【MOOC】思辨式英文寫作-南開大學(xué) 中國(guó)大學(xué)慕課MOOC答案
- 期末測(cè)試卷(試題)-2024-2025學(xué)年五年級(jí)上冊(cè)數(shù)學(xué)北師大版
- 2024年下半年中國(guó)石油大連石化分公司招聘30人易考易錯(cuò)模擬試題(共500題)試卷后附參考答案
評(píng)論
0/150
提交評(píng)論