Linux環(huán)境進(jìn)程間通信(四):信號燈
信號燈與其他進(jìn)程間通信方式不大相同,它主要提供對進(jìn)程間共享資源訪問控制機(jī)制。相當(dāng)于內(nèi)存中的標(biāo)志,進(jìn)程可以根據(jù)它判定是否能夠訪問某些共享資源,同時,進(jìn)程也可以修改該標(biāo)志。除了用于訪問控制外,還可用于進(jìn)程同步。信號燈有以下兩種類型:
- 二值信號燈:最簡單的信號燈形式,信號燈的值只能取0或1,類似于互斥鎖。
注:二值信號燈能夠?qū)崿F(xiàn)互斥鎖的功能,但兩者的關(guān)注內(nèi)容不同。信號燈強(qiáng)調(diào)共享資源,只要共享資源可用,其他進(jìn)程同樣可以修改信號燈的值;互斥鎖更強(qiáng)調(diào)進(jìn)程,占用資源的進(jìn)程使用完資源后,必須由進(jìn)程本身來解鎖。 - 計(jì)算信號燈:信號燈的值可以取任意非負(fù)值(當(dāng)然受內(nèi)核本身的約束)。
linux對信號燈的支持狀況與消息隊(duì)列一樣,在red had 8.0發(fā)行版本中支持的是系統(tǒng)V的信號燈。因此,本文將主要介紹系統(tǒng)V信號燈及其相應(yīng)API。在沒有聲明的情況下,以下討論中指的都是系統(tǒng)V信號燈。
注意,通常所說的系統(tǒng)V信號燈指的是計(jì)數(shù)信號燈集。
1、系統(tǒng)V信號燈是隨內(nèi)核持續(xù)的,只有在內(nèi)核重起或者顯示刪除一個信號燈集時,該信號燈集才會真正被刪除。因此系統(tǒng)中記錄信號燈的數(shù)據(jù)結(jié)構(gòu)(struct ipc_ids sem_ids)位于內(nèi)核中,系統(tǒng)中的所有信號燈都可以在結(jié)構(gòu)sem_ids中找到訪問入口。
2、下圖說明了內(nèi)核與信號燈是怎樣建立起聯(lián)系的:
其中:struct ipc_ids sem_ids是內(nèi)核中記錄信號燈的全局?jǐn)?shù)據(jù)結(jié)構(gòu);描述一個具體的信號燈及其相關(guān)信息。

其中,struct sem結(jié)構(gòu)如下:
struct sem{ int semval; // current value int sempid // pid of last operation }
從上圖可以看出,全局?jǐn)?shù)據(jù)結(jié)構(gòu)struct ipc_ids sem_ids可以訪問到struct kern_ipc_perm的第一個成員:struct kern_ipc_perm;而每個struct kern_ipc_perm能夠與具體的信號燈對應(yīng)起來是因?yàn)樵谠摻Y(jié)構(gòu)中,有一個key_t類型成員key,而key則唯一確定一個信號燈集;同時,結(jié)構(gòu)struct kern_ipc_perm的最后一個成員sem_nsems確定了該信號燈在信號燈集中的順序,這樣內(nèi)核就能夠記錄每個信號燈的信息了。kern_ipc_perm結(jié)構(gòu)參見《Linux環(huán)境進(jìn)程間通信(三):消息隊(duì)列》。struct sem_array見附錄1。
對消息隊(duì)列的操作無非有下面三種類型:
1、 打開或創(chuàng)建信號燈
與消息隊(duì)列的創(chuàng)建及打開基本相同,不再詳述。
2、 信號燈值操作
linux可以增加或減小信號燈的值,相應(yīng)于對共享資源的釋放和占有。具體參見后面的semop系統(tǒng)調(diào)用。
3、 獲得或設(shè)置信號燈屬性:
系統(tǒng)中的每一個信號燈集都對應(yīng)一個struct sem_array結(jié)構(gòu),該結(jié)構(gòu)記錄了信號燈集的各種信息,存在于系統(tǒng)空間。為了設(shè)置、獲得該信號燈集的各種信息及屬性,在用戶空間有一個重要的聯(lián)合結(jié)構(gòu)與之對應(yīng),即union semun。

聯(lián)合semun數(shù)據(jù)結(jié)構(gòu)各成員意義參見附錄2
信號燈API
1、文件名到鍵值
#include <sys/types.h> #include <sys/ipc.h> key_t ftok (char*pathname, char proj);
它返回與路徑pathname相對應(yīng)的一個鍵值,具體用法請參考《Linux環(huán)境進(jìn)程間通信(三):消息隊(duì)列》。
2、 linux特有的ipc()調(diào)用:
int ipc(unsigned int call, int first, int second, int third, void *ptr, long fifth);
參數(shù)call取不同值時,對應(yīng)信號燈的三個系統(tǒng)調(diào)用:
當(dāng)call為SEMOP時,對應(yīng)int semop(int semid, struct sembuf *sops, unsigned nsops)調(diào)用;
當(dāng)call為SEMGET時,對應(yīng)int semget(key_t key, int nsems, int semflg)調(diào)用;
當(dāng)call為SEMCTL時,對應(yīng)int semctl(int semid,int semnum,int cmd,union semun arg)調(diào)用;
這些調(diào)用將在后面闡述。
注:本人不主張采用系統(tǒng)調(diào)用ipc(),而更傾向于采用系統(tǒng)V或者POSIX進(jìn)程間通信API。原因已在Linux環(huán)境進(jìn)程間通信(三):消息隊(duì)列中給出。
3、系統(tǒng)V信號燈API
系統(tǒng)V消息隊(duì)列API只有三個,使用時需要包括幾個頭文件:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h>
1)int semget(key_t key, int nsems, int semflg)
參數(shù)key是一個鍵值,由ftok獲得,唯一標(biāo)識一個信號燈集,用法與msgget()中的key相同;參數(shù)nsems指定打開或者新創(chuàng)建的信號燈集中將包含信號燈的數(shù)目;semflg參數(shù)是一些標(biāo)志位。參數(shù)key和semflg的取值,以及何時打開已有信號燈集或者創(chuàng)建一個新的信號燈集與msgget()中的對應(yīng)部分相同,不再祥述。
該調(diào)用返回與健值key相對應(yīng)的信號燈集描述字。
調(diào)用返回:成功返回信號燈集描述字,否則返回-1。
注:如果key所代表的信號燈已經(jīng)存在,且semget指定了IPC_CREAT|IPC_EXCL標(biāo)志,那么即使參數(shù)nsems與原來信號燈的數(shù)目不等,返回的也是EEXIST錯誤;如果semget只指定了IPC_CREAT標(biāo)志,那么參數(shù)nsems必須與原來的值一致,在后面程序?qū)嵗羞€要進(jìn)一步說明。
2)int semop(int semid, struct sembuf *sops, unsigned nsops);
semid是信號燈集ID,sops指向數(shù)組的每一個sembuf結(jié)構(gòu)都刻畫一個在特定信號燈上的操作。nsops為sops指向數(shù)組的大小。
sembuf結(jié)構(gòu)如下:
|
sem_num對應(yīng)信號集中的信號燈,0對應(yīng)第一個信號燈。sem_flg可取IPC_NOWAIT以及SEM_UNDO兩個標(biāo)志。如果設(shè)置了SEM_UNDO標(biāo)志,那么在進(jìn)程結(jié)束時,相應(yīng)的操作將被取消,這是比較重要的一個標(biāo)志位。如果設(shè)置了該標(biāo)志位,那么在進(jìn)程沒有釋放共享資源就退出時,內(nèi)核將代為釋放。如果為一個信號燈設(shè)置了該標(biāo)志,內(nèi)核都要分配一個sem_undo結(jié)構(gòu)來記錄它,為的是確保以后資源能夠安全釋放。事實(shí)上,如果進(jìn)程退出了,那么它所占用就釋放了,但信號燈值卻沒有改變,此時,信號燈值反映的已經(jīng)不是資源占有的實(shí)際情況,在這種情況下,問題的解決就靠內(nèi)核來完成。這有點(diǎn)像僵尸進(jìn)程,進(jìn)程雖然退出了,資源也都釋放了,但內(nèi)核進(jìn)程表中仍然有它的記錄,此時就需要父進(jìn)程調(diào)用waitpid來解決問題了。
sem_op的值大于0,等于0以及小于0確定了對sem_num指定的信號燈進(jìn)行的三種操作。具體請參考linux相應(yīng)手冊頁。
這里需要強(qiáng)調(diào)的是semop同時操作多個信號燈,在實(shí)際應(yīng)用中,對應(yīng)多種資源的申請或釋放。semop保證操作的原子性,這一點(diǎn)尤為重要。尤其對于多種資源的申請來說,要么一次性獲得所有資源,要么放棄申請,要么在不占有任何資源情況下繼續(xù)等待,這樣,一方面避免了資源的浪費(fèi);另一方面,避免了進(jìn)程之間由于申請共享資源造成死鎖。
也許從實(shí)際含義上更好理解這些操作:信號燈的當(dāng)前值記錄相應(yīng)資源目前可用數(shù)目;sem_op>0對應(yīng)相應(yīng)進(jìn)程要釋放sem_op數(shù)目的共享資源;sem_op=0可以用于對共享資源是否已用完的測試;sem_op<0相當(dāng)于進(jìn)程要申請-sem_op個共享資源。再聯(lián)想操作的原子性,更不難理解該系統(tǒng)調(diào)用何時正常返回,何時睡眠等待。
調(diào)用返回:成功返回0,否則返回-1。
3) int semctl(int semid,int semnum,int cmd,union semun arg)
該系統(tǒng)調(diào)用實(shí)現(xiàn)對信號燈的各種控制操作,參數(shù)semid指定信號燈集,參數(shù)cmd指定具體的操作類型;參數(shù)semnum指定對哪個信號燈操作,只對幾個特殊的cmd操作有意義;arg用于設(shè)置或返回信號燈信息。
該系統(tǒng)調(diào)用詳細(xì)信息請參見其手冊頁,這里只給出參數(shù)cmd所能指定的操作。
IPC_STAT | 獲取信號燈信息,信息由arg.buf返回; |
IPC_SET | 設(shè)置信號燈信息,待設(shè)置信息保存在arg.buf中(在manpage中給出了可以設(shè)置哪些信息); |
GETALL | 返回所有信號燈的值,結(jié)果保存在arg.array中,參數(shù)sennum被忽略; |
GETNCNT | 返回等待semnum所代表信號燈的值增加的進(jìn)程數(shù),相當(dāng)于目前有多少進(jìn)程在等待semnum代表的信號燈所代表的共享資源; |
GETPID | 返回最后一個對semnum所代表信號燈執(zhí)行semop操作的進(jìn)程ID; |
GETVAL | 返回semnum所代表信號燈的值; |
GETZCNT | 返回等待semnum所代表信號燈的值變成0的進(jìn)程數(shù); |
SETALL | 通過arg.array更新所有信號燈的值;同時,更新與本信號集相關(guān)的semid_ds結(jié)構(gòu)的sem_ctime成員; |
SETVAL | 設(shè)置semnum所代表信號燈的值為arg.val; |
調(diào)用返回:調(diào)用失敗返回-1,成功返回與cmd相關(guān):
Cmd | return value |
GETNCNT | Semncnt |
GETPID | Sempid |
GETVAL | Semval |
GETZCNT | Semzcnt |
1、 一次系統(tǒng)調(diào)用semop可同時操作的信號燈數(shù)目SEMOPM,semop中的參數(shù)nsops如果超過了這個數(shù)目,將返回E2BIG錯誤。SEMOPM的大小特定與系統(tǒng),redhat 8.0為32。
2、 信號燈的最大數(shù)目:SEMVMX,當(dāng)設(shè)置信號燈值超過這個限制時,會返回ERANGE錯誤。在redhat 8.0中該值為32767。
3、 系統(tǒng)范圍內(nèi)信號燈集的最大數(shù)目SEMMNI以及系統(tǒng)范圍內(nèi)信號燈的最大數(shù)目SEMMNS。超過這兩個限制將返回ENOSPC錯誤。redhat 8.0中該值為32000。
4、 每個信號燈集中的最大信號燈數(shù)目SEMMSL,redhat 8.0中為250。 SEMOPM以及SEMVMX是使用semop調(diào)用時應(yīng)該注意的;SEMMNI以及SEMMNS是調(diào)用semget時應(yīng)該注意的。SEMVMX同時也是semctl調(diào)用應(yīng)該注意的。
第一個創(chuàng)建信號燈的進(jìn)程同時也初始化信號燈,這樣,系統(tǒng)調(diào)用semget包含了兩個步驟:創(chuàng)建信號燈;初始化信號燈。由此可能導(dǎo)致一種競爭狀態(tài):第一個創(chuàng)建信號燈的進(jìn)程在初始化信號燈時,第二個進(jìn)程又調(diào)用semget,并且發(fā)現(xiàn)信號燈已經(jīng)存在,此時,第二個進(jìn)程必須具有判斷是否有進(jìn)程正在對信號燈進(jìn)行初始化的能力。在參考文獻(xiàn)[1]中,給出了繞過這種競爭狀態(tài)的方法:當(dāng)semget創(chuàng)建一個新的信號燈時,信號燈結(jié)構(gòu)semid_ds的sem_otime成員初始化后的值為0。因此,第二個進(jìn)程在成功調(diào)用semget后,可再次以IPC_STAT命令調(diào)用semctl,等待sem_otime變?yōu)榉?值,此時可判斷該信號燈已經(jīng)初始化完畢。下圖描述了競爭狀態(tài)產(chǎn)生及解決方法:

