Linux進程同步之POSIX訊號量__Linux

來源:互聯網
上載者:User

POSIX訊號量是屬於POSIX標準系統介面定義的即時擴充部分。在SUS(Single UNIX Specification)單一規範中,定義的XSI IPC中也同樣定義了人們通常稱為System V訊號量的系統介面。訊號量作為進程間同步的工具是很常用的一種同步IPC類型。

在《UNIX網路編程 卷2:處理序間通訊》的前言第二頁與第1版的區別中作者提到“POSIX IPC函數時大勢所趨,因為他們比System V中的相應部分更具有優勢”,這裡所說的優勢我還得慢慢領會呀。。。<T_T>

訊號量是一種用於不同進程間進行同步的工具,當然對於進程安全的對於線程也肯定是安全的,所以訊號量也理所當然可以用於同一進程內的不同線程的同步。

有了互斥量和條件變數還提供訊號量的原因是:訊號量的主要目的是提供一種進程間同步的方式。這種同步的進程可以共用也可以不共用記憶體區。雖然訊號量的意圖在於進程間的同步,互斥量和條件變數的意圖在於線程間同步,但訊號量也可用於線程間同步,互斥量和條件變數也可通過共用記憶體區進行進程間同步。但應該根據具體應用考慮到效率和易用性進行具體的選擇。 1 POSIX訊號量的操作

POSIX訊號量有兩種:有名訊號量無名訊號量,無名訊號量也被稱作基於記憶體的訊號量。有名訊號量通過IPC名字進行進程間的同步,而無名訊號量如果不是放在進程間的共用記憶體區中,是不能用來進行進程間同步的,只能用來進行線程同步。

POSIX訊號量有三種操作:

(1)建立一個訊號量。建立的過程還要求初始化訊號量的值。

根據訊號量取值(代表可用資源的數目)的不同,POSIX訊號量還可以分為: 二值訊號量:訊號量的值只有0和1,這和互斥量很類型,若資源被鎖住,訊號量的值為0,若資源可用,則訊號量的值為1; 計數訊號量:訊號量的值在0到一個大於1的限制值(POSIX指出系統的最大限制值至少要為32767)。該計數表示可用的資源的個數。

(2)等待一個訊號量(wait)。該操作會檢查訊號量的值,如果其值小於或等於0,那就阻塞,直到該值變成大於0,然後等待進程將訊號量的值減1,進程獲得共用資源的存取權限。這整個操作必須是一個原子操作。該操作還經常被稱為P操作(荷蘭語Proberen,意為:嘗試)。

(3)掛出一個訊號量(post)。該操作將訊號量的值加1,如果有進程阻塞著等待該訊號量,那麼其中一個進程將被喚醒。該操作也必須是一個原子操作。該操作還經常被稱為V操作(荷蘭語Verhogen,意為:增加)

下面示範經典的生產者消費者問題,單個生產者和消費者共用一個緩衝區;

下面是生產者和消費者同步的虛擬碼:


//訊號量的初始化get = 0;//表示可讀資源的數目put = 1;//表示可寫資源的數目//生產者進程                               //消費者進程for(; ;){                                    for(; ;){Sem_wait(put);                                 Sem_wait(get);寫共用緩衝區;                               讀共用緩衝區;Sem_post(get);                                 Sem_post(put);}                                           }
上面的代碼大致流程如下:當生產者和消費者開始都運行時,生產者擷取put 訊號量,此時 put 為 1 表示有資源可用,生產者進入共用緩衝區,進行修改。而消費者擷取 get 訊號量,而此時 get 為 0 ,表示沒有資源可讀,於是消費者進入等待序列,直到生產者生產出一個資料,然後生產者通過掛出 get 訊號量來通知等待的消費者,有資料可以讀。

很多時候訊號量和互斥量,條件變數三者都可以在某種應用中使用,那這三者的差異有哪些呢,下面列出了這三者之間的差異: 互斥量必須由給它上鎖的線程解鎖。而訊號量不需要由等待它的線程進行掛出,可以在其他進程進行掛出操作。 互斥量要麼被鎖住,要麼是解開狀態,只有這兩種狀態。而訊號量的值可以支援多個進程成功進行wait操作。 訊號量的掛出操作總是被記住,因為訊號量有一個計數值,掛出操作總會將該計數值加1,然而當向條件變數發送一個訊號時,如果沒有線程等待在條件變數,那麼該訊號會丟失。 2 POSIX訊號量函數介面

POSIX訊號量的函數介面如下圖所示:


2.1有名訊號量的建立和刪除

#include <semaphore.h>sem_t *sem_open(const char *name, int oflag);sem_t *sem_open(const char *name, int oflag,                  mode_t mode, unsigned int value);                              //成功返回訊號量指標,失敗返回SEM_FAILED

sem_open用於建立或開啟一個訊號量,訊號量是通過name參數即訊號量的名字來進行標識的。關於POSX IPC的名字可以參考《UNIX網路編程 卷2:處理序間通訊》P14。

