8、信號量
信號量和我們前面提到的IPC不太一樣符喝,它是一個(gè)計(jì)數(shù)器闪彼,用來提供在多個(gè)進(jìn)程之間共享數(shù)據(jù)的訪問。
Single UNIX Specification 有一個(gè)對信號量的實(shí)時(shí)擴(kuò)展协饲,這里不對它進(jìn)行討論畏腕。
為了獲得一個(gè)共享的資源,進(jìn)程需要做如下的工作:
- 對控制資源的信號量進(jìn)行測試茉稠。
- 如果信號量的值是整數(shù)描馅,那么進(jìn)程可以對共享資源進(jìn)行訪問,這時(shí)候?qū)研盘柫康闹禍p少1而线,表示使用了一個(gè)單位的資源铭污。
- 如果信號量的值大于0恋日,那么進(jìn)程睡眠直到信號量大于0,然后進(jìn)程醒來執(zhí)行步驟1嘹狞。
如果一個(gè)進(jìn)程操作完信號量控制的共享數(shù)據(jù)谚鄙,那么將會(huì)給信號量加1,如果有其他進(jìn)程由于等待信號量而處于睡眠狀態(tài)刁绒,那么這些進(jìn)程將會(huì)被喚醒闷营。
為了正確地執(zhí)行信號量, 信號量值的測試和減少操作必須是一個(gè)原子操作 知市。因此傻盟,信號量一般在內(nèi)核中實(shí)現(xiàn)。
有一個(gè)叫二進(jìn)制的比較通用的信號量嫂丙,這個(gè)信號量控制一個(gè)單一的資源娘赴,它的值被初始化為1。一般來說跟啤,信號量可以被初始化成為任何正數(shù)诽表,數(shù)值表示可用的共享資源的單元數(shù)目。
XSI的信號量比這復(fù)雜多了隅肥,有三個(gè)特性導(dǎo)致了這樣的復(fù)雜性:
- 信號量并不簡單地是一個(gè)單個(gè)的非負(fù)值竿奏。相反,我們將信號量定義成了包含一個(gè)或者多個(gè)信號量值的 集合 腥放。當(dāng)創(chuàng)建了一個(gè)信號量的時(shí)候泛啸,我們指定集合中值的數(shù)目。
- 信號量的創(chuàng)建(semget)是和它的初始化(semctl)相獨(dú)立的秃症。這個(gè)缺點(diǎn)比較致命候址,因?yàn)槲覀儾荒軇?chuàng)建一個(gè)信號量集合同時(shí)初始化集合中的所有值。
- 由于所有形式的XSI IPC即使在沒有進(jìn)程使用它們的時(shí)候也會(huì)保持存在种柑,所以我們可能會(huì)擔(dān)心一個(gè)應(yīng)用程序沒有釋放分配給它的信號量就終止了岗仑。后面我們討論的undo特性用來處理這種情況。
內(nèi)核為每一個(gè)信號量集合維持一個(gè)semid_ds數(shù)據(jù)結(jié)構(gòu)聚请,如下:
struct semid_ds {
struct ipc_perm sem_perm; /* see Section 15.6.2 */
unsigned short sem_nsems; /* # of semaphores in set */
time_t sem_otime; /* last-semop() time */
time_t sem_ctime; /* last-change time */
.
};
以上是Single UNIX Specification標(biāo)準(zhǔn)定義的一些成員荠雕,具體實(shí)現(xiàn)還可以定義其他的成員。
每個(gè)信號量由至少有以下成員的匿名結(jié)構(gòu)體所表示:
struct {
unsigned short semval; /* semaphore value, always >= 0 */
pid_t sempid; /* pid for last operation */
unsigned short semncnt; /* # processes awaiting semval>curval */
unsigned short semzcnt; /* # processes awaiting semval==0 */
.
};
對于信號量集合的系統(tǒng)限制這里就不給出了良漱,具體參見參考資料舞虱。
semget
首先調(diào)用semget獲得一個(gè)信號量ID。
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
返回值:如果成功返回信號量ID母市,如果錯(cuò)誤返回1矾兜。
前面我們討論了將一個(gè)關(guān)鍵字轉(zhuǎn)換成標(biāo)識的方法,以及創(chuàng)建一個(gè)新的信號量集合和對已經(jīng)存在的信號量集合的引用患久。當(dāng)創(chuàng)建一個(gè)新的信號量集合的時(shí)候椅寺,semid_ds結(jié)構(gòu)變量的如下成員將會(huì)被初始化浑槽。
- ipc_perm結(jié)構(gòu)如同前面描述的那樣被初始化(即所有的成員都會(huì)被初始化)。結(jié)構(gòu)的mode成員將會(huì)被設(shè)置成相應(yīng)的flag權(quán)限位返帕。
- sem_otime 被設(shè)置成0桐玻。
- sem_ctime 被設(shè)置成當(dāng)前時(shí)間。
- sem_nsems 被設(shè)置成nsems荆萤。
集合中的信號量的數(shù)目用nsems進(jìn)行表示镊靴。如果一個(gè) 新的集合被創(chuàng)建(一般這個(gè)集合都是被服務(wù)進(jìn)程創(chuàng)建),我們必須指定nsems ;如果我們 只是引用一個(gè)已經(jīng)存在的集合(一般集合被客戶進(jìn)程引用)链韭,我們可以指定nsems為0 偏竟。
semctl
semctl函數(shù)用來進(jìn)行各種類型的信號量操作。
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
返回值:參見后面敞峭。
第4個(gè)參數(shù)是可選的踊谋,它取決于請求的命令,如果存在旋讹,那么這個(gè)參數(shù)的類型是semun殖蚕,這是一個(gè)各種命令的參數(shù)的聯(lián)合。
union semun {
int val; /* for SETVAL */
struct semid_ds *buf; /* for IPC_STAT and IPC_SET */
unsigned short *array; /* for GETALL and SETALL */
};
我們需要注意的是沉迹, 這個(gè)可選的參數(shù)是一個(gè)實(shí)際的聯(lián)合變量睦疫,而不是一個(gè)指向聯(lián)合變量的指針 。
參數(shù)cmd指定了如下的10個(gè)命令胚股,這些命令對semid相應(yīng)的集合進(jìn)行操作笼痛。
有5各命令使用semnum指定集合中的一個(gè)成員以引用特定的信號量值裙秋。semnum的范圍是[0,nsems-1]琅拌。
- IPC_STAT 獲取集合相應(yīng)的semid_ds結(jié)構(gòu),把它存放在第4個(gè)參數(shù)arg.buf中摘刑。
- IPC_SET 設(shè)置和集合相關(guān)聯(lián)的semid_ds結(jié)構(gòu)變量的sem_perm.uid, sem_perm.gid,和sem_perm.mode成員进宝,設(shè)置的值來自arg.buf。這個(gè)命令的執(zhí)行進(jìn)程的有效用戶id必續(xù)和sem_perm.cuid或者sem_perm.uid相等枷恕,或者執(zhí)行這個(gè)命令的進(jìn)程是具有超級用戶權(quán)限的党晋。
- IPC_RMID 從系統(tǒng)中刪除信號量集合。刪除的動(dòng)作立即生效徐块。任何使用這個(gè)信號量的進(jìn)程再次操作信號量的時(shí)候?qū)?huì)得到EIDRM錯(cuò)誤未玻。這個(gè)命令的執(zhí)行進(jìn)程的有效用戶id必續(xù)和sem_perm.cuid或者sem_perm.uid相等,或者執(zhí)行這個(gè)命令的進(jìn)程是具有超級用戶權(quán)限的胡控。
- GETVAL 返回semnum對應(yīng)的信號量的信號量值扳剿。
- SETVAL 設(shè)置semnum所對應(yīng)的信號量的值。這個(gè)值通過arg.val來進(jìn)行指定昼激。
- GETPID 返回semnum對應(yīng)的信號量的sempid成員庇绽。
- GETNCNT 返回semnum對應(yīng)的信號量的semncnt成員锡搜。
- GETZCNT 返回semnum對應(yīng)的信號量的semzcnt成員。
- GETALL 獲取集合中的所有信號量的值瞧掺。這些值被存放在arg.array所指向的數(shù)組中耕餐。
- SETALL 設(shè)置集合中的所有信號量值,這些值來自arg.array數(shù)組辟狈。
semop
函數(shù)semop會(huì)原子性地對一個(gè)信號量集合執(zhí)行數(shù)組中指定的一系列操作肠缔。
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
返回值:如果成功返回0,如果錯(cuò)誤返回1哼转。
semoparray參數(shù)是一個(gè)指向信號量操作的數(shù)組的指針桩砰,其元素的數(shù)據(jù)結(jié)構(gòu)如下:
struct sembuf {
unsigned short sem_num; /* member # in set (0, 1, ..., nsems-1) */
short sem_op; /* operation (negative, 0, or positive) */
short sem_flg; /* IPC_NOWAIT, SEM_UNDO */
};
nops參數(shù)指定操作數(shù)組中元素的數(shù)目。
對集合中的每個(gè)成員的操作通過相應(yīng)的sem_op值來進(jìn)行指定释簿。這個(gè)值可以是負(fù)數(shù)亚隅,0,或者是正數(shù)庶溶。(在下面的討論中煮纵,我們會(huì)引用到信號量的"undo"標(biāo)記,這個(gè)標(biāo)記相應(yīng)于sem_flg成員中的SEM_UNDO比特位)
情況I. 最簡單的情況是sem_op是正數(shù)偏螺。
這個(gè)情況相應(yīng)于進(jìn)程所返回的資源的數(shù)目行疏。sem_op的值會(huì)被添加到信號量的值中。如果undo標(biāo)記被指定了套像,那么會(huì)從這個(gè)進(jìn)程的信號量調(diào)整值減去sem_op酿联。
II. 如果sem_op是負(fù)數(shù),那么表示我們想要獲取信號量控制的資源夺巩。
如果信號量的值大于或者等于sem_op的絕對值(資源可用)贞让,那么會(huì)將信號量的值減去sem_op的絕對值。這保證信號量的結(jié)果值大于或者等于0柳譬。如果undo標(biāo)記被指定喳张,那么sem_op的絕對值也會(huì)被加到這個(gè)進(jìn)程的信號量調(diào)整值中。
如果信號量的值比sem_op的絕對值忻腊摹(資源不可用)销部,那么會(huì)發(fā)生如下的情況:
- 如果IPC_NOWAIT被指定了,那么semop會(huì)返回一個(gè)EAGAIN錯(cuò)誤制跟。
- 如果IPC_NOWAIT沒有被指定舅桩,那么這個(gè)信號量的semncnt值會(huì)增加(因?yàn)檎{(diào)用者將要睡覺),然后調(diào)用者會(huì)掛起直到如下的情況發(fā)生雨膨。
- 信號量的值變成了大于或者等于sem_op的絕對值的時(shí)候(也就是說其他的進(jìn)程釋放了一些資源)擂涛。信號量的semncnt的值會(huì)被減少(因?yàn)檎{(diào)用進(jìn)程正在進(jìn)行等待),同時(shí)sem_op的絕對值會(huì)被從信號量的值中被減去哥放。如果undo標(biāo)記被指定歼指,那么sem_op的絕對值也會(huì)被加到這個(gè)進(jìn)程的信號量調(diào)整值上爹土。
- 信號量被從系統(tǒng)中移除的時(shí)候。這個(gè)時(shí)候踩身,函數(shù)返回一個(gè)EIDRM錯(cuò)誤胀茵。
- 進(jìn)程捕獲到了一個(gè)信號,并且信號處理函數(shù)返回挟阻。這個(gè)情況琼娘,信號量的semncnt的值會(huì)被減少(因?yàn)檎{(diào)用進(jìn)程不會(huì)再進(jìn)行等待),并且函數(shù)返回一個(gè)EINTR錯(cuò)誤附鸽。
III. 如果sem_op的值是0脱拼,這表示調(diào)用進(jìn)程想要等待,一直到信號量的值變成為0坷备。
如果信號量的值當(dāng)前是0熄浓,那么函數(shù)會(huì)立即返回。
如果信號量的值為非0省撑,那么會(huì)根據(jù)如下情況進(jìn)行處理:
- 如果IPC_NOWAIT被指定了赌蔑,那么返回錯(cuò)誤EAGAIN。
- 如果沒有指定IPC_NOWAIT竟秫,那么信號量的semzcnt值會(huì)被增加(因?yàn)檎{(diào)用這將要進(jìn)行睡眠)娃惯,同時(shí)調(diào)用進(jìn)程掛起,直到如下的情況發(fā)生肥败。
- 信號量的值變成了0,信號量的semncnt的值會(huì)被減少(因?yàn)檎{(diào)用進(jìn)程正在進(jìn)行等待)趾浅。
- 信號量被從系統(tǒng)中移除的時(shí)候。這個(gè)時(shí)候馒稍,函數(shù)返回一個(gè)EIDRM錯(cuò)誤皿哨。
- 進(jìn)程捕獲到了一個(gè)信號,并且信號處理函數(shù)返回筷黔。這個(gè)情況往史,信號量的semncnt的值會(huì)被減少(因?yàn)檎{(diào)用進(jìn)程不會(huì)再進(jìn)行等待),并且函數(shù)返回一個(gè)EINTR錯(cuò)誤佛舱。
semop函數(shù)的操作是原子性質(zhì)的,要么數(shù)組中的操作全部被做挨决,要么一個(gè)也不做请祖。
在退出時(shí)候?qū)π盘柫康恼{(diào)整
我們前面說過,如果一個(gè)進(jìn)程通過信號量分配了資源脖祈,那么當(dāng)進(jìn)程結(jié)束的時(shí)候肆捕,可能會(huì)出現(xiàn)問題。當(dāng)我們?yōu)樾盘柫坎僮髦付⊿EM_UNDO標(biāo)記并且我們分配一個(gè)資源(sem_op值小于0)盖高,內(nèi)核會(huì)記住我們給那個(gè)信號量分配了多少資源(sem_op的絕對值)慎陵。當(dāng)進(jìn)程結(jié)束的時(shí)候眼虱,無論是主動(dòng)的還是非主動(dòng)地結(jié)束,內(nèi)核都會(huì)檢查這個(gè)進(jìn)程是否具有信號量調(diào)整值席纽,如果有捏悬,將會(huì)把對應(yīng)的信號量調(diào)整值疊加到信號量上,實(shí)現(xiàn)相應(yīng)的調(diào)整润梯。
如果我們通過SETVAL或者SETALL命令調(diào)用semctl設(shè)置信號量的值过牙,那么那個(gè)信號量在所有進(jìn)程的的調(diào)整值都被設(shè)置成0。
信號量和記錄鎖的時(shí)間對比的例子
如果我們在多個(gè)進(jìn)程之間共享一個(gè)單個(gè)的資源纺铭,我們可以使用信號量或者記錄鎖寇钉。
通過信號量技術(shù),我們創(chuàng)建了一個(gè)只有一個(gè)信號量成員的信號量集合舶赔,并且將這個(gè)成員信號量的值初始化為1扫倡。分配資源的時(shí)候,我們調(diào)用semop函數(shù)竟纳,其中的sem_op值為-1;釋放資源的時(shí)候镊辕,我們執(zhí)行同樣的函數(shù)但是其中的sem_op的值為+1。我們也可以為每一個(gè)操作指定SEM_UNDO蚁袭,以處理進(jìn)程結(jié)束而沒有釋放資源的情況征懈。
通過記錄鎖的技術(shù),我們創(chuàng)建一個(gè)空的文件揩悄,然后將文件的第一個(gè)字節(jié)做為鎖住的字節(jié)(不一定非得存在)卖哎。當(dāng)分配資源的時(shí)候,我們獲取一個(gè)在這個(gè)字節(jié)上面的寫鎖删性;釋放資源的時(shí)候亏娜,我們將這個(gè)字節(jié)解鎖。記錄鎖的特性可以保證如果一個(gè)進(jìn)程在持有鎖的時(shí)候結(jié)束了蹬挺,那么這個(gè)鎖會(huì)自動(dòng)被內(nèi)核釋放维贺。
下面的表格中,給出了Linux上面兩個(gè)技術(shù)的時(shí)間對比情況巴帮。每種情況下溯泣,資源被分配和釋放100,000此。通過三個(gè)不同的進(jìn)程同時(shí)進(jìn)行榕茧。表中的時(shí)間是所有三個(gè)進(jìn)程的總共秒數(shù)垃沦。
時(shí)間對比的表格
+--------------------------------------------------+
| Operation | User | System | Clock |
|--------------------------+------+--------+-------|
| semaphores with undo | 0.38 | 0.48 | 0.86 |
|--------------------------+------+--------+-------|
| advisory record locking | 0.41 | 0.95 | 1.36 |
+--------------------------------------------------+
在Linux上面,使用記錄鎖的時(shí)間會(huì)比信號量鎖的時(shí)間多60%用押。
盡管記錄鎖比信號量鎖要慢肢簿,如果我們只是對一個(gè)單個(gè)的資源加鎖(例如共享內(nèi)存段)并且不需要XSI信號量提供的高級功能的化,我們還是喜歡使用記錄鎖。原因就是記錄鎖非常容易被使用池充,并且系統(tǒng)會(huì)自動(dòng)處理進(jìn)程結(jié)束的時(shí)候的資源釋放等問題桩引。
譯者注
這里的信號量其實(shí)是一個(gè)信號量集合,集合中每個(gè)信號量都有包含信號量的數(shù)目以及權(quán)限信息收夸。
使用semget獲取或創(chuàng)建信號量集合的標(biāo)識
使用semctl初始化坑匠,semun類型參數(shù)用于獲取或者設(shè)置信號量集合的相應(yīng)值
使用semop對信號量集合中的一個(gè)或多個(gè)信號量進(jìn)行申請釋放,sembuf類型參數(shù)咱圆,包含UNDO標(biāo)記笛辟,以便進(jìn)程異常結(jié)束后信號量的清理。
每個(gè)信號量集合的結(jié)構(gòu)包含信號量數(shù)目序苏,每個(gè)信號量是匿名結(jié)構(gòu)包含值和等待情況手幢。
int semget(key_t key, int nsems, int flag)
中,創(chuàng)建的是一個(gè)信號量集忱详,而非單一信號量围来。權(quán)限是集合的權(quán)限;集合中每個(gè)信號量都有特定的值匈睁。int semctl(int semid, int semnum, int cmd, ... /* union semun arg */)
中监透, semnum表示是集合中的第semnum
個(gè)信號量。int semop(int semid, struct sembuf semoparray[], size_t nops)
中航唆,設(shè)置SEM_UNDO標(biāo)記并非改變信號操作胀蛮,而是額外記錄一個(gè)調(diào)整值以備異常。
記錄鎖性能不如信號量但是使用簡單糯钙。信號量每次創(chuàng)建一個(gè)信號量集合粪狼,而非單個(gè)信號量,在某些場景比如:需要的資源不止一種的時(shí)候任岸,可能維護(hù)起來會(huì)相對方便再榄,因?yàn)榧卸x資源到集合中了,而非單獨(dú)各自定義享潜。