Linux 進程間通信 - 信號燈

信號燈(Semaphores)
 
    一般意義下,信號燈是一個具有整數值的對象,它支持兩種操作P()和V()。P()操作減少信號燈的值,如果新的信號燈的值小於0,則操作阻塞;V()操作增加信號燈的值,如果結果值大於或等於0,則喚醒一個等待的進程。通常用信號燈來做進程的同步和互斥。
 
    最簡單形式的信號燈就是內存中一個存儲位置,它的取值可以由多個進程檢驗和設置。至少對於相關的進程來講,對信號燈的檢驗和設置操作是不可中斷的或者說是原子的:只要啓動就不能終止。目前許多處理器提供檢驗和設置操作指令,如Intel處理器的sete等指令。檢驗和設置操作的結果是信號燈當前值與設置值的和,可以是正或者負。根據檢驗和設置操作的結果,一個進程可能必須睡眠直到信號燈的值被另一個進程改變。信號燈可以用於實現臨界區(critical regions),就是重要的代碼區,同一時刻只能有一個進程運行的代碼區域。
 
    比如,有許多協作的進程要從同一個數據文件中讀寫記錄,並且希望對文件的訪問必須嚴格地協調。那麼,可以使用一個信號燈,將其初值設爲1,用兩個信號燈操作(P、V 操作),將進程中對文件操作的代碼括起來。第一個信號燈操作檢查並把信號燈的值減小,第二個操作檢查並增加它。訪問文件的第一個進程試圖減小信號燈的值,如果它成功(事實上,它肯定成功),信號燈的取值將變爲0,這個進程現在可以繼續運行並使用數據文件。但是,如果此時另一個進程需要使用這個文件,它也試圖減少信號燈的數值,它會失敗,因爲信號燈的值將要變成-1(但是,信號燈的值仍然保持爲0,沒有變成-1),這個進程會被掛起直到第一個進程處理完數據文件。當第一個進程處理完數據文件後,它會增加信號燈的值使其重新變爲1。現在等待的進程會被喚醒,這次它減小信號燈的嘗試會成功。

 
圖 System V IPC 機制 - 信號燈
 
    與消息隊列相似,Linux爲系統中所有的信號燈維護一個向量表semary ,其定義如下:
struct semid_ds *semary[SEMMNI];
 
    該向量表中的每個元素都是一個指向semid_ds數據結構的指針,而一個semid_ds數據結構則描述了一個信號燈數組。SEMMNI的值是128,它限制了系統中同時存在的信號燈的數組的數量。數據結構semid_ds的定義如下:
struct semid_ds {
    struct ipc_perm sem_perm;            /* permissions .. see ipc.h */
    __kernel_time_t sem_otime;           /* last semop time */
    __kernel_time_t sem_ctime;           /* last change time */
    struct sem *sem_base;                /* ptr to first semaphore in array */
    struct sem_queue *sem_pending;       /* pending operations to be processed */
    struct sem_queue **sem_pending_last; /* last pending operation */
    struct sem_undo *undo;               /* undo requests on this array */
    unsigned short sem_nsems;            /* no. of semaphores in array */
};
 
    其中:sem_perm是一個認證;
    sem_otime是最後一次在該信號燈上執行操作的時間;
    sem_ctime是信號燈最後一次改變的時間;
    sem_base是一個信號燈數組,該數組中的每個元素都是一個信號燈;
    sem_pending和sem_pending_last描述了一個等待隊列;
    undo調整(undo)序列;
    sem_nsems該信號燈數組中的信號燈數。
 
    由此可見,每一個semid_ds數據結構都表示一個信號燈對象,而System V IPC的每一個信號燈對象都描述了一個信號燈數組。每一個信號燈數組(semid_ds)中都有一個域sem_nsems,表示該信號燈數組中信號燈的數量。信號燈由sem數據結構描述。sem_nsems個sem數據結構組成一個信號燈數組,sem_base域指向該數組。
struct sem {
    int semval;    /* current value */
    int sempid;    /* pid of last operation */
};
 
    Linux在信號燈上提供三種操作。
    1. 象消息隊列一樣,進程在使用信號燈以前必須創建信號燈或獲得對已存在信號燈的引用標識符,該工作通過系統調用sys_ipc實現,其中的call值爲SEMGET。具體實現該操作的函數是sys_semget,其定義如下:
