本文繼《System V IPC 之共享內存》之后接着介紹 System V IPC 的信號量編程。在開始正式的內容前讓我們先概要的了解一下 Linux 中信號量的分類。
信號量的分類
在學習 IPC 信號量之前,讓我們先來了解一下 Linux 提供兩類信號量:
- 內核信號量,由內核控制路徑使用。
- 用戶態進程使用的信號量,這種信號量又分為 POSIX 信號量和 System V 信號量。
POSIX 信號量與 System V 信號量的區別如下:
- 對 POSIX 來說,信號量是個非負整數,常用於線程間同步。而 System V 信號量則是一個或多個信號量的集合,它對應的是一個信號量結構體,這個結構體是為 System V IPC 服務的,信號量只不過是它的一部分,常用於進程間同步。
- POSIX 信號量的引用頭文件是 "<semaphore.h>",而 System V 信號量的引用頭文件是 "<sys/sem.h>"。
- 從使用的角度,System V 信號量的使用比較復雜,而 POSIX 信號量使用起來相對簡單。
本文介紹 System V 信號量編程的基本內容。
System V IPC 信號量
信號量是一種用於對多個進程訪問共享資源進行控制的機制。共享資源通常可以分為兩大類:
- 互斥共享資源,即任一時刻只允許一個進程訪問該資源
- 同步共享資源,即同一時刻允許多個進程訪問該資源
信號量是為了解決互斥共享資源的同步問題而引入的機制。信號量的實質是整數計數器,其中記錄了可供訪問的共享資源的單元個數。本文接下來提到的信號量都特指 System V IPC 信號量。
當有進程要求使用某一資源時,系統首先要檢測該資源的信號量,如果該資源的信號量的值大於 0,則進程可以使用這一資源,同時信號量的值減 1。進程對資源訪問結束時,信號量的值加 1。如果該資源信號量的值等於 0,則進程休眠,直至信號量的值大於 0 時進程被喚醒,訪問該資源。
信號量中一種常見的形式是雙態信號量。雙態信號量對應於只有一個可供訪問單元的互斥共享資源,它的初始值被設置為 1,任一時刻至多只允許一個進程對資源進行訪問。
信號量用於實現對任意資源的鎖定機制。它可以用來同步對任何共享資源的訪問。
相關數據結構
System V 子系統提供的信號量機制是比較復雜的。我們不能單獨定義一個信號量,而只能定義一個信號量集,其中包括一組信號量,同一信號量集中的信號量可以使用同一 ID 引用。每個信號量集都有一個與其相對應的結構,其中包含了信號量集的各種信息,該結構的聲明如下:
struct semid_ds { struct ipc_perm sem_perm; struct sem *sem_base; ushort sem_nsems; time_t sem_otime; time_t sem_ctime; };
下面簡單介紹一下 semid_ds 結構中字段的含義。
sem_perm:對應於該信號量集的 ipc_perm 結構(該結構的詳情請參考《System V IPC 之內存共享》)指針。
sem_base:sem 結構指針,指向信號量集中第一個信號量的 sem 結構。
sem_nsems:信號量集中信號量的個數。
sem_otime:最近一次調用 semop 函數的時間。
sem_ctime:最近一次改變該信號量集的時間。
sem 結構記錄了一個信號量的信息,其聲明如下:
struct sem { ushort semval; /* 信號量的值 */ pid_t sempid; /* 最后一次返回該信號量的進程ID 號 */ ushort semncnt; /* 等待可利用資源出現的進程數 */ ushort semzcnt; /* 等待全部資源可被獨占的進程數 */ };
與信號量相關的函數
信號量集的創建與打開
要使用信號量,首先要創建一個信號量集,創建信號量集的函數聲明如下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg);
函數 semget 用於創建一個新的信號量集或打開一個已存在的信號量集。其中參數 key 表示所創建或打開的信號量集的鍵。參數 nsems 表示創建的信號量集中信號量的個數,此參數只在創建一個新的信號量集時有效。參數 semflg 表示調用函數的操作類型,也可用於設置信號量集的訪問權限。所以調用函數 semget 的作用由參數 key 和 semflg 決定。
當函數調用成功時,返回值為信號量的引用標識符,調用失敗時,返回值為 -1。當調用 semget 函數創建一個信號量時,它相應的 semid_ds 數據結構被初始化。ipc_perm 中的各個字段被設置為相應的值,sem_nsems 被設置為 nsems 所表示的值,sem_otime 被設置為 0,sem_ctime 被設置為當前時間。
對信號量集的操作
對信號量集操作的函數聲明如下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, size_t nsops);
參數 semid 為信號量集的引用標識符。sops 為指向 sembuf 類型的數組的指針,sembuf 結構用於指定調用 semop 函數所做的操作。sembuf 結構的定義如下:
struct sembuf { short sem_num; // 要操作的信號量在信號量集里的編號 short sem_op; short sem_flag; };
其中,sem_num 指定要操作的信號量。sem_flag 為操作標記,與此函數相關的有 IPC_NOWAIT 和 SEM_UNDO。sem_op 用於表示所要執行的操作,相應的取值和含義如下:
sem_op > 0:表示進程對資源使用完畢,交回該資源。此時信號量集的 semid_ds 結構的 sem_base.semval 將加上 sem_op 的值。若此時設置了 SEM_UNDO 位,則信號量的調整值將減去 sem_op 的絕對值。
sem_op = 0:表示進程要等待,直至 sem_base.semval 變為 0。
sem_op < 0:表示進程希望使用資源。此時將比較 sem_base.semval 和 sem_op 的絕對值大小。如果 sem_base.semval 大於等於 sem_op 的絕對值,說明資源足夠分配給此進程,則 sem_base.semval 將減去 sem_op 的絕對值。若此時設置了 SEM_UNDO 位,則信號量的調整值將加上 sem_op 的絕對值。如果 sem_base.semval 小於 sem_op 的絕對值,表示資源不足。若設置了 IPC_NOWAIT 位,則函數出錯返回,否則 semid_ds 結構中的 sem_base.semncnt 加 1,進程等待直至 sem_base.semval 大於等於 sem_op 的絕對值或該信號量被刪除。
sops 指向的數組中的每個元素表示一個操作,由於此函數是一個原子操作,一旦執行就將執行數組中所有的操作。
信號量的控制
對信號量的具體控制操作是通過函數 semctl 來實現的,其聲明如下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, union semun arg);
參數 semid 為信號量集的引用標識符。參數 semnum 用於指明某個特定的信號量。參數 cmd 表示調用該函數希望執行的操作。參數 arg 是一個用戶自定義的聯合體:
union semun { int val; struct semid_ds *buf; ushort *array; // cmd == SETALL,或 cmd = GETALL };
此聯合中各個字段的使用情況與參數 cmd 的設置有關。具體的說明如下:
GETALL:獲得 semid 所表示的信號量集中信號量的個數,並將該值存放在無符號短整型數組 array 中。
GETNCNT:獲得 semid 所表示的信號量集中的等待給定信號量鎖的進程數目,即 semid_ds 結構中 sem.semncnt 的值。
GETPID:獲得 semid 所表示的信號量集中最后一個使用 semop 函數的進程 ID,即 semid_ds 結構中的 sem.sempid 的值。
GETVAL:獲得 semid 所表示的信號量集中 semunm 所指定信號量的值。
GETZCNT:獲得 semid 所表示的信號量集中的等待信號量成為 0 的進程數目,即 semid_ds 結構中的 sem.semncnt 的值。
IPC_RMID:刪除該信號量。
IPC_SET:按參數 arg.buf 指向的結構中的值設置該信號量對應的 semid_ds 結構。只有有效用戶 ID 和信號量的所有者 ID 或創建者 ID 相同的用戶進程,以及超級用戶進程可以執行這一操作。
IPC_STAT:獲得該信號量的 semid_ds 結構,保存在 arg.buf 指向的緩沖區。
SETALL:以 arg.array 中的值設置 semid 所表示的信號量集中信號量的個數。
SETVAL:設置 semid 所表示的信號量集中 semnum 所指定信號量的值。
應用信號量的 demo
下面我們通過一個 demo 來看看如何在程序中使用信號量。這是一個通過共享內存進行進行進程間通信的例子:
#include <sys/types.h> #include <sys/sem.h> #include <sys/shm.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <signal.h> #define SHMDATASIZE 1000 #define SN_EMPTY 0 #define SN_FULL 1 int deleteSemid = 0; void server(void); void client(int shmid, int semid); void delete(void); void sigdelete(int signum); void locksem(int signum, int semnum); void unlocksem(int semid, int semnum); void clientwrite(int shmid, int semid, char *buffer); union semun { int val; struct semid_ds *buf; ushort *array; }; int safesemget(key_t key, int nssems, int semflg); int safesemctl(int semid, int semunm, int cmd, union semun arg); int safesemop(int semid, struct sembuf *sops, unsigned nsops); int safeshmget(key_t key, int size, int shmflg); void *safeshmat(int shmid, const void *shmaddr, int shmflg); int safeshmctl(int shmid, int cmd, struct shmid_ds *buf); int main(int argc, char *argv[ ]) { if(argc < 3){ server(); } else{ client(atoi(argv[1]), atoi(argv[2])); } return 0; } void server(void) { union semun sunion; int semid, shmid; char *buffer; semid = safesemget(IPC_PRIVATE, 2, SHM_R|SHM_W); deleteSemid = semid; // 在服務器端程序退出時刪除掉信號量集。 atexit(&delete); signal(SIGINT, &sigdelete); // 把第一個信號量設置為 1,第二個信號量設置為 0, // 這樣來控制:必須在客戶端程序把數據寫入共享內存后服務器端程序才能去讀共享內存 sunion.val = 1; safesemctl(semid, SN_EMPTY, SETVAL, sunion); sunion.val = 0; safesemctl(semid, SN_FULL, SETVAL, sunion); shmid = safeshmget(IPC_PRIVATE, SHMDATASIZE, IPC_CREAT|SHM_R|SHM_W); buffer = safeshmat(shmid, 0, 0); safeshmctl(shmid, IPC_RMID, NULL); // 打印共享內存 ID 和 信號量集 ID,客戶端程序需要用它們作為參數 printf("Server is running with SHM id ** %d**\n", shmid); printf("Server is running with SEM id ** %d**\n", semid); while(1) { printf("Waiting until full..."); fflush(stdout); locksem(semid, SN_FULL); printf("done.\n"); printf("Message received: %s.\n", buffer); unlocksem(semid, SN_EMPTY); } } void client(int shmid, int semid) { char *buffer; buffer = safeshmat(shmid, 0, 0); printf("Client operational: shm id is %d, sem id is %d\n", shmid, semid); while(1) { char input[3]; printf("\n\nMenu\n1.Send a message\n"); printf("2.Exit\n"); fgets(input, sizeof(input), stdin); switch(input[0]) { case '1': clientwrite(shmid, semid, buffer); break; case '2': exit(0); break; } } } … void locksem(int semid, int semnum) { struct sembuf sb; sb.sem_num = semnum; sb.sem_op = -1; sb.sem_flg = SEM_UNDO; safesemop(semid, &sb, 1); } void unlocksem(int semid, int semnum) { struct sembuf sb; sb.sem_num = semnum; sb.sem_op = 1; sb.sem_flg = SEM_UNDO; safesemop(semid, &sb, 1); } …
由於完整的 demo 代碼比較長,這里僅貼出來了程序的主干,完整的程序請訪問這里。
把程序代碼保存到文件 sem.c 文件中,並編譯:
$ gcc -Wall sem.c -o sem_demo
先不傳遞參數運行服務器端程序:
$ sudo ./sem_demo
然后再啟動一個終端運行客戶端程序,並把服務器端輸出的 SHM id 和 SEM id 作為參數傳入到客戶端程序中:
$ sudo ./sem_demo 2064397 131072
服務器端(左側窗口)程序會等待客戶端(右側窗口)程序的輸入,並按照順序把客戶端中的輸入在服務器端輸出。服務器端程序和客戶端程序通過信號量來控制對共享內存的訪問,從而實現進程間數據的同步(具體的實現請參考代碼)。
接着我們通過下面的命令查看系統中的 IPC 信號量:
$ ipcs -s
這就是服務器與客戶端程序用來實現同步機制的信號量集!在我們的 demo 中,當服務器端程序退出時會刪除掉這個信號量集,以免給系統添加垃圾。
總結
我們在《System V IPC 之共享內存》一文中寫了一個很簡陋的應用共享內存的 demo,由於沒有應用任何的同步訪問技術,其輸出是比較混亂的。本文的 demo 則是在其基礎上添加了信號量來控制進程對共享內存的訪問。從程序的輸出我們可以看到,使用信號量解決互斥共享資源的同步問題后,服務器端程序的輸出變得和客戶端的輸入一致了。
參考:
《深入理解 Linux 內核》
《Linux 環境下 C 編程指南》