Linux 程式間通訊之System V 訊號量

Wu_XMing發表於2018-10-22

1.概述

  • System V 訊號量不是用來在程式間傳輸資料的。相反,它們用來同步程式的動作。訊號量的一個常見用途是同步一塊共享記憶體的訪問以防止一個程式在訪問共享記憶體的同時另一個程式更新這塊記憶體的情況。

  • 一個訊號量是一個由核心維護的整數,其值被限制為大於或等於0。在一個訊號量上可以執行各種操作(即系統呼叫):

    • 將訊號量設定成一個絕對值;
    • 在訊號量當前值的基礎上加一個數量;
    • 在訊號量當前值的基礎上減去一個數量;
    • 等待訊號量的值等於0;

上面後兩個操作可能導致呼叫阻塞。因為核心會將所有試圖將訊號量降低到0之下的操作阻塞。類似的,如果訊號量的當前值不為0,那麼等待訊號量的值等於0的呼叫程式將會發生阻塞。

  • System V 訊號量的分配是以組單位進行分配的,所以在建立一個訊號量集(即建立訊號量)的時候需要指定集合中訊號量的數量。
  • System V 訊號量的建立(初始值為0)和初始化是在不同的步驟完成的,因此當兩個程式同時都試圖執行這兩個步驟的時候就會出現競爭條件。

2.建立一個訊號量集

#include<sys/types.h>
#include<sys/sem.h>
int semget(key_t key,int nsems,int semflg);
//return  semaphore set identifier on success,or -1 on error
複製程式碼
  • key: 使用值IPC_PRIVATE或由ftok()返回的鍵

  • nsems: 指定集合中訊號量的數量,並且其值必須大於0.如果使用semget()來獲取一個既有集的識別符號,那麼nsems必須要小於或等於集合的大小(否則發生EINVAL錯誤)。無法修改一個既有集中的訊號量數量。

  • semflg:引數是一個位掩碼,它指定了施加於新訊號量集之上的許可權或需檢查的一個既有集合的許可權。

    • IPC_CREAT: 如果不存在與指定的key相關聯的訊號量集,那麼就建立一個新集合。
    • IPC_EXCL:與IPC_CREAT同時使用,如果指定key關聯的訊號量集已經存在,則返EEXIST錯誤。

建立一個System V 訊號量

int semid=semget(IPC_PRIVATE,1,S_IRUSR | S_IWUSR);//建立一個訊號量集,數量為1,所屬使用者可讀可寫  使用IPC_PRIVATE時,可以不顯示指出IPC_CREAT
if (semid == -1)
     errExit("semid");
複製程式碼

3.訊號量的控制操作

#include<sys/types.h>
#include<sys/sem.h>
int semctl(int semid,int semnum,int cmd,.../*union semun arg*/);
複製程式碼
  • semid: 引數是操作所施加的訊號量集的識別符號。
  • semnum:在單個訊號量上執行的操作時,semnum需指明瞭集合中具體訊號量。對於其他操作則會忽略這個引數,並且可以將其設定為0。
  • ... :(semun union 需程式顯示定義這個union)

/* The user should define a union like the following to use it for arguments
   for `semctl`.

   union semun
   {
     int val;				<= value for SETVAL
     struct semid_ds *buf;		<= buffer for IPC_STAT & IPC_SET
     unsigned short int *array;		<= array for GETALL & SETALL
     struct seminfo *__buf;		<= buffer for IPC_INFO
   };

   Previous versions of this file used to define this union but this is
   incorrect.  One can test the macro _SEM_SEMUN_UNDEFINED to see whether
   one must define the union or not.  */
複製程式碼
  • cmd: 引數指定了需執行的操作

    常規控制操作:

    • IPC_RMID:立即刪除訊號量集及其關聯的semid_ds資料結構。所有因semop()呼叫操作堵塞的程式都會立即喚醒,並返回EIDRM錯誤。這操作不需要arg引數。

    • IPC_STAT: 在arg.buf指向的緩衝區中放置一份與這個訊號量集相關聯的semid_ds資料結構的副本。

    • IPC_SET: 使用arg.buf指向的緩衝區中的值來更新與這個訊號量集相關聯的semid_ds資料結構中選中的欄位。

    獲取和初始化訊號量值

    • GETVAL:semctl()返回由semid指定的訊號量集中第semnum個訊號量的值。這個操作無需arg引數。

    • SETVAL:將semid指定的訊號量集中第semnum個訊號量的值修改為arg.val。

    • GETALL: 獲取由semid指向的訊號量集中所有訊號量的值並將它們放在arg.array指向的陣列中。程式設計師必須要確保該陣列具備足夠的空間。

    • SETALL:使用arg.array指向的陣列中的值修改semid指向的集合中的所有訊號量。

    獲取單個訊號量的資訊

    下面操作返回semid引用的集合中第semnum個訊號量的資訊。所有這些操作都需要在訊號量集合中具備讀許可權,並且無需arg引數。

    • GETPID:返回上一個在該訊號量上執行semop()的程式的程式ID; 這個值被稱為sempid值。如果還沒有程式在該訊號量上執行semop(),那麼就返回0。

    • GETNCNT: 返回當前等待該訊號量的值增長的程式數; 這個值被稱為semncnt值。

    • GETZCNT: 返回當前等待該訊號量的值變成0的程式數; 這個值被稱為semzcnt值。

