1. 程式人生 > >linux c 訊號量程式設計

linux c 訊號量程式設計

訊號量

當我們在多使用者系統,多程序系統,或是兩者混合的系統中使用執行緒操作編寫程式時,我們經常會發現我們有段臨界程式碼,在此處我們需要保證一個程序(或是一個執行緒的執行)需要排他的訪問一個資源。
訊號量有一個複雜的程式設計介面。幸運的是,我們可以很容易的為自己提供一個對於大多數的訊號量程式設計問題足夠高效的簡化介面。
為了阻止多個程式同時訪問一個共享資源所引起的問題,我們需要一種方法生成並且使用一個標記從而保證在臨界區部分一次只有一個執行緒執行。執行緒相關的方法,我們可以使用互斥或訊號量來控制一個多執行緒程式對於臨界區的訪問。


編寫通用目的的程式碼保證一個程式排他的訪問一個特定的資源是十分困難的,儘管有一個名為Dekker的演算法解決方法。不幸的是,這個演算法依賴於”忙等待” 或是”自旋鎖”,即一個程序的連續執行需要等待一個記憶體地址發生改變。在一個多工環境中,例如Linux,這是對CPU資源的無謂浪費。如果硬體支援, 這樣的情況就要容易得多,通常以特定CPU指令的形式來支援排他訪問。硬體支援的例子可以是訪問指令與原子方式增加暫存器值,從而在讀取/增加/寫入的操 作之間就不會有其他的指令執行。

我們已經瞭解到的一個要行的解決方法就是使用O_EXCL標記呼叫open函式來建立檔案,這提供了原子方式的檔案建立。這會使得一個程序成功的獲得一個標記:新建立的檔案。這個方法可以用於簡單的問題,但是對於複雜的情況就要顯得煩瑣與低效了。

當Dijkstr引入訊號量的概念以後,並行程式設計領域前進了一大步。正如我們在第12章所討論的,訊號量是一個特殊的變數,他是一個整數,並且只有兩個操 作可以使得其值增加:等待(wait)與訊號(signal)。因為在Linux與UNIX程式設計中,”wait”與”signal”已經具有特殊的意義 了,我們將使用原始概念:
用於等待(wait)的P(訊號量變數)
用於訊號(signal)的V(訊號量變數)

這兩字母來自等待(passeren:通過,如同臨界區前的檢測點)與訊號(vrjgeven:指定或釋放,如同釋放臨界區的控制權)的荷蘭語。有時我們也會遇到與訊號量相關的術語”up”與”down”,來自於訊號標記的使用。

訊號量定義



最簡單的訊號量是一個只有0與1兩個值的變數,二值訊號量。這是最為通常的形式。具有多個正數值的訊號量被稱之為通用訊號量。在本章的其餘部分,我們將會討論二值訊號量。

P與V的定義出奇的簡單。假定我們有一個訊號量變數sv,兩個操作定義如下:

P(sv)    如果sv大於0,減小sv。如果sv為0,掛起這個程序的執行。
V(sv)    如果有程序被掛起等待sv,使其恢復執行。如果沒有進行被掛起等待sv,增加sv。

訊號量的另一個理解方式就是當臨界區可用時訊號量變數sv為true,當臨界區忙時訊號量變數被P(sv)減小,從而變為false,當臨界區再次可用時 被V(sv)增加。注意,簡單的具有一個我們可以減小或是增加的通常變數並不足夠,因為我們不能用C,C++或是其他的程式語言來表述生成訊號,進行原子 測試來確定變數是否為true,如果是則將其變為false。這就是使得訊號量操作特殊的地方。

一個理論例子


我們可以使用一個簡單的理論例子來了解一下訊號量是如何工作的。假設我們有兩個程序proc1與proc2,這兩個程序會在他們執行的某一時刻排他的訪問 一個數據庫。我們定義一個單一的二值訊號量,sv,其初始值為1並且可以為兩個程序所訪問。兩個程序然後需要執行同樣的處理來訪問臨界區程式碼;實際上,這 兩個程序可以是同一個程式的不同調用。

這兩個程序共享sv訊號量變數。一旦一個程序已經執行P(sv)操作,這個程序就可以獲得訊號量並且進入臨界區。第二個程序就會被阻止進行臨界區,因為當他嘗試執行P(sv)時,他就會等待,直到第一個程序離開臨界區並且執行V(sv)操作來釋放訊號量。

