Linux 程序間通訊(四)訊號量
1 訊號量概述
訊號量和其他IPC不同,並沒有在程序之間傳送資料,訊號量用於多程序在存取共享資源時的同步控制就像交通路口的紅路燈一樣,當訊號量大於0,表示綠燈允許通過,當訊號量等於0,表示紅燈,必須停下來等待綠燈才能通過。
程序間的互斥關係與同步關係存在的根源在於臨界資源。
臨界資源是在同一個時刻只允許有限個(通常只有一個) 程序可以訪問(讀) 或修改(寫)的資源, 通常包括硬體資源(處理器、 記憶體、 儲存器及其他外圍裝置等) 和軟體資源(共享程式碼段、 共享結構和變數等)。訪問臨界資源的程式碼叫做臨界區, 臨界區本身也會成為臨界資源。
訊號量是用來解決程序間的同步與互斥問題的一種程序間通訊機制,包括一個稱為訊號量的變數和在該訊號量下等待資源的程序等待列, 以及對訊號量進行的兩個原子操作(PV操作)。 其中訊號量對應於某一種資源, 取一個非負的整型值。 訊號量值指的是當前可用的該資源的數量, 若等於 0 則意味著目前沒有可用的資源。PV 原子操作的具體定義如下。
- P 操作: 如果有可用的資源(訊號量值>0), 則佔用一個資源(給訊號量值減 1,進入臨界區程式碼); 如果沒有可用的資源(訊號量值=0), 則被阻塞直到系統將資源分配給該程序(進入等待佇列, 一直等到資源輪到該程序)。
- V 操作: 如果在該訊號量的等待佇列中有程序在等待資源, 則喚醒一個阻塞程序;如果沒有程序等待它, 則釋放一個資源(給訊號量值加 1)。
常見的使用訊號量訪問臨界區的虛擬碼如下:
{ /* 設 R 為某種資源, S 為資源 R 的訊號量 */ INIT_VAL(S); /* 對訊號量 S 進行初始化 */ 非臨界區; P(S); /* 進行 P 操作 */ 臨界區(使用資源 R) ; /* 只有有限個(通常只有一個) 程序被允許進入該區 */ V(S); /* 進行 V 操作 */ 非臨界區; }
最簡單的訊號量只能取 0 和 1 兩種值, 這種訊號量叫做二維訊號量。 這裡主要討論二維訊號量。 二維訊號量的應用比較容易擴充套件到使用多維訊號量的情況。
2 訊號量程式設計
2.1 函式說明 在 Linux 系統中, 使用訊號量通常分為以下幾個步驟: (1) 建立訊號量或獲得在系統中已存在的訊號量, 此時需要呼叫 semget()函式。 不同程序通過使用同一個訊號量鍵值來獲得同一個訊號量。 (2) 初始化訊號量, 此時使用 semctl()函式的 SETVAL 操作。 當使用二維訊號量時, 通常將訊號量初始化為 1。 (3) 進行訊號量的 PV 操作, 此時呼叫 semop()函式。 這一步是實現程序間的同步和互斥的核心工作部分。 (4)如果不需要訊號量, 則從系統中刪除它, 此時使用 semctl ()函式的 IPC_RMID 操作。需要注意的是, 在程式中不應該出現對已經被刪除的訊號量的操作。
注意:建立了一個訊號就會在系統中一直存在,直到我們去刪除
1.建立訊號量的函式
這裡列舉了 semget()函式的語法要點。
key:如果為IPC_PRIVATE,則表示建立訊號量,如果key不為IPC_PRIVATE且key所對應的訊號量已經存在,則返回訊號量
nsems: 需要建立的訊號量數目, 通常取值為 1
semflg:
如果semget用於建立新的訊號量,則的值為IPC_CREAT | perm,perm為新建立訊號量的存取許可權
如果semget用於獲得已經存在的訊號量則semflg的值為0
例項程式碼:
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
int main(void)
{
int semid = -1;
semid = semget(222, 1, 0666 | IPC_CREAT);
if(semid >= 0)
{
printf("新建訊號的識別符號,值為 [%d]\n", semid);
}else
{
perror("semget");
}
return 0;
}
結果:
# ./a.out 新建訊號的識別符號,值為 [65538]
注意:
建立的識別符號的值,不等於在semget函式中給定的key值
2.獲得和釋放訊號量的函式:
int semop(int semid, struct sembuf *sops, size_t nsops)
semid: semget()函式返回的訊號量識別符號sops: 指向訊號量運算元組, 一個數組包括以下成員。nsops: 運算元組 sops 中的操作個數(元素數目) , 通常取值為 1(一個操作)
struct sembuf{
short sem_num; /* 訊號量編號, 使用單個訊號量時, 通常取值為 0 */
short sem_op;/* 訊號量操作: 取值為-1 則表示 P 操作, 取值為+1 則表示 V 操作 */
short sem_flg;/* 通常設定為 SEM_UNDO。 這樣在程序沒釋放訊號量而退出時, 系統自動釋放該程序中未釋放的訊號量 */
}
sem_op > 0:那麼操作將sem_op加入到訊號量的值中,並喚醒等待訊號增加的程序
sem_op == 0:當訊號量的值是0時,函式返回,否則阻塞直到訊號量的值為0
sem_op < 0:則判斷訊號量的值加上sem_op後的值,
如果為0,喚醒等待訊號量為0的程序,
如果小於0,呼叫該函式的程序阻塞,
如果大於0,那麼訊號量減去這個值並返回
獲取訊號量的程式碼:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
int main(void)
{
int semid = -1;
struct sembuf sb;
semid = semget(222, 1, 0);
if(semid >= 0)
{
sb.sem_num = 0;
sb.sem_op = -1; //-1表示獲取訊號量
sb.sem_flg = 0;
printf("值為 [%d]\n", semid);
if(semop(semid, &sb, 1) == -1) //如果沒得到訊號量會阻塞在這
{
perror("semop");
exit(-1);
}
printf("值為 [%d]\n", semid);
}
return 0;
}
釋放訊號量的程式碼:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
int main(void)
{
int semid = -1;
struct sembuf sb;
semid = semget(222, 1, 0);
if(semid >= 0)
{
sb.sem_num = 0;
sb.sem_op = 1; //大於0,表示釋放訊號量
sb.sem_flg = 0;
printf("值為 [%d]\n", semid);
if(semop(semid, &sb, 1) == -1)
{
perror("semop");
exit(-1);
}
printf("值為 [%d]\n", semid);
}
return 0;
}
結果:
終端1執行獲取訊號量的程式碼,如果它沒有獲取到訊號量,就會一直阻塞,直到終端2釋放訊號量
3.控制訊號量操作的函式:
刪除訊號量例項程式碼:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
int main(void)
{
int semid = -1;
int ret = -1;
struct sembuf sb;
semid = semget(222, 1, 0);
if(semid >= 0)
{
ret = semctl(semid, 0, IPC_RMID);
if(ret != -1)
{
printf("刪除值為 [%d] 訊號成功\n", semid);
}else
{
perror("semctl");
}
}
return 0;
}
完整訊號量處理程式碼:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
/* 訊號量初始化(賦值) 函式 */
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value; /* init_value 為初始值 */
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
{
perror("Initialize semaphore");
return -1;
}
return 0;
}
/* 從系統中刪除訊號量的函式 */
int del_sem(int sem_id)
{
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
{
perror("Delete semaphore");
return -1;
}
}
/* P 操作函式 */
int sem_p(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0; /* 單個訊號量的編號應該為 0 */
sem_b.sem_op = -1; /* 表示 P 操作 */
sem_b.sem_flg = SEM_UNDO; /* 系統自動釋放將會在系統中殘留的訊號量 */
if (semop(sem_id, &sem_b, 1) == -1)
{
perror("P operation");
return -1;
}
return 0;
}
/* V 操作函式 */
int sem_v(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0; /* 單個訊號量的編號應該為 0 */
sem_b.sem_op = 1; /* 表示 V 操作 */
sem_b.sem_flg = SEM_UNDO; /* 系統自動釋放將會在系統中殘留的訊號量 */
if (semop(sem_id, &sem_b, 1) == -1)
{
perror("V operation");
return -1;
}
return 0;
}
int main(void)
{
pid_t result;
int sem_id;
sem_id = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT); /* 建立一個訊號量 */
init_sem(sem_id, 0);
/* 呼叫 fork()函式 */
result = fork();
if(result == -1)
{
perror("Fork\n");
}else if (result == 0) /* 返回值為 0 代表子程序 */
{
sleep(5);
printf("子程序一下\n");
sem_v(sem_id);
sem_p(sem_id);
sem_v(sem_id);
printf("子程序再一下\n");
}else /* 返回值大於 0 代表父程序 */
{
sem_p(sem_id);
printf("父程序一下\n");
sem_v(sem_id);
sem_p(sem_id);
sem_v(sem_id);
printf("父程序再一下\n");
del_sem(sem_id);
}
exit(0);
}
結果:
# ./a.out 子程序一下 父程序一下 子程序再一下 父程序再一下
總結:
1.訊號量類似於執行緒中的互斥鎖,(一個執行緒上鎖後,另一個執行緒要等待另一個程序解鎖後才能繼續執行)
2.定義了一個訊號量後,會一直在系統中存在
3.訊號量比互斥鎖複雜太多了
4.semctl函式中的union semun需要自己定義
5.有一點沒搞明白,就是如果多個程序等待一個訊號量,該如何處理