Linux 进程间通信之System V 信号量

1,864 阅读10分钟

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)
};