所謂信號量集,就是由多個信號量組成的一個數組。作爲一個整體,信號量集中的所有信號量使用同一個等待隊列。Linux的信號量集爲進程請求多個資源創造了條件。Linux規定,當進程的一個操作需要多個共享資源時,如果只成功獲得了其中的部分資源,那麼這個請求即告失敗,進程必須立即釋放所有已獲得資源,以防止形成死鎖。
信號量集的結構
信號量結構
描述信號量的內核數據結構如下:
struct sem {
int semval; /* 信號量的當前值 */
int sempid; /* 上一次操作本信號的進程PID */
};
其中,域semval爲一個整型變量,表示相應共享資源的被佔用情況;域sempid則記錄了上一次使用這個信號量的進程的標識號。
信號量集的結構
如果把若干個信號量組成一個數組sem[],那麼這個數組就是信號量集。使用信號量集可以同時把多個共享資源設置爲互斥資源。
Linux用一個數組頭來管理這個信號量集,它包含信號量集的所有基本信息。
數組頭的結構sem_array如下:
struct sem_array {
struct kern_ipc_perm sem_perm; /* IPC許可結構 */
time_t sem_otime; /* 上一次信號量的操作時間 */
time_t sem_ctime; /* 信號量變化時間 */
struct sem *sem_base; /* 指向信號量數組的指針 */
struct list_head sem_pending; /* 等待隊列 */
struct list_head list_id; /* undo結構 */
unsigned long sem_nsems; /* 信號量集裏面信號量的數目 */
};
結構的第一個域sem_perm爲檢查用戶權限的許可結構。數組頭結構中的指針sem_base指向信號量數組,該數組中的每一個元素都是sem結構的變量,即信號量。
一個信號量集的結構如下圖所示:
從上圖可以看出,信號量集統一有一個進程等待隊列,而不是每個信號量都有一個,這正是信號量集的特點。
進程等待隊列結構sem_queue如下:
struct sem_queue {
struct list_head list; /* queue of pending operations */
struct task_struct *sleeper; /* 指向等待進程控制塊的指針 */
struct sem_undo *undo; /* undo請求操作結構指針 */
int pid; /* 請求操作的進程標識 */
int status; /* 操作完成狀態 */
struct sembuf *sops; /* 掛起的操作集 */
int nsops; /* 操作數目 */
int alter; /* does the operation alter the array? */
};
等待隊列是一個由進程控制塊所組成的隊列,每個進程控制塊代表着一個等待進程,sem_queue的域爲sleeper指向了該等待隊列。
另外,爲了使系統可以從等待進程控制塊中得到該進程所在的等待隊列,進程控制塊task_struct中有一個指向等待隊列的指針semsleeping。
內核管理結構
Linux系統所有的信號量集都註冊在一個數組中,該數組是內核全局數據結構struct ipc_id_ary的一個域。結構struct ipc_id_ary的定義如下:
struct ipc_id_ary
{
int size;
struct kern_ipc_perm *p[0]; //存放段描述結構的數組
};
結構中的數組p[]就是信號量集的註冊數組。
數組p[]暫時只定義了0個元素,數組的長度在系統運行時會在相應的操作裏動態地增加或減少。
爲了方便對上述數組進行管理,Linux又定義了一個數組頭struct ipc_ids。struct ipc_ids的定義如下:
struct ipc_ids {
int in_use;
unsigned short seq;
unsigned short seq_max;
struct rw_semaphore rw_mutex;
struct idr ipcs_idr;
struct ipc_id_ary *entries; //指向struct ipc_id_ary的指針
};
很清楚,域entries就是指向信號量集數組的指針。
另外需要注意的是,由於爲了充分利用內存空間,進程消亡時需要及時釋放其所創建的信號量集,所以數組p[]的下標是動態的。這也就意味着以信號量在數組中的位置(下標)來作爲標識不唯一,因此在上述結構中有一個叫做序列號的域seq,系統每增加一個信號量集,系統就會將seq加1,然後把信號量集在數組p[]中的下標與之拼接起來形成唯一的標識,以供內核來識別。
內核對於信號量集的管理結構如下圖所示:
信號量集的操作
信號量集的創建或打開
進程可以通過調用函數semget()創建或打開一個信號量集,這個函數是通過系統調用sys_semget()來實現的。系統調用sys_semget()的原型如下:
asmlinkage long sys_semget(key_t key, int nsems, int semflg);
其中,參數key是用戶給定的鍵值;參數semflg是該函數的功能標誌。
系統調用sys_semget()有兩個功能:如果參數semflg的IPC_CREATE的值給定爲1,則這個系統調用會爲用戶創建或打開一個信號量集,並返回信號量集標識符;如果爲0,則會在系統已有的信號量集中尋找與鍵值相同的信號量集,找到後,打開該信號量集並返回信號量集的標識號。參數nsems用來指明在所創建的信號量集中信號量的個數,即定義sem_base指向的數組的大小。
信號量集的操作
用於信號量操作的函數是semop()。爲了用戶的方便,Linux提供了數據結構sembuf,用戶在這個數據結構中指明對信號量的操作。sembuf結構定義如下:
struct sembuf {
unsigned short sem_num; /* 信號量集在集中的序號 */
short sem_op; /* 信號量操作 */
short sem_flg; /* 操作標誌 */
};
其中,域sem_num指明待操作信號量在集中的位置;域sem_op就是對信號量的增量。通常,在訪問共享資源之前,域sem_op應設爲-1(對信號量進行減1的P操作);訪問之後,設爲1(對信號量進行加1的V操作)。
前面講過,爲了防止產生死鎖,信號量集的操作必須對集中的所有信號量同時操作,所以用戶還需要定義一個其長度與信號量數目相等的sembuf類型數組,以便把各個信號量的sembuf結構數據存放到對應的元素中。
函數semop()由系統調用sys_semop()實現,其原型如下:
asmlinkage long sys_semop(int semid, struct sembuf __user *sops,
unsigned nsops);
其中,參數semid爲信號量集的標識;參數sops就是指向上述sembuf數組的指針,數組每個元素都是對應信號量集的操作結構sembuf;參數nspos爲這個數組的長度。
結構undo
介紹信號量的基本原理時曾經說過,P和V操作必須成對出現。也就是說,對於Linux信號量集,在臨界段前要用semop()來請求資源,而在臨界段後要用semop()來釋放資源,但在具體應用中可能會因進程非正常中止而導致臨界段沒有機會來釋放資源。
如果有產生這種情況的可能,進程必須將釋放資源的任務轉交給內核來完成。即在調用semop()請求資源時,把傳遞給函數的sembuf結構的域sem_flg設置爲SEM_UNDO。這樣,函數semop()在執行時就會爲信號量配置一個sem_undo的結構,並在該結構中記錄釋放信號量的調整值;然後把信號量集中所有sem_undo組成一個隊列,並在等待進程隊列中用指針undo指向該隊列。
也就是說,通過設置SEM_UNDO,當進程非正常中止時內核會產生響應操作,以保證信號量處於正常狀態。
結構sem_undo定義如下:
struct sem_undo {
struct list_head list_proc; /* per-process list: all undos from one process. */
/* rcu protected */
struct rcu_head rcu; /* rcu struct for sem_undo() */
struct sem_undo_list *ulp; /* sem_undo_list for the process */
struct list_head list_id; /* per semaphore array list: all undos for one array */
int semid; /* 信號量集標識符 */
short * semadj; /* 存放信號量集調整值的數組指針 */
};
於是,當系統執行內核函數do_ext()結束一個進程時,如果sembuf結構中的sem_flg的值爲SEM_UNDO,則該函數會掃描該進程的sem_undo隊列,並根據每個sem_undo結構中的調整信息,依次調整各個信號量值,以釋放各個信號量。
信號量的控制
爲實現對信號量的初始化等控制,Linux提供了函數semctl()。其對應的內核函數原型如下:
asmlinkage long sys_semctl(int semid, int semnum, int cmd, union semun arg);
其中,semid爲信號量集的表示;semnum爲信號量的數目;cmd爲操作命令;arg爲信號量的初始值。
從參數定義中可知,arg的類型爲union semun。該類型定義如下:
union semun {
int val; /* 信號量初始值 */
struct semid_ds __user *buf; /* buffer for IPC_STAT & IPC_SET */
unsigned short __user *array; /* array for GETALL & SETALL */
struct seminfo __user *__buf; /* buffer for IPC_INFO */
void __user *__pad;
};
內核函數sys_semctl()將根據其命令參數cmd(第三個參數)及參數arg來對信號量集實時控制。
進程控制塊中關於信號量集的域
進程使用信號量集的相關信息也被記錄在進程控制塊中。進程控制塊與信號量及相關的域如下:
struct task_struct
{
...
struct sem_undo * semundo; //指向進程使用的信號量集undo隊列
struct sem_queue * semsleeping; //指向進程所在等待隊列的指針
...
};
其實就是兩個指針:一個指向進程使用的信號量undo隊列;另一個指向進程所在的等待隊列。
特別地,信號量集的undo隊列被組織在進程控制塊和信號量集兩個隊列中,如下圖所示: