Linux 進程間通訊之System V 信號量

1.概述

  • System V 信號量不是用來在進程間傳輸數據的。相反,它們用來同步進程的動做。信號量的一個常見用途是同步一塊共享內存的訪問以防止一個進程在訪問共享內存的同時另外一個進程更新這塊內存的狀況。linux

  • 一個信號量是一個由內核維護的整數,其值被限制爲大於或等於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()返回的鍵bash

  • nsems: 指定集合中信號量的數量,而且其值必須大於0.若是使用semget()來獲取一個既有集的標識符,那麼nsems必需要小於或等於集合的大小(不然發生EINVAL錯誤)。沒法修改一個既有集中的信號量數量。數據結構

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

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

建立一個System V 信號量oop

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: 參數指定了需執行的操做ui

    常規控制操做:this

    • IPC_RMID:當即刪除信號量集及其關聯的semid_ds數據結構。全部因semop()調用操做堵塞的進程都會當即喚醒,並返回EIDRM錯誤。這操做不須要arg參數。spa

    • 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)
};
複製代碼
相關文章
相關標籤/搜索