所需要的過程如下:

semaphore sv = 1;
loop forever {
    P(sv);
    critical code section;
    V(sv);
    noncritical code section;
}

這段程式碼出奇的簡單,因為P操作與V操作是十分強大的。圖14-1顯示了P操作與V操作如何成為進行臨界區程式碼的門檻。

Linux訊號量工具


現在我們已經瞭解了什麼是訊號量以及他們在理論上是如何工作的,現在我們可以來了解一下這些特性在Linux中是如何實現的。訊號量函式介面設計十分精 細,並且提供了比通常所需要的更多的實用效能。所有的Linux訊號量函式在通用的訊號量陣列上進行操作,而不是在一個單一的二值訊號量上進行操作。乍看 起來,這似乎使得事情變得更為複雜,但是在一個程序需要鎖住多個資源的複雜情況下,在訊號量陣列上進行操作將是一個極大的優點。在這一章,我們將會關注於 使用單一訊號量,因為在大多數情況下,這正是我們需要使用的。

訊號量函式定義如下:

#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, …);
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

事實上,為了獲得我們特定操作所需要的#define定義,我們需要在包含sys/sem.h檔案之前通常需要包含sys/types.h與sys/ipc.h檔案。而在某些情況下,這並不是必須的。

因為我們會依次瞭解每一個函式,記住,這些函式的設計是用於操作訊號量值陣列的,從而會使用其操作向比單個訊號量所需要的操作更為複雜。

注意,key的作用類似於一個檔名,因為他表示程式也許會使用或是合作所用的資源。相類似的,由semget所返回的並且為其他的共享記憶體函式所用的標 識符與由fopen函式所返回 的FILE *十分相似,因為他被程序用來訪問共享檔案。而且與檔案類似,不同的程序會有不同的訊號量識別符號,儘管他們指向相同的訊號量。key與識別符號的用法對於在 這裡所討論的所有IPC程式都是通用的,儘管每一個程式會使用獨立的key與識別符號。

semget

semget函式建立一個新的訊號量或是獲得一個已存在的訊號量鍵值。

int semget(key_t key, int num_sems, int sem_flags);

第一個引數key是一個用來允許不相關的程序訪問相同訊號量的整數值。所有的訊號量是為不同的程式通過提供一個key來間接訪問的,對於每一個訊號量系統 生成一個訊號量識別符號。訊號量鍵值只可以由semget獲得,所有其他的訊號量函式所用的訊號量識別符號都是由semget所返回的。

還有一個特殊的訊號量key值,IPC_PRIVATE(通常為0),其作用是建立一個只有建立程序可以訪問的訊號量。這通常並沒有有用的目的,而幸運的是,因為在某些Linux系統上,手冊頁將IPC_PRIVATE並沒有阻止其他的程序訪問訊號量作為一個bug列出。

num_sems引數是所需要的訊號量數目。這個值通常總是1。

sem_flags引數是一個標記集合,與open函式的標記十分類似。低九位是訊號的許可權,其作用與檔案許可權類似。另外,這些標記可以與 IPC_CREAT進行或操作來建立新的訊號量。設定IPC_CREAT標記並且指定一個已經存在的訊號量鍵值並不是一個錯誤。如果不需 要,IPC_CREAT標記只是被簡單的忽略。我們可以使用IPC_CREAT與IPC_EXCL的組合來保證我們可以獲得一個新的,唯一的訊號量。如果 這個訊號量已經存在,則會返回一個錯誤。

如果成功,semget函式會返回一個正數;這是用於其他訊號量函式的識別符號。如果失敗,則會返回-1。

semop

函式semop用來改變訊號量的值:

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

第一個引數,sem_id,是由semget函式所返回的訊號量識別符號。第二個引數,sem_ops,是一個指向結構陣列的指標,其中的每一個結構至少包含下列成員:

struct sembuf {
    short sem_num;
    short sem_op;
    short sem_flg;
}

第一個成員,sem_num,是訊號量數目,通常為0,除非我們正在使用一個訊號量陣列。sem_op成員是訊號量的變化量值。(我們可以以任何量改變信 號量值,而不只是1)通常情況下中使用兩個值,-1是我們的P操作,用來等待一個訊號量變得可用,而+1是我們的V操作,用來通知一個訊號量可用。

