47 System V 信号量

SysV 信号量是以信号量集的形式出现的。

创建 SysV 信号量后需要显式初始化

在 Linux 中,创建的 SysV 信号量的 semval 会被初始化为 0,但是这个行为是不可以移植的。在其他系统中,需要手动创建,再跟着初始化,这两部分可能会出现同步错误。

使用 semctl 前需要自己提供 semun 定义

参考 man semctl,应用程序必须自己提供 semun 的定义(如果想要给 semctl 传递第 4 个参数)。这个定义并不在头文件里!

union semun {
   int              val;    /* Value for SETVAL */
   struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
   unsigned short  *array;  /* Array for GETALL, SETALL */
   struct seminfo  *__buf;  /* Buffer for IPC_INFO
                               (Linux-specific) */
};

semop() 和操作系统课程所学有什么差异?

共性:永不为负,P 和 V 都是原子操作。关于“永不为负”这一点我还去求证了一下,发现确实是自己本科学的东西记错了。

semop() 执行操作系统课程中学习的信号量操作,有一点差异。

权限检查:当 semop() 指定的信号量增量不为 0 的时候,进程就需要对信号量集有写权限;否则需要有读权限。semop() 指定信号量增量为 0 表示测试一下当前的资源量是否为负,如果为负会阻塞进程。

Wait-for-zero/increase:Man 手册上面同时有 semzcntsemncnt 两个计数。semop() 指定的信号量增量为 0 时,如果阻塞,则执行的是 wait-for-zero 操作(会增加 semzcnt 计数)。semop() 指定的信号量增量为负时,如果阻塞,则会增加 semncnt 计数,等待资源增加(不需要等待资源非负,只要申请方需要的资源可以得到满足即可)。

Each semaphore in a System V semaphore set has the following associated values:

   unsigned short  semval;   /* semaphore value */
   unsigned short  semzcnt;  /* # waiting for zero */
   unsigned short  semncnt;  /* # waiting for increase */
   pid_t           sempid;   /* PID of process that last
                                modified the semaphore value */

如果 semop() 导致程序阻塞,程序可能会因为以下原因解除阻塞:(1)信号量值增加;(2)调用线程的阻塞等待被信号中断;(3)semop() 操作的信号量集被删除。

P、V 操作量:semop() 可以对信号量集中的多个信号量同时操作,每次操作可以施加绝对值大于 1 的改变量。

semop() API

int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
              const struct timespec *_Nullable timeout);

struct sembuf 有以下字段(参考 man 手册):

   unsigned short sem_num;  /* semaphore number */
   short          sem_op;   /* semaphore operation */
   short          sem_flg;  /* operation flags */

其中 sem_flg 可以是 IPC_NOWAITSEM_UNDO。前者(在本会阻塞进程的情况下)不会阻塞进程,后者表示在进程退出后会自动将其进行的信号量操作撤销。(Stack Overflow 有人说就是因为这个原因他才会使用 System V 信号量而不是 POSIX 信号量。

System V 可以支持对信号量集中的多个信号量同时进行操作。书中 Listing 4.7.7 如下(sops 参数其实就是一个数组):

struct sembuf sops[3];

sops[0].sem_num = 0;                 /* Subtract 1 from semaphore 0 */    
sops[0].sem_op = -1;
sops[0].sem_flg = 0;

sops[1].sem_num = 1;                 /* Add 2 to semaphore 1 */
sops[1].sem_op = 2;    
sops[1].sem_flg = 0;

sops[2].sem_num = 2;                 /* Wait for semaphore 2 to equal 0 */
sops[2].sem_op = 0;
sops[2].sem_flg = IPC_NOWAIT;        /* But don't block if operation
                                       can't be performed immediately */
if (semop(semid, sops, 3) == -1) {
   if (errno == EAGAIN)             /* Semaphore 2 would have blocked */
       printf("Operation would have blocked\n");
   else
       errExit("semop");            /* Some other error */
}

多个阻塞信号量操作的处理

如果两个进程申请的资源(信号量集中的那个 / 些信号量、信号量的减少数量)相同,那么它们谁先获得资源都是有可能的。如果两个进程申请的资源不同,则要求更少的那个进程会先获得资源,书上对此的说法是“先满足条件先服务”。

信号量撤销值

内核为每个进程维护了一个 semadj 值,每次使用 semop() 修改信号量时如若有 SEM_UNDO 标记则会将信号量变化累加到 semadj 中去。当使用 semctl() SETVALSETALL 操作设置一个信号量值时,所有使用这个信号量的进程中相应的 semadj 会被清空(即设置为 0)。

clone() 时指定 CLONE_SYSVSEM 会共享信号量撤销值,创建 Pthreads 线程时就会有这个标志。调用 fork() 会清除 semadj,但是调用 exec() 不会。

如果一个进程的 semadj 大于 0,那么进程退出时,将会减少信号量的计数。如果信号量计数不够减,有些系统会减少到 0 就退出(Linux),其他实现则什么都不做。对此 SUSv3 没有做出规定。