oflag參數可以為:0,O_CREAT,O_EXCL。如果為0表示開啟一個已存在的訊號量,如果為O_CREAT,表示如果訊號量不存在就建立一個訊號量,如果存在則開啟被返回。此時mode和value需要指定。如果為O_CREAT | O_EXCL,表示如果訊號量已存在會返回錯誤。

mode參數用於建立訊號量時,表示訊號量的許可權位,和open函數一樣包括:S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。

value表示建立訊號量時,訊號量的初始值。

#include <semaphore.h>int sem_close(sem_t *sem);int sem_unlink(const char *name);                              //成功返回0,失敗返回-1

sem_close用於關閉開啟的訊號量。當一個進程終止時,核心對其上仍然開啟的所有有名訊號量自動執行這個操作。調用sem_close關閉訊號量並沒有把它從系統中刪除它,POSIX有名訊號量是隨核心持續的。即使當前沒有進程開啟某個訊號量它的值依然保持。直到核心重新自舉或調用sem_unlink()刪除該訊號量。

sem_unlink用於將有名訊號量立刻從系統中刪除,但訊號量的銷毀是在所有進程都關閉訊號量的時候。 2.2訊號量的P操作

#include <semaphore.h>int sem_wait (sem_t *sem);#ifdef __USE_XOPEN2Kint sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);#endifint sem_trywait (sem_t * sem);                              //成功返回0,失敗返回-1
sem_wait() 用於擷取訊號量,首先會測試指定訊號量的值,如果大於 0 ,就會將它減 1 並立即返回,如果等於 0 ,那麼調用線程會進入睡眠,指定訊號量的值大於 0.

sem_trywait和sem_wait的差別是,當訊號量的值等於0的,調用線程不會阻塞,直接返回,並標識EAGAIN錯誤。

sem_timedwait和sem_wait的差別是當訊號量的值等於0時,調用線程會限時等待。當等待時間到後,訊號量的值還是0,那麼就會返回錯誤。其中 struct timespec *abs_timeout是一個絕對時間,具體可以參考條件變數關於等待時間的使用 2.3訊號量的V操作

#include <semaphore.h>int sem_post(sem_t *sem);                            //成功返回0,失敗返回-1

當一個線程使用完某個訊號量後,調用sem_post,使該訊號量的值加1,如果有等待的線程,那麼會喚醒等待的一個線程。 2.4擷取當前訊號量的值

#include <semaphore.h>int sem_getvalue(sem_t *sem,  int *sval);                            //成功返回0,失敗返回-1

該函數返回當前訊號量的值,通過sval輸出參數返回,如果當前訊號量已經上鎖(即同步對象不可用),那麼傳回值為0,或為負數,其絕對值就是等待該訊號量解鎖的線程數。

下面測試在Linux下的訊號量是否會出現負值:

#include <iostream>#include <unistd.h>#include <semaphore.h>#include <fcntl.h>using namespace std;#define SEM_NAME "/sem_name"sem_t *pSem;void * testThread (void *ptr){    sem_wait(pSem);    sleep(10);    sem_close(pSem);}int main(){    pSem = sem_open(SEM_NAME, O_CREAT, 0666, 5);    pthread_t pid;    int semVal;    for (int i = 0; i < 7; ++i)    {        pthread_create(&pid, NULL, testThread, NULL);        sleep(1);        sem_getvalue(pSem, &semVal);         cout<<"semaphore value:"<<semVal<<endl;    }    sem_close(pSem);    sem_unlink(SEM_NAME);}

執行結果如下:

semaphore value:4semaphore value:3semaphore value:2semaphore value:1semaphore value:0semaphore value:0semaphore value:0

這說明在Linux 2.6.18中POSIX訊號量是不會出現負值的。 2.5無名訊號量的建立和銷毀

#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);                            //若出錯則返回-1int sem_destroy(sem_t *sem);                            //成功返回0,失敗返回-1

sem_init()用於無名訊號量的初始化。無名訊號量在初始化前一定要在記憶體中分配一個sem_t訊號量類型的對象,這就是無名訊號量又稱為基於記憶體的訊號量的原因。

sem_init()第一個參數是指向一個已經分配的sem_t變數。第二個參數pshared表示該訊號量是否由於進程間通步,當pshared = 0,那麼表示該訊號量只能用於進程內部的線程間的同步。當pshared != 0,表示該訊號量存放在共用記憶體區中,使使用它的進程能夠訪問該共用記憶體區進行進程同步。第三個參數value表示訊號量的初始值。

這裡需要注意的是,無名訊號量不使用任何類似O_CREAT的標誌,這表示sem_init()總是會初始化訊號量的值,所以對於特定的一個訊號量,我們必須保證只調用sem_init()進行初始化一次,對於一個已初始化過的訊號量調用sem_init()的行為是未定義的。如果訊號量還沒有被某個線程調用還好,否則基本上會出現問題。

使用完一個無名訊號量後,調用sem_destroy摧毀它。這裡要注意的是:摧毀一個有線程阻塞在其上的訊號量的行為是未定義的2.6有名和無名訊號量的持久性