實(shí)際上,這種解決方法也是基于這樣一個假定:第一個創(chuàng)建信號燈的進(jìn)程必須調(diào)用semop,這樣sem_otime才能變?yōu)榉橇阒怠A硗猓驗(yàn)榈谝粋€進(jìn)程可能不調(diào)用semop,或者semop操作需要很長時間,第二個進(jìn)程可能無限期等待下去,或者等待很長時間。
本實(shí)例有兩個目的:1、獲取各種信號燈信息;2、利用信號燈實(shí)現(xiàn)共享資源的申請和釋放。并在程序中給出了詳細(xì)注釋。
|
注:讀者可以嘗試一下注釋掉初始化步驟,進(jìn)程在運(yùn)行時會出現(xiàn)何種情況(進(jìn)程在申請資源時會睡眠),同時可以像程序結(jié)尾給出的注釋那樣,把該程序編譯成兩個不同版本。下面是本程序的運(yùn)行結(jié)果(操作系統(tǒng)redhat8.0):
|
Summary:信號燈與其它進(jìn)程間通信方式有所不同,它主要用于進(jìn)程間同步。通常所說的系統(tǒng)V信號燈實(shí)際上是一個信號燈的集合,可用于多種共享資源的進(jìn)程間同步。每個信號燈都有一個值,可以用來表示當(dāng)前該信號燈代表的共享資源可用(available)數(shù)量,如果一個進(jìn)程要申請共享資源,那么就從信號燈值中減去要申請的數(shù)目,如果當(dāng)前沒有足夠的可用資源,進(jìn)程可以睡眠等待,也可以立即返回。當(dāng)進(jìn)程要申請多種共享資源時,linux可以保證操作的原子性,即要么申請到所有的共享資源,要么放棄所有資源,這樣能夠保證多個進(jìn)程不會造成互鎖。Linux對信號燈有各種各樣的限制,程序中給出了輸出結(jié)果。另外,如果讀者想對信號燈作進(jìn)一步的理解,建議閱讀sem.h源代碼,該文件不長,但給出了信號燈相關(guān)的重要數(shù)據(jù)結(jié)構(gòu)。
附錄1: struct sem_array如下:
|
其中,sem_queue結(jié)構(gòu)如下:
|
附錄2:union semun是系統(tǒng)調(diào)用semctl中的重要參數(shù):
|
[1] UNIX網(wǎng)絡(luò)編程第二卷:進(jìn)程間通信,作者:W.Richard Stevens,譯者:楊繼張,清華大學(xué)出版社。對POSIX以及系統(tǒng)V信號燈都有闡述,對Linux環(huán)境下的程序開發(fā)有極大的啟發(fā)意義。
[2] linux內(nèi)核源代碼情景分析(上),毛德操、胡希明著,浙江大學(xué)出版社,給出了系統(tǒng)V信號燈相關(guān)的源代碼分析,尤其在闡述保證操作原子性方面,以及闡述undo標(biāo)志位時,討論的很深刻。
[3]GNU/Linux編程指南,第二版,Kurt Wall等著,張輝譯
[4]semget、semop、semctl手冊
鄭彥興,國防科大攻讀博士學(xué)位。聯(lián)系方式: mlinux@163.com
from: http://www.ddvip.net/program/vc/index6/61.htm
posted on 2005-08-04 13:06 weidagang2046 閱讀(323) 評論(0) 編輯 收藏 所屬分類: Linux