3.1 訊號量關聯的資料結構

/* Data structure describing a set of semaphores.  */
struct semid_ds
{
  struct ipc_perm sem_perm;		/* operation permission struct */
  __time_t sem_otime;			/* last semop() time */
  __syscall_ulong_t __glibc_reserved1;
  __time_t sem_ctime;			/* last time changed by semctl() */
  __syscall_ulong_t __glibc_reserved2;
  __syscall_ulong_t sem_nsems;		/* number of semaphores in set */
  __syscall_ulong_t __glibc_reserved3;
  __syscall_ulong_t __glibc_reserved4;
};

/* Data structure used to pass permission information to IPC operations.  */
struct ipc_perm
  {
    __key_t __key;			/* Key.  */
    __uid_t uid;			/* Owner's user ID.  */
    __gid_t gid;			/* Owner's group ID.  */
    __uid_t cuid;			/* Creator's user ID.  */
    __gid_t cgid;			/* Creator's group ID.  */
    unsigned short int mode;		/* Read/write permission.  */
    unsigned short int __pad1;
    unsigned short int __seq;		/* Sequence number.  */
    unsigned short int __pad2;
    __syscall_ulong_t __glibc_reserved1;
    __syscall_ulong_t __glibc_reserved2;
  };

複製程式碼

3.2 訊號量初始化

  • 程式設計師必須要使用semctl()系統呼叫顯式地初始化訊號量。(在linux上,semget()返回的訊號量實際上會被初始化為0,但為了取得移植性就不能依賴於此。)

  • 因建立和初始化訊號量是分開進行的,所以當多個程式要對同一個訊號量進行建立和初始化訊號量時,就會出現競爭,那麼訊號量的初始值將由最後呼叫初始化的程式所決定。

    解決辦法:與訊號量集相關聯的semid_ds資料結構中的sem_otime欄位的初始化。在一個訊號量集首次被建立時,sem_otime欄位會被初始化為0,並且只有後續的semop()呼叫才會修改這個欄位的值。因此可以利用這個特性消除競爭條件。即只需要插入額外的程式碼來強制第二個程式(即沒有建立訊號量的那個程式)等待知道第一個程式即初始化了訊號量又執行了一個更新sem_otime欄位但不修改訊號量的值的semop()呼叫為止。

    semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms);
    
    if (semid != -1) {                  /* Successfully created the semaphore */
        union semun arg;
        struct sembuf sop;
    
        sleep(5);
        printf("%ld: created semaphore\n", (long) getpid());
    
        arg.val = 0;                    /* So initialize it to 0 */
        if (semctl(semid, 0, SETVAL, arg) == -1)
            errExit("semctl 1");
        printf("%ld: initialized semaphore\n", (long) getpid());
    
        /* Perform a "no-op" semaphore operation - changes sem_otime
           so other processes can see we`ve initialized the set. */
    
        sop.sem_num = 0;                /* Operate on semaphore 0 */
        sop.sem_op = 0;                 /* Wait for value to equal 0 */
        sop.sem_flg = 0;
        if (semop(semid, &sop, 1) == -1)
            errExit("semop");
        printf("%ld: completed dummy semop()\n", (long) getpid());
    
    } else {                            /* We didn`t create the semaphore set */
    
        if (errno != EEXIST) {          /* Unexpected error from semget() */
            errExit("semget 1");
    
        } else {                        /* Someone else already created it */
            const int MAX_TRIES = 10;
            int j;
            union semun arg;
            struct semid_ds ds;
    
            semid = semget(key, 1, perms);      /* So just get ID */
            if (semid == -1)
                errExit("semget 2");
    
            printf("%ld: got semaphore key\n", (long) getpid());
            /* Wait until another process has called semop() */
    
            arg.buf = &ds;
            for (j = 0; j < MAX_TRIES; j++) {
                printf("Try %d\n", j);
                if (semctl(semid, 0, IPC_STAT, arg) == -1)
                    errExit("semctl 2");
    
                if (ds.sem_otime != 0)          /* Semop() performed? */
                    break;                      /* Yes, quit loop */
                sleep(1);                       /* If not, wait and retry */
            }
    
            if (ds.sem_otime == 0)              /* Loop ran to completion! */
                fatal("Existing semaphore not initialized");
        }
    }
    複製程式碼

4.訊號量操作

#include<sys/types.h>
#include<sys/sem.h>
int semop(int semid,struct sembuf *sops,unsigned int nsops);
//return 0 on succes,or -1 on error

struct sembuf
{
  unsigned short int sem_num;	/* semaphore number */
  short int sem_op;		/* semaphore operation */
  short int sem_flg;		/* operation flag */
};