有名訊號量是隨核心持續的。當有名訊號量建立後,即使當前沒有進程開啟某個訊號量它的值依然保持。直到核心重新自舉或調用sem_unlink()刪除該訊號量。

無名訊號量的持久性要根據訊號量在記憶體中的位置: 如果無名訊號量是在單個進程內部的資料空間中,即訊號量只能在進程內部的各個線程間共用,那麼訊號量是隨進程的持久性,當進程終止時它也就消失了。 如果無名訊號量位於不同進程的共用記憶體區,因此只要該共用記憶體區仍然存在,該訊號量就會一直存在。所以此時無名訊號量是隨核心的持久性2.7訊號量的繼承和銷毀

(1)繼承

對於有名訊號量在父進程中開啟的任何有名訊號量在子進程中仍是開啟的。即下面代碼是正確的:

sem_t *pSem;pSem = sem_open(SEM_NAME, O_CREAT, 0666, 5);if(fork() == 0){    //...    sem_wait(pSem);    //...}

對於無名訊號量的繼承要根據訊號量在記憶體中的位置: 如果無名訊號量是在單個進程內部的資料空間中,那麼訊號量就是進程資料區段或者是堆棧上,當fork產生子進程後,該訊號量只是原來的一個拷貝,和之前的訊號量是獨立的。下面是測試代碼:

int main(){    sem_t mSem;    sem_init(&mSem, 0, 3);    int val;    sem_getvalue(&mSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;    sem_wait(&mSem);    sem_getvalue(&mSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;    if(fork() == 0)    {           sem_getvalue(&mSem, &val);        cout<<"child:semaphore value:"<<val<<endl;          sem_wait(&mSem);        sem_getvalue(&mSem, &val);        cout<<"child:semaphore value:"<<val<<endl;        exit(0);    }    sleep(1);    sem_getvalue(&mSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;}

測試結果如下:

parent:semaphore value:3parent:semaphore value:2child:semaphore value:2child:semaphore value:1parent:semaphore value:2
如果無名訊號量位於 不同進程的共用記憶體區,那麼fork產生的子進程中的訊號量仍然會存在該共用記憶體區,所以該訊號量仍然保持著之前的狀態。

(2)銷毀

對於有名訊號量,當某個持有該訊號量的進程沒有解鎖該訊號量就終止了,核心並不會將該訊號量解鎖。這跟記錄鎖不一樣。

對於無名訊號量,如果訊號量位於進程內部的記憶體空間中,當進程終止後,訊號量也就不存在了,無所謂解鎖了。如果訊號量位於進程間的共用記憶體區中,當進程終止後,核心也不會將該訊號量解鎖。

下面是測試代碼:

int main(){    sem_t *pSem;    pSem = sem_open(SEM_NAME, O_CREAT, 0666, 5);    int val;    sem_getvalue(pSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;       if(fork() == 0)    {           sem_wait(pSem);        sem_getvalue(pSem, &val);        cout<<"child:semaphore value:"<<val<<endl;        exit(0);    }    sleep(1);    sem_getvalue(pSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;    sem_unlink(SEM_NAME);}

下面是測試結果:

parent:semaphore value:5child:semaphore value:4parent:semaphore value:4
2.8訊號量代碼測試

對於有名訊號量在父進程中開啟的任何有名訊號量在子進程中仍是開啟的。即下面代碼是正確的:

對於訊號量用於進程間同步的代碼的測試,我沒有採用經典的生產者和消費者問題,原因是這裡會涉及到共用記憶體的操作。我只是簡單的用一個同步檔案操作的例子進行描述。 在下面的測試代碼中,POSIX有名訊號量初始值為2,允許兩個進程獲得檔案的操作許可權。代碼如下:

#include <iostream>#include <fstream>#include <cstdlib>#include <unistd.h>#include <semaphore.h>#include <fcntl.h>using namespace std;#define SEM_NAME "/sem_name"void semTest(int flag){     sem_t *pSem;    pSem = sem_open(SEM_NAME, O_CREAT, 0666, 2);    sem_wait(pSem);    ofstream fileStream("./test.txt", ios_base::app);      for (int i = 0; i < 5; ++i)      {          sleep(1);          fileStream<<flag;          fileStream<<' '<<flush;      }      sem_post(pSem);    sem_close(pSem);}int main(){   for (int i = 1; i <= 3; ++i)   {       if (fork() == 0)       {           semTest(i);           sleep(1);           exit(0);       }   }}

程式的運行結果,“./test.txt”檔案的內容如下:

//./test.txt1 2 1 2 1 2 1 2 1 2 3 3 3 3 3   

Jul 1, 2013 PM 22:04 @dorm

雜談:

轉眼間到7月份了,按著之前的計劃,我現在應該在幹自己想乾的事,可是總是事與願違,的確很大因素上是自己的原因,哎,苦逼呀。。。只能多看點書,9月份找個好工作吧,希望暑假沒有亂七八糟的事煩心。。。

今天是莎姐的生日,老弟這裡祝你生日快樂,雖然不能一起幫你過個生日,還是希望你每天都開心快樂,越來越漂亮,愛情甜蜜蜜。。。<^_^>

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.