int sys_semget (key_t key, int nsems, int semflg)
 
    該函數根據所給的鍵值(key)創建或獲得一個信號燈引用標識符,它所做的工作如下:
    1) 如果key == IPC_PRIVATE,則:
    * 在數組semary中找一個空位置。
    * 申請一塊內存,其大小爲sizeof (struct semid_ds) + nsems * sizeof (struct sem),即一個semid_ds數據結構的大小加上nsems個信號燈的大小。該塊內存區的前部用做semid_ds數據結構,剩餘部分用做sem數組。填寫semid_ds數據結構數據結構的各個域。將填寫好的semid_ds數據結構加入到數組semary的空位置。喚醒在sem_lock上等待的進程。
    * 返回該信號燈的引用標識符。
 
    2) 在數組semary上查找鍵值爲key的信號燈對象(semid_ds數據結構)。有下面結果:
    A. 沒找到。如果在參數semflg中指明要創建新的信號燈,則如1)所述,創建一個新的信號燈。否則,錯誤返回。
    B. 找到。如果在參數semflg中指明要創建新的信號燈,而且該種鍵值的信號燈不能存在,則錯誤返回。否則,認證檢查,如有錯,則錯誤返回。否則,返回找到的信號燈的引用標識符。
 
    2. 所有允許在一個System V IPC信號燈對象的信號燈數組上操作的進程,都可以通過系統調用sys_ipc對它們操作,其對應的call值爲SEMOP,對應的函數爲sys_semop。對信號燈的操作,Linux只定義了一個函數,其定義如下:
int sys_semop (int semid, struct sembuf *tsops, unsigned nsops)
 
    其中semid是信號燈引用標識符,tsops是在該信號燈上要求執行的一組操作,nsops是此次要執行的操作的個數。每一個操作都用如下的一個數據結構表示:
struct sembuf {
    unsigned short sem_num;  /* semaphore index in array */
    short sem_op;            /* semaphore operation */
    short sem_flg;           /* operation flags */
};
 
    其中sem_num是信號燈數組的一個索引,指明要做該操作的一個具體的信號燈。
    sem_op是一個操作值,是要加到當前信號燈上的數值。
    sem_flg是一個標誌,表示對該次操作的特別要求。
 
    一個sembuf數據結構表示對一個信號燈的一次操作,sembuf數據結構數組tsops表示對一個信號燈對象上的信號燈數組要做的一組操作。
 
    函數sys_semop就是要在信號燈對象(由semid表示)上完成由tsops指定的一組操作。它所做的工作如下:
    1) 檢查參數nsops和semid的合法性。
    2) 將tsops數組拷貝到內核。
    3) 算出引用標識符semid在數組semary中對應的索引:
id = (unsigned int) semid % SEMMNI
    4) 檢查統計操作數組tsops中指定的各個操作(合法、加、減)。
    5) 認證檢查。
    6) Linux維護對信號燈數組的調整(undo)序列(每個信號燈對象一個)來避免可能出現的死鎖現象。基本思路是,如果實施了這些調整,信號燈就會返回到進程對其實施操作以前的狀態。這些調整被放在sem_undo數據結構中,排在 semid_ds 數據結構的隊列中(由 undo 域指示),同時也排在使用這些信號燈的進程的task_struct數據結構的隊列中。
 
    sem_undo數據結構的定義如下:
