資源共享是UNIX多用戶系統的一個重要特征,信號量(SEMAPHORE)則是防止兩個或多個進程同時訪問共享資源的一種機制。在信號量機制實現之前,通常采用加鎖文件的方法,其算法描述如下:
⑴加鎖算法
int lock(lockfile)
/*返回值0代表成功,其它為失敗*/
char *lockfile; /*加鎖文件名*/
{
intfd,ret=0;
extern int errno;
if((fd=open(lockfile,O_WRONLY|O_CREAT|O_EXCL,0666))==-1
&&errno==EEXIST) ret=1;
return(ret);
}
⑵解鎖算法
unlock(lockfile)
char *lockfile; /*鎖文件名*/
{
unlink(lockfile);
}
這種方法對訪問共享資源次數較少的進程是可行的,但對重載的使用則開銷太大了,況且一旦加鎖失敗則進程不知何時可以再試;當系統崩潰或重啟動時,加鎖文件可能會被忘掉了。
Dijkstra發表的Dekker算法給出了信號量的一種實現,為整值對象定義了兩個了原語操作:P和V。其C描述如下:
void P(sem)
int *sem;
{
while (*sem<=0);
(*sem)--;
}
void V(sem)
int *sem;
{
(*sem)++;
}
但上述算法不能在用戶空間編程,因為①sem指向的信號量變量不能在進程間共享,它們有自己的數據段;②函數非原子執行,內核可在任何時候中斷一個進程;③若sem為0,進程並不釋放CPU。
所以信號量必須由內核提供,它可在進程間共享數據,可執行原子操作(即一組操作要麼全部執行,要麼都不執行),可在一個進程阻塞時將CPU給另外一個進程。
UNIXSYSTEMV以一個長整數的鍵值作為信號量集合的唯一標識,信號量通常由下列元素組成:
①信號量的值,
②操作該信號量的最後一個進程的進程標識,
③等待增加該信號量的值的進程數,
④等待該信號量的值為0的進程數。
與之有關的系統調用如下:
#include
#include
#include
int semget(key,count,flags)
/*獲取信號量集合的標識符*/
key_tkey; /*信號量集合的鍵*/
intcount; /*信號量集合中元素個數*/
intflags; /*任選參數*/
/*返回信號量集合標識符,若出錯則返回-1*/
int semop(sid,ops,nops) /*信號量操作*/
int sid; /*信號量集合標識符*/
struct sembuf *ops; /*信號量操作結構的指針*/
intnops; /*信號量操作結構的個數*/
/*返回運算完成前該組信號量中最後一個被運算的信號量的
值,若出錯則返回-1*/
int semctl(sid,semnum,cmd,arg)
/*控制信號量操作*/
intsid; /*信號量集合標識符*/
intsemnum; /*信號量元素編號*/
intcmd; /*控制命令*/
union semun{
intval;
struct semid_ds *buf;
ushort*array;} arg; /*命令參數*/
系統調用semget用來把信號量集合的鍵值譯成代表信號量集合的標識符,該集合中有count個元素,其存取權限定義與文件相同,由flags定義。若flags的IPC_CREAT位被置位,則當該集合不存在時系統就創建之。因此各進程可都用置IPC_CREAT位的flags參數來獲取信號量集合的標識符,不需要由某一進程事先創建。若flags為IPC_PRIDVATE則不管同鍵值的信號量集合是否存在系統都建立之,並返回下一個可用的標識符。
系統調用semctl在一組信號量上做各種控制操作,諸如信號量集合的初始化、刪除和狀態查詢等。常用的操作及相關的命令格式如下:
①取消信號量集合
int semctl(sid,count,IPC_RMID,0)
int sid; /*信號量集合標識符*/
int count; /*信號量集合中元素個數*/
②設置信號量集合的初值(初始化)
信號量集合剛建立時,各信號量的初值不確定,需要設定初值。初值的設定可用SETALL或SETVAL命令。若用SETALL命令,其格式為:
int semctl(sid,count,SETALL,arg)
int sid; /*信號量集合標識符*/
int count; /*信號量集合中元素個數*/
ushort *arg; /*命令參數*/
該命令把數組arg中的前count個值依次賦給集合中各信號量,一次可設定多個信號量的初值。
若用SETVAL命令,其格式為:
int semctl(sid,semnum,SETVAL,arg)
int sid; /*信號量集合標識符*/
int semnum; /*信號量元素編號*/
int arg; /*命令參數*/
該命令將arg的值賦給集合中第semnum個信號量,一次僅能設定一個信號量的初值。
③查詢信號量集合的當前值
查詢信號量集合的當前值可用GETALL或GETVAL命令。若用GETALL命令,其格式為:
int semctl(sid,count,GETALL,arg)
int sid; /*信號量集合標識符*/
int count; /*信號量集合中元素個數*/
ushort *arg; /*命令參數*/
該命令把信號量集合中各信號量的當前值返回到數組arg中。
若用GETVAL命令,其格式為:
int semctl(sid,semnum,GETVAL,0)
int sid; /*信號量集合標識符*/
int semnum; /*信號量元素編號*/
該命令把集合中第semnum個信號量的當前值作為調用的返回值。
④查詢某個信號量的等待進程數
當一個進程要執行信號量操作時若條件不具備則被阻塞,有關信號量的等待進程數也相應變化。
通過GETNCNT命令可查詢等待信號量增值的進程數,其格式如下:
int semctl(sid,semnum,GETNCNT,0)
int sid; /*信號量集合標識符*/
int semnum; /*信號量元素編號*/
該命令把等待第semnum個信號量增值的進程數作為調用的返回值。
通過GETZCNT命令可查詢等待信號量值為0的進程數,其格式如下:
int semctl(sid,semnum,GETZCNT,0)
int sid; /*信號量集合標識符*/
int semnum; /*信號量元素編號*/
該命令把等待第semnum個信號量值為0的進程數作為調用的返回值。
至於其它的控制命令,因不常用而不再累述。
系統調用semop用來對信號量集合中的一個或多個信號量進行操作,操作命令由用戶提供的操作結構數組來定義,該結構如下:
struct sembuf{
short sem_num; /*信號量在集合中的下標*/
short sem_op; /*操作值*/
short sem_flg; /*操作標志*/
};
系統從用戶地址空間讀信號量操作結構數組,並核實信號量下標的合法性及進程是否具備讀或修改信號量所必需的權限。若權限不夠則調用失敗;若進程必須睡眠,則它將已操作過的信號量恢復為該系統調用開始時的值,然後它就睡眠,直到它等待的事件發生時再重新執行該系統調用。由於系統將操作數組保存在一個全局數組中,因此若它必須重新執行該調用的話,它必須重新從用戶空間讀該數組。這樣,操作按原語方式執行--或一次做完或根本不做。
系統根據操作值來改變信號量的值:①若操作值為正,系統就增加信號量的值並喚醒所有等待信號量增值的進程;②若操作值是0,系統就檢查信號量的值:如果為0,就繼續數組中的其它操作;否則把等待信號量的值為0的睡眠進程數加1,然後睡眠;③若操作值為負且其絕對值不超過信號量的值,系統就把操作值(一個負數)加到信號量值上,如果結果為0則系統就喚醒所有等待信號量的值為0的睡眠進程;④若信號量的值小於操作值的絕對值,系統就讓進程睡眠在"等待信號量增值"這一事件上。
當進程在信號量操作過程中睡眠時,它睡眠在可中斷級上,因此當它接收到軟中斷信號時就被喚醒了。用戶可在操作標志中設置IPC_NOWAIT標志以防止進程睡眠。
如果進程執行了一個信號量操作,鎖住了某些資源,卻沒有恢復信號量的值就退出了(如收到kill信號),那麼就可能出現危險情況。為了避免這類問題,用戶可在操作標志中設置SEM_UNDO標志。當進程退出時,系統便撤除該進程做過的每個信號量操作的影響。
值得指出的是,當你使用兩個或多個信號量時,死鎖總是可能的,系統並不能檢查多個信號量間的死鎖。
本文所用算法及調用格式均已在SCOUNIX3.2、SCOOpenSever3.X及5.X上運行通過。