这篇文章既不介绍什么是信号量,也不解释为什么要用信号量,这些都是可以网上随便搜得到的,这篇文章主要讲解如何使用Linux下的信号量。Linux下的信号量是用系统调用直接实现的,它既可以用作binary semaphore
也可以用作counting semaphore
。为方便起见,这里我用binary semaphore
作为例子进行讲解。Linux的信号量使用了3个系统调用 - semget()
, semctl()
和semop()
。因为是系统调用而不是封装好的API,因此使用起来还是要麻烦些,我们下面就来看看如何使用这些系统调用。
相关系统调用
semget()
semget()
函数既可以用于获取之前创建的信号量集合,也可以用于创建新的信号量集合。semget()
的函数原型如下:
1 | // 成功返回信号量集合的标识符,否则返回-1 |
key
是绑定在信号量集合上的,我们可以用key
来寻找已经创建的信号量集合,或者用于创建并绑定新的信号量集合。注意,key
和函数的返回值(信号量集合)不是一样的。nsems
表示该信号量集合有多少个信号量,如果用信号量来创建binary semaphore
,我们只需要将nsems
设置为1。semflg
是设置信号量集合的标志位,其中最低9位是信号量集合的访问权限(和文件一样,3个8进制数字,比如0777)。其他位和文件创建的标志位类似,比如IPC_CREAT
是创建新的信号量集合(文件是O_CREAT
),在使用信号量前,我们都需要创建信号量集合。
在调用该系统调用后,与信号量集合绑定的数据结构semid_ds
也被初始化。
semop()
semop()
用于操作信号量,简单来说就是对信号量的计数器进行加减。函数原型如下:
1 | int semop(int semid, struct sembuf *sops, size_t nsops); |
semid
是信号量集合的标识符,注意,这不是semget()
中的key
而是这个函数的返回值。nsops
是有多少个信号量需要被操作,在我们的例子中只有1个信号量需要被操作。sops
是对信号量操作的具体命令,它是一个struct sembuf
类型的结构体,一共有nsops
个这样的结构体:
1 | unsigned short sem_num; /* semaphore number */ |
sem_num
是从0开始计数的,表示是第几个信号量;sem_flg
可以选择IPC_NOWAIT
和SEM_UNDO
,SEM_UNDO
表示在进程结束后,内核会自动释放没有主动释放的信号量。sops
是按照数组的顺序执行的,并且是原子的,如果所有的信号量不能同时操作,那么就不进行操作。
sem_op
的使用很简单,但具体如何设置、不同设置有什么不同却比较麻烦。在binary semaphore
的例子中,sem_op
设置为1就是信号量计数器加1;设置为-1就是信号量计数器减1,下面具体讲讲sem_op
设置背后的故事。
除了上面提到的sembuf
结构体,每个信号量还对应一系列变量:
1 | unsigned short semval; /* semaphore value */ |
之前提到的信号量计数器就是semval
,semzcnt
是等待semval
为0的进程数,semncnt
是等待semval
增加的进程数。semop()
对信号量的操作都会改变这些变量。
如果sem_op
设置为正数,那么每次操作后semval
将变成semval + sem_op
;如果sem_flg
设置了SEM_UNDO
,那么semadj
将变成semadj - sem_op
。进程需要有写的权限才能修改信号量。
如果sem_op
设置为负数,情况要复杂些:
- 如果
abs(sem_op)
小于等于semval
,那么semval
将变成semval - abs(sem_op)
;如果sem_flg
设置了SEM_UNDO
,那么semadj
将变成semadj + abs(sem_op)
; - 如果
abs(sem_op)
大于semval
并且sem_flg
设置了IPC_NOWAIT
,那么将返回错误码EAGAIN
,并且sem_op
的操作不会进行; - 如果2中没有设置
IPC_NOWAIT
,那么semval
将变成semval - abs(sem_op)
,同时semncnt
变成semncnt + 1
,并且进程会进入sleep
状态。当semval >= abs(sem_op)
时,进程会被唤醒。
其他具体细节请参考man semop
。
从加锁和解锁的角度进行思考,当semval = 0
时,如果操作是sem_op = -1
,那么semval
将变成-1,因此进程进入睡眠状态:这就是加锁的操作;同理,sem_op = 1
是解锁的操作。
semctl()
通过上面两个系统调用,我们知道如何创建信号量以及如何操作信号量,下面这个系统调用可以初始化信号量以及删除信号量。我们先来看看semctl()
的函数原型:
1 | int semctl(int semid, int semnum, int cmd, ...); |
semctl()
支持的命令有很多,这里我们主要介绍两个 - IPC_RMID
和SETVAL
。semid
是信号量集合的索引,semnum
是其中第几个信号量(从0开始),cmd
就是命令。这个函数是可变长度参数,有些命令是4个参数,第4个参数是union semun
,这个union
定义如下,注释中注明了哪些命令要用这个union
:
1 | union semun { |
使用SETVAL
命令时,我们就是通过val
给semval
赋值,用于初始化信号量。我们这里看看如何使用semctl()
来初始化和删除信号量:
1 | // 初始化信号量 |
实例
实例请参考从 0 开始学习 Linux 系列之「24.信号量 semaphore」和Linux进程间通信(六)—信号量通信之semget()、semctl()、semop()及其基础实验,上面的代码也来自这两篇博客。