信號量及信號量上的操做是E.W.Dijkstra 在1965年提出的一種解決同步、互斥問題的較通用的方法,並在不少操做系統中得以實現, Linux改進並實現了這種機制。linux
信號量 (semaphore )實際是一個整數,它的值由多個進程進行測試(test)和設置(set)。就每一個進程所關心的測試和設置操做而言,這兩個操做是不可中斷的,或稱「原 子」操做,即一旦開始直到兩個操做所有完成。測試和設置操做的結果是:信號量的當前值和設置值相加,其和或者是正或者爲負。根據測試和設置操做的結果,一 個進程可能必須睡眠,直到有另外一個進程改變信號量的值。數組
信號量可用來實現所謂的「臨界區」的互斥使用,臨界區指同一時刻只能有一個進程執行其中代碼的代碼段。爲了進一步理解信號量的使用,下面咱們舉例說明。數據結構
假設你有不少相 互協做的進程,它們正在讀或寫一個數據文件中的記錄。你可能但願嚴格協調對這個文件的存取,因而你使用初始值爲1的信號量,在這個信號量上實施兩個操做, 首先測試而且給信號量的值減1,而後測試並給信號量的值加1。當第一個進程存取文件時,它把信號量的值減1,並得到成功,信號量的值如今變爲0,這個進程 能夠繼續執行並存取數據文件。可是,若是另一個進程也但願存取這個文件,那麼它也把信號量的值減1,結果是不能存取這個文件,由於信號量的值變爲-1。 這個進程將被掛起,直到第一個進程完成對數據文件的存取。當第一個進程完成對數據文件的存取,它將增長信號量的值,使它從新變爲1,如今,等待的進程被喚 醒,它對信號量的減1操做將得到成功。ide
上述的進程互斥問 題,是針對進程之間要共享一個臨界資源而言的,信號量的初值爲1。實際上,信號量做爲資源計數器,它的初值能夠是任何正整數,其初值不必定爲0或1。另 外,若是一個進程要先得到兩個或多個的共享資源後才能執行的話,那麼,相應地也須要多個信號量,而多個進程要分別得到多個臨界資源後方能運行,這就是信號 量集合機制,Linux 討論的就是信號量集合問題。函數
1. 信號量的數據結構測試
Linux中信號 量是經過內核提供的一系列數據結構實現的,這些數據結構存在於內核空間,對它們的分析是充分理解信號量及利用信號量實現進程間通訊的基礎,下面先給出信號 量的數據結構(存在於include/linux/sem.h中),其它一些數據結構將在相關的系統調用中介紹。spa
(1)系統中每一個信號量的數據結構(sem)操作系統
struct sem {指針
int semval; /* 信號量的當前值 */對象
int sempid; /*在信號量上最後一次操做的進程識別號 *
};
(2)系統中表示信號量集合(set)的數據結構(semid_ds)
struct semid_ds {
struct ipc_perm sem_perm; /* IPC權限 */
long sem_otime; /* 最後一次對信號量操做(semop)的時間 */
long sem_ctime; /* 對這個結構最後一次修改的時間 */
struct sem *sem_base; /* 在信號量數組中指向第一個信號量的指針 */
struct sem_queue *sem_pending; /* 待處理的掛起操做*/
struct sem_queue **sem_pending_last; /* 最後一個掛起操做 */
struct sem_undo *undo; /* 在這個數組上的undo 請求 */
ushort sem_nsems; /* 在信號量數組上的信號量號 */
};
(3) 系統中每一信號量集合的隊列結構(sem_queue)
struct sem_queue {
struct sem_queue * next; /* 隊列中下一個節點 */
struct sem_queue ** prev; /* 隊列中前一個節點, *(q->prev) == q */
struct wait_queue * sleeper; /* 正在睡眠的進程 */
struct sem_undo * undo; /* undo 結構*/
int pid; /* 請求進程的進程識別號 */
int status; /* 操做的完成狀態 */
struct semid_ds * sma; /*有操做的信號量集合數組 */
struct sembuf * sops; /* 掛起操做的數組 */
int nsops; /* 操做的個數 */
};
(4)幾個主要數據結構之間的關係
從7.3圖能夠看出,semid_ds結構的sem_base指向一個信號量數組,容許操做這些信號量集合的進程能夠利用系統調用執行操做 。注意,信號量信號量集合的區別,從上面能夠看出,信號量用「sem」 結構描述,而信號量集合用「semid_ds"結構描述,實際上,在後面的討論中,咱們以信號量集合爲討論的主要對象。下面咱們給出這幾個結構之間的關係,如圖7.3所示。
Linux對信號量的這種實現機制,是爲了與消息和共享內存的實現機制保持一致,但信號量是這三者中最難理解的,所以咱們將結合系統調用作進一步的介紹,經過對系統調用的深刻分析,咱們能夠較清楚地瞭解內核對信號量的實現機制。
2. 系統調用:semget()
爲了建立一個新的信號量集合,或者存取一個已存在的集合,要使用segget()系統調用,其描述以下:
原型: int semget ( key_t key, int nsems, int semflg );
返回值: 若是成功,則返回信號量集合的IPC識別號
若是爲-1,則出現錯誤:
semget()中的第一個參數是鍵值, 這個鍵值要與已有的鍵值進行比較,已有的鍵值指在內核中已存在的其它信號量集合的鍵值。對信號量集合的打開或存取操做依賴於semflg參數的取值:
IPC_CREAT :若是內核中沒有新建立的信號量集合,則建立它。
IPC_EXCL :當與IPC_CREAT一塊兒使用時,但信號量集合已經存在,則建立失敗。
若是 IPC_CREAT單獨使用,semget()爲一個新建立的集合返回標識號,或者返回具備相同鍵值的已存在集合的標識號。若是IPC_EXCL與 IPC_CREAT一塊兒使用,要麼建立一個新的集合,要麼對已存在的集合返回-1。IPC_EXCL單獨是沒有用的,當與IPC_CREAT結合起來使用 時,能夠保證新建立集合的打開和存取。
做爲System V IPC的其它形式,一種可選項是把一個八進制與掩碼或,造成信號量集合的存取權限。
第二個參數nsems指的是在新建立的集合中信號量的個數。其最大值在「linux/sem.h」中定義:
#define SEMMSL 250 /* <= 8 000 max num of semaphores per id */
注意:若是你是顯式地打開一個現有的集合,則nsems參數能夠忽略。
下面舉例說明。
int open_semaphore_set( key_t keyval, int numsems )
{
int sid;
if ( ! numsems )
return(-1);
if((sid = semget( keyval, numsems, IPC_CREAT | 0660 )) == -1)
{
return(-1);
}
return(sid);
}
注意,這個例子顯式地用了0660權限。這個函數要麼返回一個集合的標識號,要麼返回-1而出錯。鍵值必須傳遞給它,信號量的個數也傳遞給它,這是由於若是建立成功則要分配空間。
3. 系統調用: semop()
原型: int semop ( int semid, struct sembuf *sops, unsigned nsops);
返回: 若是全部的操做都執行,則成功返回0。
若是爲-1,則出錯。
semop()中的第一個參數(semid)是集合的識別號(能夠由semget()系統調用獲得)。第二個參數(sops)是一個指針,它指向在集合上執行操做的數組。而第三個參數(nsop)是在那個數組上操做的個數。
sops參數指向類型爲sembuf的一個數組,這個結構在/inclide/linux/sem.h 中聲明,是內核中的一個數據結構,描述以下:
struct sembuf {
ushort sem_num; /* 在數組中信號量的索引值 */
short sem_op; /* 信號量操做值(正數、負數或0) */
short sem_flg; /* 操做標誌,爲IPC_NOWAIT或SEM_UNDO*/
};
若是sem_op爲負數,那麼就從信號量的值中減去sem_op的絕對值,這意味着進程要獲取資源,這些資源是由信號量控制或監控來存取的。若是沒有指定IPC_NOWAIT,那麼調用進程睡眠到請求的資源數獲得知足(其它的進程可能釋放一些資源)。
若是sem_op是正數,把它的值加到信號量,這意味着把資源歸還給應用程序的集合。
最後,若是sem_op爲0,那麼調用進程將睡眠到信號量的值也爲0,這至關於一個信號量到達了100%的利用。
綜上所 述,Linux 按以下的規則判斷是否全部的操做均可以成功:操做值和信號量的當前值相加大於 0,或操做值和當前值均爲 0,則操做成功。若是系統調用中指定的全部操做中有一個操做不能成功時,則 Linux 會掛起這一進程。可是,若是操做標誌指定這種狀況下不能掛起進程的話,系統調用返回並指明信號量上的操做沒有成功,而進程能夠繼續執行。若是進程被掛 起,Linux 必須保存信號量的操做狀態並將當前進程放入等待隊列。爲此,Linux 內核在堆棧中創建一個 sem_queue 結構並填充該結構。新的 sem_queue 結構添加到集合的等待隊列中(利用 sem_pending 和 sem_pending_last 指針)。當前進程放入 sem_queue 結構的等待隊列中(sleeper)後調用調度程序選擇其餘的進程運行。
爲了進一步解釋semop()調用,讓咱們來看一個例子。假設咱們有一臺打印機,一次只能打印一個做業。咱們建立一個只有一個信號量的集合(僅一個打印機),而且給信號量的初值爲1(由於一次只能有一個做業)。
每當咱們但願把一個做業發送給打印機時,首先要肯定這個資源是可用的,能夠經過從信號量中得到一個單位而達到此目的。讓咱們裝載一個sembuf數組來執行這個操做:
struct sembuf sem_lock = { 0, -1, IPC_NOWAIT };
從這個初始化結構 能夠看出,0表示集合中信號量數組的索引,即在集合中只有一個信號量,-1表示信號量操做(sem_op),操做標誌爲IPC_NOWAIT,表示或者調 用進程不用等待可當即執行,或者失敗(另外一個進程正在打印)。下面是用初始化的sembuf結構進行semop()系統調用的例子:
if((semop(sid, &sem_lock, 1) == -1)
fprintf(stderr,"semop\n");
第三個參數(nsops)是說咱們僅僅執行了一個操做(在咱們的操做數組中只有一個sembuf結構),sid參數是咱們集合的IPC識別號。
當咱們使用完打印機,咱們必須把資源返回給集合,以便其它的進程使用。
struct sembuf sem_unlock = { 0, 1, IPC_NOWAIT };
上面這個初始化結構表示,把1加到集合數組的第0個元素,換句話說,一個單位資源返回給集合。
4. 系統調用 : semctl()
原型: int semctl ( int semid, int semnum, int cmd, union semun arg );
返回值: 成功返回正數,出錯返回-1。
注意:semctl()是在集合上執行控制操做。
semctl()的第一個參數(semid)是集合的標識號,第二個參數(semnn)是將要操做的信號量個數,從本質上說,它是集合的一個索引,對於集合上的第一個信號量,則該值爲0。
·cmd參數表示在集合上執行的命令,這些命令及解釋如表7.2所示:
·arg參數的類型爲semun,這個特殊的聯合體在 include/linux/sem.h中聲明,對它的描述以下:
/* arg for semctl system calls. */
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
ushort *array; /* array for GETALL & SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
void *__pad;
};
表7.2 cmd命令及解釋
命令 |
解 釋 |
IPC_STAT |
從信號量集合上檢索semid_ds結構,並存到semun聯合體參數的成員buf的地址中 |
IPC_SET |
設置一個信號量集合的semid_ds結構中ipc_perm域的值,並從semun的buf中取出值 |
IPC_RMID |
從內核中刪除信號量集合 |
GETALL |
從信號量集合中得到全部信號量的值,並把其整數值存到semun聯合體成員的一個指針數組中 |
GETNCNT |
返回當前等待資源的進程個數 |
GETPID |
返回最後一個執行系統調用semop()進程的PID |
GETVAL |
返回信號量集合內單個信號量的值 |
GETZCNT |
返回當前等待100%資源利用的進程個數 |
SETALL |
與GETALL正好相反 |
SETVAL |
用聯合體中val成員的值設置信號量集合中單個信號量的值 |
這個聯合體中,有三個成員已經在表7-1中提到,剩下的兩個成員_buf 和_pad用在內核中信號量的實現代碼,開發者不多用到。事實上,這兩個成員是Linux操做系統所特有的,在UINX中沒有。
這個系統調用比較複雜,咱們舉例說明。
下面這個程序段返回集合上索引爲semnum對應信號量的值。當用GETVAL命令時,最後的參數(semnum)被忽略。
int get_sem_val( int sid, int semnum )
{
return( semctl(sid, semnum, GETVAL, 0));
}
關於信號量的三個系統調用,咱們進行了詳細的介紹。從中能夠看出,這幾個系統調用的實現和使用都和系統內核密切相關,所以,若是在瞭解內核的基礎上,再理解系統調用,相對要簡單地多,也深刻地多。
5. 死鎖
和信號量操做相關 的概念還有「死鎖」。當某個進程修改了信號量而進入臨界區以後,卻由於崩潰或被「殺死(kill)"而沒有退出臨界區,這時,其餘被掛起在信號量上的進程 永遠得不到運行機會,這就是所謂的死鎖。Linux 經過維護一個信號量數組的調整列表(semadj)來避免這一問題。其基本思想是,當應用這些「調整」時,讓信號量的狀態退回到操做實施前的狀態。
關於調整的描述是在sem_undo數據結構中,在include/linux/sem.h描述以下:
/*每個任務都有一系列的恢復(undo)請求,當進程退出時,自動執行undo請求*/
struct sem_undo {
struct sem_undo * proc_next; /*在這個進程上的下一個sem_undo節點 */
struct sem_undo * id_next; /* 在這個信號量集和上的下一個sem_undo節點*/
int semid; /* 信號量集的標識號*/
short * semadj; /* 信號量數組的調整,每一個進程一個*/
};
sem_undo結構也出如今task_struct數據結構中。
每個單獨的信號 量操做也許要請求獲得一次「調整」,Linux將爲每個信號量數組的每個進程維護至少一個sem_undo結構。若是請求的進程沒有這個結構,當必要 時則建立它,新建立的sem_undo數據結構既在這個進程的task_struct數據結構中排隊,也在信號量數組的semid_ds結構中排隊。當對 信號量數組上的一個信號量施加操做時,這個操做值的負數與這個信號量的「調整」相加,所以,若是操做值爲2,則把-2加到這個信號量的「調整」域。
當進程被刪除時,Linux完成了對sem_undo數據結構的設置及對信號量數組的調整。若是一個信號量集合被刪除,sem_undo結構依然留在這個進程的task_struct結構中,但信號量集合的識別號變爲無效。