複製程式碼
  • sops 引數是一個指向陣列的指標,陣列中包含了需要執行的操作。

  • nsops引數給出了陣列的大小(陣列至少需包含一個元素)。操作將會按照在陣列中的順序以原子的方式被執行了。

  • sem_num 欄位標識出了在集合中的哪個訊號量上執行操作。

  • sem_op 欄位指定了需執行的操作。

    • 如果sem_op 大於0,那麼就將sem_op的之加到訊號量值上,其結果是其他等待減小的訊號量值的程式可能會被喚醒並執行它們的操作。呼叫程式必須具備在訊號量上的修改(寫)許可權。
    • 如果sem_op 等於0,那麼就對訊號量值進行檢查以確定它當前是否等於0。 如果等於0,那麼操作將立即結束,否則semop()就會阻塞知道訊號量值變成0位置。呼叫程式必須要具備在訊號量上的讀許可權。
    • 如果sem_op 小於0,那麼就將訊號量值減去sem_op。如果訊號量的當前值大於或等於sem_op的絕對值,那麼操作會立即結束。否則堵塞直到訊號量的當前值大於或等於0。呼叫程式必須具備在訊號量上的修改(寫)許可權。

    當semop()呼叫阻塞事,程式會保持阻塞直到發生下列某種情況為止。

    • 另一個程式修改了訊號量值使得待執行的操作能夠繼續向前。
    • 一個訊號中斷了semop()呼叫。發生這種情況時會返回EINTR錯誤。
    • 另一個程式刪除了semid引用的訊號量。發生這種情況時semop()會返回EIDRM錯誤
  • sem_flg:引數是一個位掩碼。

    • IPC_NOWAIT標記來防止semop()阻塞。如果semop()本來要發生阻塞的話就會返回EAGAIN錯誤。
    • SEM_UNDO標標記用來撤銷程式終止前的所有操作,即從訊號量的當前值減去總和(一個程式在一個訊號量上操作的總和,被稱為semadj)(這個標記有一定的限制,可以翻閱資料檢視)

需特別指出: semop()是原子操作,要麼立即執行所有操作,要麼堵塞直到能夠同時執行所有操作。

semtimedop()系統呼叫與semop()執行的任務一樣,但多了一個timeout引數,這個引數可以指定呼叫所阻塞的時間上限。

#define _GNU_SOURCE
#include<sys/types.h>
#include<sys/sem.h>
int semtimedop(int semid,struct sembuf *sops,unsigned int nsops,struct timespec *timeout);
//return 0 on success, or -1 on error
複製程式碼

5. 使用System V 訊號量實現二元訊號量

Boolean bsUseSemUndo = FALSE;
Boolean bsRetryOnEintr = TRUE;
int                     /* Initialize semaphore to 1 (i.e., "available") */
initSemAvailable(int semId, int semNum)
{
    union semun arg;

    arg.val = 1;
    return semctl(semId, semNum, SETVAL, arg);
}
int                     /* Initialize semaphore to 0 (i.e., "in use") */
initSemInUse(int semId, int semNum)
{
    union semun arg;

    arg.val = 0;
    return semctl(semId, semNum, SETVAL, arg);
}
/* Reserve semaphore (blocking), return 0 on success, or -1 with 'errno'
   set to EINTR if operation was interrupted by a signal handler */

int                     /* Reserve semaphore - decrement it by 1 */
reserveSem(int semId, int semNum)
{
    struct sembuf sops;

    sops.sem_num = semNum;
    sops.sem_op = -1;
    sops.sem_flg = bsUseSemUndo ? SEM_UNDO : 0;

    while (semop(semId, &sops, 1) == -1)
        if (errno != EINTR || !bsRetryOnEintr)
            return -1;

    return 0;
}
int                     /* Release semaphore - increment it by 1 */
releaseSem(int semId, int semNum)
{
    struct sembuf sops;

    sops.sem_num = semNum;
    sops.sem_op = 1;
    sops.sem_flg = bsUseSemUndo ? SEM_UNDO : 0;

    return semop(semId, &sops, 1);
}
複製程式碼

6.獲取訊號量的限制

union semun arg;
struct seminfo buf;
arg.__buf=&buf;
semctl(0,0,IPC_INFO,arg);


struct  seminfo
{
  int semmap;
  int semmni;  //系統級別的限制,限制了所能建立的訊號量識別符號的數量
  int semmns;//系統級別限制,限制了所有訊號量集中的訊號量數量。
  int semmnu;//系統級別限制,限制了訊號量撤銷結構的總數量。
  int semmsl; //一個訊號量集中能分配的訊號量的最大數量
  int semopm;  //每個semop()呼叫能夠執行的操作的最大數量。(semop(),E2BIG)
  int semume; //每個訊號量撤銷結構中撤銷條目的最大數量。
  int semusz;
  int semvmx;//一個訊號量能取的最大值。(semop(),ERANGE)
  int semaem;  //在semadj總和中能夠記錄的最大值。(semop(),ERANGE)
};
複製程式碼

相關文章