標籤:linux
處理序間通訊的機制——訊號量。注意請不要把它與之前所說的訊號混淆起來,訊號與訊號量是不同的兩種事物。有關訊號的更多內容,可以閱讀我的另一篇文章:Linux處理序間通訊——使用訊號。下面就進入訊號量的講解。
一、什麼是訊號量為了防止出現因多個程式同時訪問一個共用資源而引發的一系列問題,我們需要一種方法,它可以通過產生並使用令牌來授權,在任一時刻只能有一個執行線程存取碼的臨界地區。臨界地區是指執行資料更新的代碼需要獨佔式地執行。而訊號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個線程在訪問它,也就是說訊號量是用來調協進程對共用資源的訪問的。
訊號量是一個特殊的變數,程式對其訪問都是原子操作,且只允許對它進行等待(即P(訊號變數))和發送(即V(訊號變數))資訊操作。最簡單的訊號量是只能取0和1的變數,這也是訊號量最常見的一種形式,叫做二進位訊號量。而可以取多個正整數的訊號量被稱為通用訊號量。這裡主要討論二進位訊號量。
二、訊號量的工作原理由於訊號量只能進行兩種操作等待和發送訊號,即P(sv)和V(sv),他們的行為是這樣的:
P操作 負責把當前進程由運行狀態轉換為阻塞狀態,直到另外一個進程喚醒它。
操作為:申請一個空閑資源(把訊號量減1),若成功,則退出;若失敗,則該進程被阻塞;
V操作 負責把一個被阻塞的進程喚醒,它有一個參數表,存放著等待被喚醒的進程資訊。
操作為:釋放一個被佔用的資源(把訊號量加1),如果發現有被阻塞的進程,則選擇一個喚醒之。
補充:查看共用資訊的記憶體的命令是ipcs [-m|-s|-q] (全部的話是ipcs -a) ;查看共用資訊的記憶體的命令是ipcs [-m|-s|-q]。
舉個例子,就是兩個進程共用訊號量sv,一旦其中一個進程執行了P(sv)操作,它將得到訊號量,並可以進入臨界區,使sv減1。而第二個進程將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會被掛起以等待第一個進程離開臨界地區並執行V(sv)釋放訊號量,這時第二個進程就可以恢複執行。
三、Linux的訊號量機制Linux提供了一組精心設計的訊號量介面來對訊號進行操作,它們不只是針對二進位訊號量,下面將會對這些函數進行介紹,但請注意,這些函數都是用來對成組的訊號量值進行操作的。它們聲明在標頭檔sys/sem.h中。
1、semget函數它的作用是建立一個新訊號量或取得一個已有訊號量,原型為:
int semget(key_t key, int num_sems, int sem_flags);
第一個參數key是整數值(唯一非零),不相關的進程可以通過它訪問一個訊號量,它代表程式可能要使用的某個資源,程式對所有訊號量的訪問都是間接的,程式先通過調用semget函數並提供一個鍵,再由系統產生一個相應的訊號標識符(semget函數的返回值),只有semget函數才直接使用訊號量鍵,所有其他的訊號量函數使用由semget函數返回的訊號量標識符。如果多個程式使用相同的key值,key將負責協調工作。
第二個參數num_sems指定需要的訊號量數目,它的值幾乎總是1。
第三個參數sem_flags是一組標誌,當想要當訊號量不存在時建立一個新的訊號量,可以和值IPC_CREAT做按位或操作。設定了IPC_CREAT標誌後,即使給出的鍵是一個已有訊號量的鍵,也不會產生錯誤。而IPC_CREAT | IPC_EXCL則可以建立一個新的,唯一的訊號量,如果訊號量已存在,返回一個錯誤。
semget函數成功返回一個相應訊號標識符(非零),失敗返回-1.
2、semop函數它的作用是改變訊號量的值,原型為:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id是由semget返回的訊號量標識符,sembuf結構的定義如下:
struct sembuf{ short sem_num;//除非使用一組訊號量,否則它為0 short sem_op;//訊號量在一次操作中需要改變的資料,通常是兩個數,一個是-1,即P(等待)操作, //一個是+1,即V(發送訊號)操作。 short sem_flg;//通常為SEM_UNDO,使作業系統跟蹤訊號, //並在進程沒有釋放該訊號量而終止時,作業系統釋放訊號量 };
3、semctl函數該函數用來直接控制訊號量資訊,它的原型為:
int semctl(int sem_id, int sem_num, int command, ...);
如果有第四個參數,它通常是一個union semum結構,定義如下:
union semun{ int val; struct semid_ds *buf; unsigned short *arry; };
前兩個參數與前面一個函數中的一樣,command通常是下面兩個值中的其中一個SETVAL:用來把訊號量初始化為一個已知的值。p 這個值通過union semun中的val成員設定,其作用是在訊號量第一次使用前對它進行設定。IPC_RMID:用於刪除一個已經無需繼續使用的訊號量標識符。
四、進程使用訊號量通訊下面使用一個例子來說明進程間如何使用訊號量來進行通訊,這個例子是兩個相同的程式同時向螢幕輸出資料,我們可以看到如何使用訊號量來使兩個進程協調工作,使同一時間只有一個進程可以向螢幕輸出資料。注意,如果程式是第一次被調用(為了區分,第一次調用程式時帶一個要輸出到螢幕中的字元作為一個參數),則需要調用set_semvalue函數初始化訊號並將message字元設定為傳遞給程式的參數的第一個字元,同時第一個啟動的進程還負責訊號量的刪除工作。如果不刪除訊號量,它將繼續在系統中存在,即使程式已經退出,它可能在你下次運行此程式時引發問題,而且訊號量是一種有限的資源。
在main函數中調用semget來建立一個訊號量,該函數將返回一個訊號量標識符,儲存於全域變數sem_id中,然後以後的函數就使用這個標識符來訪問訊號量。
建立一個項目測試一下:semun.h
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED) /* union semun is defined by including <sys/sem.h> */#else /* according to X/OPEN we have to define it ourselves */ union semun { int val; /* value for SETVAL */ struct semid_ds *buf; /* buffer for IPC_STAT, IPC_SET */ unsigned short int *array; /* array for GETALL, SETALL */ struct seminfo *__buf; /* buffer for IPC_INFO */ };#endif
MySem.C source code:
/* After the #includes, the function prototypes and the global variable, we come to the main function. There the semaphore is created with a call to semget, which returns the semaphore ID. If the program is the first to be called (i.e. it‘s called with a parameter and argc > 1), a call is made to set_semvalue to initialize the semaphore and op_char is set to X. */#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <sys/sem.h>#include "semun.h"static int set_semvalue(void);static void del_semvalue(void);static int semaphore_p(void);static int semaphore_v(void);static int sem_id;int main(int argc, char *argv[]){ int i; int pause_time; char op_char = ‘O‘; srand((unsigned int)getpid()); sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); if (argc > 1) { if (!set_semvalue()) { fprintf(stderr, "Failed to initialize semaphore\n"); exit(EXIT_FAILURE); } op_char = ‘X‘; sleep(2); }/* Then we have a loop which enters and leaves the critical section ten times. There, we first make a call to semaphore_p which sets the semaphore to wait, as this program is about to enter the critical section. */ for(i = 0; i < 10; i++) { if (!semaphore_p()) exit(EXIT_FAILURE); printf("%c", op_char);fflush(stdout); pause_time = rand() % 3; sleep(pause_time); printf("%c", op_char);fflush(stdout);/* After the critical section, we call semaphore_v, setting the semaphore available, before going through the for loop again after a random wait. After the loop, the call to del_semvalue is made to clean up the code. */ if (!semaphore_v()) exit(EXIT_FAILURE); pause_time = rand() % 2; sleep(pause_time); } printf("\n%d - finished\n", getpid()); if (argc > 1) { sleep(10); del_semvalue(); } exit(EXIT_SUCCESS);}/* The function set_semvalue initializes the semaphore using the SETVAL command in a semctl call. We need to do this before we can use the semaphore. */static int set_semvalue(void){ union semun sem_union; sem_union.val = 1; if (semctl(sem_id, 0, SETVAL, sem_union) == -1) return(0); return(1);}/* The del_semvalue function has almost the same form, except the call to semctl uses the command IPC_RMID to remove the semaphore‘s ID. */static void del_semvalue(void){ union semun sem_union; if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) fprintf(stderr, "Failed to delete semaphore\n");}/* semaphore_p changes the semaphore by -1 (waiting). */static int semaphore_p(void){ struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = -1; /* P() */ sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphore_p failed\n"); return(0); } return(1);}/* semaphore_v is similar except for setting the sem_op part of the sembuf structure to 1, so that the semaphore becomes available. */static int semaphore_v(void){ struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = 1; /* V() */ sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphore_v failed\n"); return(0); } return(1);}
開啟兩個進程,編譯後輸出:
XX00XXOOXX00XXOOXX00XXOO……