最後一個成員,sem_flg,通常設定為SEM_UNDO。這會使得作業系統跟蹤當前程序對訊號量所做的改變,而且如果程序終止而沒有釋放這個訊號量, 如果訊號量為這個程序所佔有,這個標記可以使得作業系統自動釋放這個訊號量。將sem_flg設定為SEM_UNDO是一個好習慣,除非我們需要不同的行 為。如果我們確實變我們需要一個不同的值而不是SEM_UNDO,一致性是十分重要的,否則我們就會變得十分迷惑,當我們的程序退出時,核心是否會嘗試清 理我們的訊號量。

semop的所用動作會同時作用,從而避免多個訊號量的使用所引起的競爭條件。我們可以在手冊頁中瞭解關於semop處理更為詳細的資訊。

semctl

semctl函式允許訊號量資訊的直接控制:

int semctl(int sem_id, int sem_num, int command, …);

第一個引數,sem_id,是由semget所獲得的訊號量識別符號。sem_num引數是訊號量數目。當我們使用訊號量陣列時會用到這個引數。通常,如果 這是第一個且是唯一的一個訊號量,這個值為0。command引數是要執行的動作,而如果提供了額外的引數,則是union semun,根據X/OPEN規範,這個引數至少包括下列引數:

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
}

許多版本的Linux在標頭檔案(通常為sem.h)中定義了semun聯合,儘管X/Open確認說我們必須定義我們自己的聯合。如果我們發現我們確實需 要定義我們自己的聯合,我們可以檢視semctl手冊頁瞭解定義。如果有這樣的情況,建議使用手冊頁中提供的定義,儘管這個定義與上面的有區別。

有多個不同的command值可以用於semctl。在這裡我們描述兩個會經常用到的值。要了解semctl功能的詳細資訊,我們應該檢視手冊頁。

這兩個通常的command值為:

SETVAL:用於初始化訊號量為一個已知的值。所需要的值作為聯合semun的val成員來傳遞。在訊號量第一次使用之前需要設定訊號量。
IPC_RMID:當訊號量不再需要時用於刪除一個訊號量標識。

semctl函式依據command引數會返回不同的值。對於SETVAL與IPC_RMID,如果成功則會返回0,否則會返回-1。

使用訊號量

正如我們在前面部分的描述中所看到的,訊號量操作是相當複雜的。這是最不幸的,因為使用臨界區進行多程序或是多執行緒程式設計是一個十分困難的問題,而其擁有其自己複雜的程式設計介面也增加了程式設計負擔。

幸運的是,我們可以使用最簡單的二值訊號量來解決大多數需要訊號量的問題。在我們的例子中,我們會使用所有的程式設計介面來建立一個非常簡單的用於二值訊號量的P
與V型別介面。然後,我們會使用這個簡單的介面來演示訊號量如何工作。

要試驗訊號量,我們將會使用一個簡單的程式,sem1.c,這個程式我們可以多次呼叫。我們將會使用一個可選的引數來標識這個程式是負責建立訊號量還是銷燬訊號量。

我們使用兩個不同字元的輸出來標識進入與離開臨界區。使用引數呼叫的程式會在進入與離開其臨界區時輸出一個X,而另一個程式呼叫會在進入與離開其臨界區時輸出一個O。因為在任何指定的時間內只有一個程序能夠進入其臨界區,所以所有X與O字元都是成對出現的。

試驗--訊號量

1 在#include語句之後,我們定義函式原型與全域性變數,然後我們進入main函式。在這裡使用semget函式呼叫建立訊號量,這會返回一個訊號量 ID。如果程式是第一次呼叫(例如,使用一個引數並且argc > 1來呼叫),程式就會呼叫set_semvalue來初始化訊號量並且將op_char設定為X。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

include “semun.h”


static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);

static int sem_id;

