从零到负一

Linux编程基础3 - 进程间通信(信号量)

2020/06/22

这篇文章既不介绍什么是信号量,也不解释为什么要用信号量,这些都是可以网上随便搜得到的,这篇文章主要讲解如何使用Linux下的信号量。Linux下的信号量是用系统调用直接实现的,它既可以用作binary semaphore也可以用作counting semaphore。为方便起见,这里我用binary semaphore作为例子进行讲解。Linux的信号量使用了3个系统调用 - semget(), semctl()semop()。因为是系统调用而不是封装好的API,因此使用起来还是要麻烦些,我们下面就来看看如何使用这些系统调用。

相关系统调用

semget()

semget()函数既可以用于获取之前创建的信号量集合,也可以用于创建新的信号量集合。semget()的函数原型如下:

1
2
// 成功返回信号量集合的标识符,否则返回-1
int semget(key_t key, int nsems, int semflg);

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
2
3
unsigned short sem_num;  /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */

sem_num是从0开始计数的,表示是第几个信号量;sem_flg可以选择IPC_NOWAITSEM_UNDOSEM_UNDO表示在进程结束后,内核会自动释放没有主动释放的信号量。sops是按照数组的顺序执行的,并且是原子的,如果所有的信号量不能同时操作,那么就不进行操作。

sem_op的使用很简单,但具体如何设置、不同设置有什么不同却比较麻烦。在binary semaphore的例子中,sem_op设置为1就是信号量计数器加1;设置为-1就是信号量计数器减1,下面具体讲讲sem_op设置背后的故事。

除了上面提到的sembuf结构体,每个信号量还对应一系列变量:

1
2
3
4
unsigned short  semval;   /* semaphore value */
unsigned short semzcnt; /* # waiting for zero */
unsigned short semncnt; /* # waiting for increase */
pid_t sempid; /* process ID of [the] last operation */

之前提到的信号量计数器就是semvalsemzcnt是等待semval为0的进程数,semncnt是等待semval增加的进程数。semop()对信号量的操作都会改变这些变量。

如果sem_op设置为正数,那么每次操作后semval将变成semval + sem_op;如果sem_flg设置了SEM_UNDO,那么semadj将变成semadj - sem_op。进程需要有写的权限才能修改信号量。

如果sem_op设置为负数,情况要复杂些:

  1. 如果abs(sem_op)小于等于semval,那么semval将变成semval - abs(sem_op);如果sem_flg设置了SEM_UNDO,那么semadj将变成semadj + abs(sem_op)
  2. 如果abs(sem_op)大于semval并且sem_flg设置了IPC_NOWAIT,那么将返回错误码EAGAIN ,并且sem_op的操作不会进行;
  3. 如果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_RMIDSETVALsemid是信号量集合的索引,semnum是其中第几个信号量(从0开始),cmd就是命令。这个函数是可变长度参数,有些命令是4个参数,第4个参数是union semun,这个union定义如下,注释中注明了哪些命令要用这个union

1
2
3
4
5
6
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) */
};

使用SETVAL命令时,我们就是通过valsemval赋值,用于初始化信号量。我们这里看看如何使用semctl()来初始化和删除信号量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 初始化信号量
int set_sem(int sem_id) {
union semun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1) {
fprintf(stderr, "Failed to set sem\n");
return 0;
}
return 1;
}

// 删除信号量
void del_sem(int sem_id) {
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete sem, sem has been del.\n");
}

实例

实例请参考从 0 开始学习 Linux 系列之「24.信号量 semaphore」Linux进程间通信(六)—信号量通信之semget()、semctl()、semop()及其基础实验,上面的代码也来自这两篇博客。

CATALOG
  1. 1. 相关系统调用
    1. 1.1. semget()
    2. 1.2. semop()
    3. 1.3. semctl()
  2. 2. 实例