1. 程式人生 > >Linux程序同步之POSIX訊號量

Linux程序同步之POSIX訊號量

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

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

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

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

1 POSIX訊號量的操作

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

POSIX訊號量有三種操作:

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

根據訊號量取值(代表可用資源的數目)的不同,POSIX訊號量還可以分為:

  • 二值訊號量:訊號量的值只有01,這和互斥量很型別,若資源被鎖住,訊號量的值為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訊號量,此時put1表示有資源可用,生產者進入共享緩衝區,進行修改。而消費者獲取get訊號量,而此時get0,表示沒有資源可讀,於是消費者進入等待序列,直到生產者生產出一個數據,然後生產者通過掛出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引數可以為:0O_CREATO_EXCL。如果為0表示開啟一個已存在的訊號量,如果為O_CREAT,表示如果訊號量不存在就建立一個訊號量,如果存在則開啟被返回。此時modevalue需要指定。如果為O_CREAT | O_EXCL,表示如果訊號量已存在會返回錯誤。

mode引數用於建立訊號量時,表示訊號量的許可權位,和open函式一樣包括:S_IRUSRS_IWUSRS_IRGRPS_IWGRPS_IROTHS_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_XOPEN2K
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
#endif

int sem_trywait (sem_t * sem);
                              //成功返回0,失敗返回-1
sem_wait()用於獲取訊號量,首先會測試指定訊號量的值,如果大於0,就會將它減1並立即返回,如果等於0,那麼呼叫執行緒會進入睡眠,指定訊號量的值大於0.

sem_trywaitsem_wait的差別是,當訊號量的值等於0的,呼叫執行緒不會阻塞,直接返回,並標識EAGAIN錯誤。

sem_timedwaitsem_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:4
semaphore value:3
semaphore value:2
semaphore value:1
semaphore value:0
semaphore value:0
semaphore value:0

這說明在Linux 2.6.18中POSIX訊號量是不會出現負值的。

2.5無名訊號量的建立和銷燬
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
                            //若出錯則返回-1
int 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:3
parent:semaphore value:2
child:semaphore value:2
child:semaphore value:1
parent: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:5
child:semaphore value:4
parent: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.txt
1 2 1 2 1 2 1 2 1 2 3 3 3 3 3   

Jul 1, 2013 PM 22:04 @dorm

雜談:

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

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