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 手册上面同时有 semzcnt
和 semncnt
两个计数。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_NOWAIT
和 SEM_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()
SETVAL
或 SETALL
操作设置一个信号量值时,所有使用这个信号量的进程中相应的 semadj
会被清空(即设置为 0)。
clone()
时指定 CLONE_SYSVSEM
会共享信号量撤销值,创建 Pthreads 线程时就会有这个标志。调用 fork()
会清除 semadj
,但是调用 exec()
不会。
如果一个进程的 semadj
大于 0,那么进程退出时,将会减少信号量的计数。如果信号量计数不够减,有些系统会减少到 0 就退出(Linux),其他实现则什么都不做。对此 SUSv3 没有做出规定。