int main(int argc, char **argv)
{
    int i;
    int pause_time;
    char op_char = ‘O’;

    srand((unsigned int)getpid());

    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

    if(argc > 1)
    {
        if(!set_semvalue())
        {
            fprintf(stderr, “Failed to initialize semaphore/n”);
            exit(EXIT_FAILURE);
        }
        op_char = ‘X’;
        sleep(2);
    }
2 然後我們使用一個迴圈程式碼進入並且離開臨界區10次。此時會呼叫semaphore_p函式,這個函式會設定訊號量並且等待程式進入臨界區。
    for(i=0;i<10;i++)
    {
        if(!semaphore_p()) exit(EXIT_FAILURE);
        printf(“%c”, op_char); fflush(stdout);
        pause_time = rand() % 3;
        sleep(pause_time);
        printf(“%c”, op_char); fflush(stdout);
3 在臨界區之後,我們呼叫semaphore_v函式,在隨機的等待之後再次進入for迴圈之後,將訊號量設定為可用。在迴圈之後,呼叫del_semvalue來清理程式碼。
        if(!semaphore_v()) exit(EXIT_FAILURE);

        pause_time = rand() % 2;
        sleep(pause_time);
    }

    printf(“/n%d - finished/n”, getpid());

    if(argc > 1)
    {
        sleep(10);
        del_semvalue();
    }

    exit(EXIT_SUCCESS);
    }

4 函式set_semvalue在一個semctl呼叫中使用SETVAL命令來初始化訊號量。在我們使用訊號量之前,我們需要這樣做。

static int set_semvalue(void)
{
    union semun sem_union;

    sem_union.val = 1;
    if(semctl(sem_id, 0, SETVAL, sem_union) == -1) return 0;
    return 1;
}
5 del_semvalue函式幾乎具有相同的格式,所不同的是semctl呼叫使用IPC_RMID命令來移除訊號量ID:

static void del_semvalue(void)
{
    union semun sem_union;

    if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
        fprintf(stderr, “Failed to delete semaphore/n”);
}

6 semaphore_p函式將訊號量減1(等待):

static int semaphore_p(void)
{
    struct sembuf sem_b;

    sem_b.sem_num = 0;
    sem_b.sem_op = -1;
    sem_b.sem_flag = SEM_UNDO;
    if(semop(sem_id, &sem_b, 1) == -1)
    {
        fprintf(stderr, “semaphore_p failed/n”);
        return 0;
    }
    return 1;
}

7 semaphore_v函式將sembuf結構的sem_op部分設定為1,從而訊號量變得可用。

static int semaphore_v(void)
{
    struct sembuf sem_b;

    sem_b.sem_num = 0;
    sem_b.sem_op = 1;
    sem_b.sem_flag = SEM_UNDO;
    if(semop(sem_id, &sem_b, 1) == -1)
    {
        fprintf(stderr, “semaphore_v failed/n”);
        return 0;
    }
    return 1;
}

注意,這個簡單的程式只有每個程式有一個二值訊號量,儘管如果我們需要多個訊號量,我們可以擴充套件這個程式來傳遞多個訊號量變數。通常,一個簡單的二值訊號量就足夠了。

我們可以通過多次呼叫這個程式來測試我們的程式。第一次,我們傳遞一個引數來通知程式他並不負責建立與刪除訊號量。另一次呼叫沒有傳遞引數。

下面是兩次呼叫的示例輸出結果:

./sem1 1 & 
[1] 1082
./sem1
OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
1083 - finished
1082 - finished
$

正如我們所看到了,O與X是成對出現的,表明臨界區部分被正確的處理了。如果這個程式在我們的系統上不能正常執行,也許我們需要在呼叫程式之前使用命令stty -tostop來保證生成tty輸出的後臺程式不會引起訊號生成。

工作原理


這個程式由我們選擇使用semget函式所獲得的鍵生成一個訊號量標識開始。IPC_CREAT標記會使得如果需要的時候建立一個訊號量。

如果這個程式有引數,他負責使用我們的set_semvalue函式來初始化訊號量,這是更為通用的semctl函式的一個簡化介面。同時,他也使用所提
供的引數來決定要輸出哪一個字元。sleep只是簡單的使得我們在這個程式執行多次之前有時間呼叫程式的另一個拷貝。在程式中我們使用srand與
rand來引入一些偽隨機計數。

這個程式迴圈十次,在其臨界區與非臨界區等待一段隨機的時間。臨界區程式碼是通過呼叫我們的semaphore_p與semaphore_v函式來進行保護的,這兩個函式是更為通用的semop函式的簡化介面。

在刪除訊號量之前,使用引數呼叫的程式拷貝會等待其他的呼叫結束。如果訊號量沒有刪除,他就會繼續存在於系統中,儘管已經沒有程式再使用他。在實際的程式
中,保證我們沒有遺留訊號是十分重要的。在我們下一次執行程式時,遺留的訊號量會引起問題,而且訊號量是限制資源,我們必須小心使用。