要掌握信号量,只要熟练掌握对信号量的请求和释放(归还)就行了,也就是 semop 函数,本质上它就是用来对信号量进行加或减的操作。当然,semctl 也很重要,它主要用来获取和设置 ipc 内核对象,另外它也可以获取和设置信号量的值。
1. 创建和获取信号量
函数 semget 主要是用来创建或获取信号量的 ipc 内核对象,同时返回其 id 号。
- 函数原型
int semget(key_t key, int nsems, int
参数 nsems 表示创建几个信号量。
- 例
下面的代码表示创建一个 ipc 内核对象,包含 3 个信号量。
int id = semget(0x8888, 3, IPC_CREAT | IPC_EXCL | 0664);
2. 设置和获取信号量值
这里主要使用函数 semctl,命令可以是 SETVAL,也可以是 SETALL。前者用来设置某个信号量的值,后者表示设置所有信号量的值。
命令 GETVAL 和 GETALL 用来获取信号量的值。
- semctl 函数原型
int semctl(int semid, int semnum, int cmd, union semun);
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
2.1 设置值
- 设置第2个信号量的值为 5
semctl(id, 2, SETVAL, 5);
- 设置所有信号量的值
// 将 3 个信号量的值分别设置为 3, 6, 9
unsigned short vals[3] = {3, 6, 9};
// 注意:这时候第二个参数被忽略
semctl(id, 0, SETALL, vals);
2.2 获取值
- 获取第 1 个信号量的值
// 注意这时候第 4 个参数就不用写了,获取的值通过返回值返回
int val = semctl(id, 1, GETVAL);
- 获取所有信号量的值
unsigned short vals[3];
// 这时候第二个参数被忽略,获取的值保存到 vals 数组
semctl(id, 0, GETALL, vals);
3. 请求和释放信号量
主要函数是 semop。当请求的信号量值 > 0 时,semop 直接返回,否则会阻塞,直到信号量值大于 0。如果是释放(归还)资源,semop 直接返回。
int semop(int semid, struct sembuf *sops, unsigned nsops);
该函数第二个参数是一个数组,第三个参数表示数组大小。第二个参数的结构体如下:
struct sembuf {
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
- sem_num : 操作第几个信号量
- sem_op:为一个短整型,操作完成后,这个数值加到信号量上
- sem_flg:可选项,一般为 0
- IPC_NOWAIT:无论请求的资源有没有,立即返回。如果没有资源,errno 设置为 EAGAIN.
- SEM_UNDO:如果设置该值,当进程结束后,该进程执行的所有的操作全部撤销。
3.1 请求资源(P 操作)
- 请求 3 个资源(将信号量值减 3)
struct sembuf op;
op.sem_num = 2; // 请求第2个资源(信号量)
op.sem_op = -3;
op.sem_flg = 0;
semop(id, &op, 1);
- 同时操作多个信号量
struct sembuf ops[3];
op[0].sem_num = 0; // 请求第 0 个资源(信号量)
op[0].sem_op = -1; // 申请 1 个资源
op[0].sem_flg = 0;
op[1].sem_num = 1; // 请求第 1 个资源(信号量)
op[1].sem_op = -5; // 申请 5 个资源
op[1].sem_flg = 0;
op[2].sem_num = 2; // 请求第 2 个资源(信号量)
op[2].sem_op = -3; // 申请 3 个资源
op[2].sem_flg = 0;
semop(id, ops, 3);
当然上面的写法是给新手看的,实际上可以这样写:
struct sembuf ops[3] = {
{0, -1, 0},
{1, -5, 0},
{2, -3, 0}
};
semop(id, ops, 3);
3.2 释放(归还)资源(V 操作)
- 释放 2 个资源(将信号量值加 2)
struct sembuf op;
op.sem_num = 1; // 请求第 1 个资源(信号量)
op.sem_op = 2;
op.sem_flg = 0;
semop(id, &op, 1);
- 同时操作多个信号量
struct sembuf ops[3] = {
{0, 4, 0}, // 释放 4 个 0 号资源
{1, 2, 0}, // 释放 2 个 1 号资源
{2, 5, 0} // 释放 5 个 2 号资源
};
semop(id, ops, 3);
4. 实例
- 代码
// semop.c
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#define R0 0
#define R1 1
#define R2 2
void printSem(int id) {
unsigned short vals[3] = { 0 };
semctl(id, 3, GETALL, vals);
printf("R0 = %d, R1= %d, R2 = %d\n\n", vals[0], vals[1], vals[2]);
}
int main() {
int id = semget(0x8888, 3, IPC_CREAT | IPC_EXCL | 0664);
// 打印信号量值
puts("信号量初始值(默认值)");
printSem(id);
// 1. 设置第 2 个信号量值
puts("1. 设置第 2 个信号量(R2)值为 20");
semctl(id, 2, SETVAL, 20);
printSem(id);
// 2. 同时设置 3 个信号量的值
puts("2. 同时设置 3 个信号量的值为 12, 5, 9");
unsigned short vals[3] = {12, 5, 9};
semctl(id, 0, SETALL, vals);
printSem(id);
// 3. 请求 2 个 R0 资源
puts("3. 请求 2 个 R0 资源");
struct sembuf op1 = {0, -2, 0};
semop(id, &op1, 1);
printSem(id);
// 4. 请求 3 个 R1 和 5 个 R2
puts("4. 请求 3 个 R1 和 5 个 R2");
struct sembuf ops1[2] = {
{1, -3, 0},
{2, -5, 0}
};
semop(id, ops1, 2);
printSem(id);
// 5. 释放 2 个 R1
puts("5. 释放 2 个 R1");
struct sembuf op2 = {1, 2, 0};
semop(id, &op2, 1);
printSem(id);
// 6. 释放 1 个 R0, 1 个 R1,3 个 R2
puts("6. 释放 1 个 R0, 1 个 R1,3 个 R2");
struct sembuf ops2[3] = {
{0, 1, 0},
{1, 1, 0},
{2, 3, 0}
};
semop(id, ops2, 3);
printSem(id);
// 7. 删除 ipc 内核对象
puts("7. 删除 ipc 内核对象");
semctl(id, 0, IPC_RMID);
return 0;
}
- 编译和运行
$ gcc semop.c -o semop
$ ./semop
- 结果
图1 结果
修正:图 1 中第 6 步更正为:释放 1 个 R0, 1 个 R1, 3 个 R2
5. 总结
- 掌握设置和获取信号量值
- 掌握请求和释放信号量
练习:尝试将请求资源封闭成 P 操作,释放资源封装成 V 操作。函数接口可声明如下:
// 请求 1 个第 semnum 个信号量
void P(id, semnum);
// 释放 1 个第 semnum 个信号量
void V(id, semnum);