struct sem_undo {
    struct sem_undo *proc_next; /* next entry on this process */
    struct sem_undo *id_next;  /* next entry on this semaphore set */
    int semid;                 /* semaphore set identifier */
    short * semadj;            /* array of adjustments, one per semaphore */
};
 
    其中semid是該結構所對應的信號燈對象。semadj是一個數組,信號燈對象的每個信號燈對應該數組的一個元素,其中記錄對信號燈的累計操作的結果(在信號燈上增加或減少數值的總和取反)。
 
    如果一個信號燈操作(sembuf數據結構)的標誌sem_flg中指明SEM_UNDO標誌,則在做該信號燈操作的同時還要維護一個調整動作。Linux至少爲每一個進程的每一個信號燈對象都維護一個sem_undo數據結構。如果請求的進程沒有該結構,就在需要的時候爲它創建一個。這個新的sem_undo數據結構同時在進程的task_struct數據結構和信號燈隊列的semid_ds數據結構的相應隊列中排隊。對信號燈隊列中的信號燈執行 undo 操作的時候,和原操作值相反的值(負值)被加到該信號燈上,這個操作值記錄在進程的sem_undo數據結構的調整隊列中該信號燈對應的條目上。所以,如果操作值爲2,那麼這個信號燈的調整條目上記錄的是-2。
 
    當進程被刪除,比如退出的時候,Linux遍歷它的sem_undo數據結構數組,並實施對於信號燈數組的調整。如果信號燈被刪除,它的sem_undo數據結構仍舊保留在進程的task_struct隊列中,但是相應的信號燈數組標識符被標記爲無效。在這種情況下,信號燈清除代碼只是簡單地廢棄這個sem_undo數據結構。
 
    函數sys_semop在此處檢查該次操作有沒有undo操作,如果有undo操作,則看當前進程有沒有就該信號燈對象定義sem_undo數據結構,如沒有定義,則申請一塊內存,爲其定義一個sem_undo數據結構,並將其加入到進程和信號燈對象的相應隊列中。
 
    7) 在信號燈對象上做tsops中指定的所有操作(增加指定信號燈的數值,對標誌sem_flg中指明SEM_UNDO的操作記錄其undo數據等)。只有操作數加上信號燈的當前值大於0或者操作值和信號燈的當前值都是0時,操作纔算成功。
 
    8) 如果任意一個信號燈操作失敗,只要操作標記不要求系統調用無阻塞返回,Linux就會掛起這個進程。如果進程要掛起,Linux必須保存要進行的信號燈操作的狀態並把當前進程放到等待隊列中。Linux在堆棧中爲每一個有進程等待的信號燈建立一個sem_queue數據結構,並填滿它來實現上述過程(保存信號燈操作狀態、移動進程隊列等)。
struct sem_queue {
    struct sem_queue * next;    /* next entry in the queue */
    struct sem_queue ** prev;   /* previous entry in the queue, *(q->prev) == q */
    struct wait_queue * sleeper;  /* sleeping process */
    struct sem_undo * undo;       /* undo structure */
    int pid;               /* process id of requesting process */
    int status;            /* completion status of operation */
    struct semid_ds *sma;  /* semaphore array for operations */
    struct sembuf *sops;   /* array of pending operations */
    int nsops;             /* number of operations */
    int alter;             /* operation will alter semaphore */
};
 
    這個新創建的sem_queue數據結構被放到了該信號燈對象的等待隊列的結尾(由semid_ds數據結構中的sem_pending和sem_pending_last指針指出)。當前進程被放到了這個sem_queue數據結構的等待隊列中(由它的sleeper指針指出),然後調用調度程序,運行另外一個進程。當該進程被喚醒時,再次執行指定的那組操作,如果此次成功,則將進程從等待隊列中摘下,正常返回。
 
    9) 如果所有的信號燈操作都成功,當前的進程就不需要被掛起。Linux繼續向前並把這些操作施加到信號燈數組的合適的成員上。現在Linux必須檢查任何睡眠或者掛起的進程,它們的操作現在可能可以實施。Linux順序查找在該信號燈上的操作等待隊列(由sem_pending指出)中的每一個成員,檢查它的信號燈操作現在是否可以成功。如果可以,它就把這個sem_queue數據結構從操作等待表中刪除,並把這種信號燈操作施加到信號燈數組的合適的成員上。它喚醒睡眠的進程,讓它在下次調度程序運行的時候可以繼續運行。Linux從頭到尾檢查操作等待隊列,直到無法執行信號燈操作,從而無法喚醒更多的進程爲止。
    參見include/linux/sem.h,linux/ipc/sem.c
 
    3. Linux在信號燈上實現的第三種操作是對信號燈的控制(call值爲SEMCTL的sys_ipc調用),它由函數sys_semctl實現。控制操作包括獲得信號燈的狀態、獲得信號燈的值,設置信號燈的值,釋放信號燈對象資源等。
 
    信號燈機制爲互相操作的進程提供了一種複雜的同步方法。但信號燈也存在一些問題,如死鎖(deadlock)。這發生在一個進程改變了信號燈的值,從而進入一個臨界區(critical region),但是因爲崩潰或者被kill而沒有離開這個臨界區域的情況下。Linux使用undo操作來避免死鎖,如上所述。另一個問題是所有System V IPC機制同時存在的問題,即必須顯式地釋放IPC資源,如果某進程忘記釋放某IPC資源,則會造成內存垃圾。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章