信號量sem
一、什么是信號量
為了防止出現因多個程序同時訪問一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成并使用令牌來授權,在任一時刻只能有一個執行線程訪問代碼的臨界區域。臨界區域是指執行數據更新的代碼需要獨占式地執行。而信號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個線程在訪問它,也就是說信號量是用來調協進程對共享資源的訪問的。
信號量是一個特殊的變量,程序對其訪問都是原子操作,且只允許對它進行等待(即P(信號變量))和發送(即V(信號變量))信息操作。最簡單的信號量是只能取0和1的變量,這也是信號量最常見的一種形式,叫做二進制信號量。而可以取多個正整數的信號量被稱為通用信號量。這里主要討論二進制信號量。
二、信號量的工作原理
由于信號量只能進行兩種操作等待和發送信號,即P(sv)和V(sv),他們的行為是這樣的:
P(sv):如果sv的值大于零,就給它減1;如果它的值為零,就掛起該進程的執行
V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1.
舉個例子,就是兩個進程共享信號量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中,然后以后的函數就使用這個標識符來訪問信號量。
源代碼如下:
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
static int sem_id = 0;
static int init_sem()
{
semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1) {
printf("error to open semaphore!\n");
return 0;
}
return 1;
}
static int del_sem()
{
semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) {
printf("err to delete semaphore!\n");
return 0;
}
return 1;
}
static int semaphore_p()
{
sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = -1;
sem_buf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_buf, 1) == -1) {
printf("semaphore failed\n");
return 0;
}
return 1;
}
static int semaphore_v()
{
sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = 1;
sem_buf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_buf, 1) == -1) {
printf("semaphore failed\n");
return 0;
}
return 1;
}
int main(int argc, char* argv[])
{
char message = 'X';
sem_id = semget((key_t)1234, 1, 0666|IPC_CREAT);
if (argc > 1) {
if (!init_sem()) {
printf("create semaphore failed\n");
exit(EXIT_FAILURE);
}
message = 'F';
sleep(3);
}
for (int i = 0; i < 10; i++) {
if (!semaphore_p()) {
exit(EXIT_FAILURE);
}
printf("%c", message);
fflush(stdout);
sleep(rand()%3 );
printf("%c", message);
fflush(stdout);
sleep(rand()%2);
if (!semaphore_v()) {
exit(EXIT_FAILURE);
}
sleep(rand()%2);
}
sleep(10);
printf("\n %d- finished\n", getpid());
if (argc > 1) {
sleep(3);
del_sem();
}
exit(EXIT_FAILURE);
return 0;
}
需要注意,執行的方法:
brackendeMacBook-Pro:cpp_test bracken$ gcc -o selm.exe main.cpp -lm brackendeMacBook-Pro:cpp_test bracken$ ./selm.exe 0 & ./selm.exe [1] 10796 XXFFXXXXFFFFXXFFXXXXFFXXFFXXFFXXFFXXFFFF 10797- finished brackendeMacBook-Pro:cpp_test bracken$ 10796- finished
例子分析 :同時運行一個程序的兩個實例,注意第一次運行時,要加上一個字符作為參數,例如本例中的字符‘O’,它用于區分是否為第一次調用,同時這個字符輸出到屏幕中。因為每個程序都在其進入臨界區后和離開臨界區前打印一個字符,所以每個字符都應該成對出現,正如你看到的上圖的輸出那樣。在main函數中循環中我們可以看到,每次進程要訪問stdout(標準輸出),即要輸出字符時,每次都要檢查信號量是否可用(即stdout有沒有正在被其他進程使用)。所以,當一個進程A在調用函數semaphore_p進入了臨界區,輸出字符后,調用sleep時,另一個進程B可能想訪問stdout,但是信號量的P請求操作失敗,只能掛起自己的執行,當進程A調用函數semaphore_v離開了臨界區,進程B馬上被恢復執行。然后進程A和進程B就這樣一直循環了10次。
浙公網安備 33010602011771號