信號量及信號量上的操作是E.W.Dijkstra 在1965年提出的一種解決同步、互斥問題的較通用的方法,並在很多操作系統中得以實現, Linux改進並實現了這種機制。
信號量(semaphore )實際是一個整數,它的值由多個進程進行測試(test)和設置(set)。就每個進程所關心的測試和設置操作而言,這兩個操作是不可中斷的,或稱“原子”操作,即一旦開始直到兩個操作全部完成。測試和設置操作的結果是:信號量的當前值和設置值相加,其和或者是正或者為負。根據測試和設置操作的結果,一個進程可能必須睡眠,直到有另一個進程改變信號量的值。
信號量可用來實現所謂的“臨界區”的互斥使用,臨界區指同一時刻只能有一個進程執行其中代碼的代碼段。為了進一步理解信號量的使用,下面我們舉例說明。
假設你有很多相互協作的進程,它們正在讀或寫一個數據文件中的記錄。你可能希望嚴格協調對這個文件的存取,於是你使用初始值為1的信號量,在這個信號量上實施兩個操作,首先測試並且給信號量的值減1,然后測試並給信號量的值加1。當第一個進程存取文件時,它把信號量的值減1,並獲得成功,信號量的值現在變為0,這個進程可以繼續執行並存取數據文件。但是,如果另外一個進程也希望存取這個文件,那么它也把信號量的值減1,結果是不能存取這個文件,因為信號量的值變為-1。這個進程將被掛起,直到第一個進程完成對數據文件的存取。當第一個進程完成對數據文件的存取,它將增加信號量的值,使它重新變為1,現在,等待的進程被喚醒,它對信號量的減1操作將獲得成功。
上述的進程互斥問題,是針對進程之間要共享一個臨界資源而言的,信號量的初值為1。實際上,信號量作為資源計數器,它的初值可以是任何正整數,其初值不一定為0或1。另外,如果一個進程要先獲得兩個或多個的共享資源后才能執行的話,那么,相應地也需要多個信號量,而多個進程要分別獲得多個臨界資源后方能運行,這就是信號量集合機制,Linux 討論的就是信號量集合問題。
1. 信號量的數據結構
Linux中信號量是通過內核提供的一系列數據結構實現的,這些數據結構存在於內核空間,對它們的分析是充分理解信號量及利用信號量實現進程間通信的基礎,下面先給出信號量的數據結構(存在於include/linux/sem.h中),其它一些數據結構將在相關的系統調用中介紹。
(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所示。
圖7.3 System V IPC信號量數據結構之間的關系
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結構中,但信號量集合的識別號變為無效。
原文:http://oss.org.cn/kernel-book/ch07/7.3.1.htm
