多进程系统中,有时候多个进程同时访问一个资源,可能会发生与时间有关的错误。在一个时间点只允许一个程序访问的内存区域就叫做临界区,使用信号灯可以解决临界区问题。本文介绍Linux下提供的信号灯操作有关的系统调用。
Linux信号量函数包含在头文件 sys/sem.h里,有三个。
一、int semget(key_t key, int num_sems, int sem_flags)
semget的作用: 创建一个新信号量 或者 取得一个已有的信号量
第一个参数 key_t key
第一个参数是一个整数,你可以随便指定一个正整数。例如 程序A中 调用
idA = semget(2333, num_sems, sem_flags);
semget根据参数key,也就是2333(随便指定的),新创建一个信号量,返回这个信号量的标示(也是一个整数)。这个整数是系统分配的,用在其他信号量函数中用来说明对哪一个信号量操作。
如果在进程B中,再次使用
idB = semget(2333, num_sems, sem_flags);
这时候会 取得已有的信号量,也就是A中创建的那个,idB和idA的值是一样的。他们都标示了同一个信号量。
对于相同的key,semget函数返回相同的信号量标示,该标示可以用于指定信号量。
如果在B中不使用2333而是使用别的数字,那么B中创建一个新的信号量,和A中 的没有任何关系。
第二个参数 int num_sems
这也是一个整数,用来说明需要的信号量的数目。这是什么意思呢,上面idA就标示了一个信号量,这个信号量其实是一个数组,它有num_sems个信号灯。
一般num_sems取1,一个信号量只有一个信号灯,这样可以创建多个信号量来表示多个信号灯。
如果用一个信号量标示多个信号灯,用下标0 1 2 3区分看起来不是很直观。
第三个参数 int sem_flags
这个参数是一组标志,他和fopen函数的那些 “w” “b” “r”什么的有点类似。它的最低九位2进制数字代表了这个信号量的权限信息。
一般取值为 IPC_CREAT | 0666
宏定义的IPC_CREAT可以和其他进行或操作,它既可以用于新建信号量,同时又可以用于取一个已有的信号量。
使用IPC_CREAT | IPC_EXCL 来确保新建信号量,如果信号量已经有了会返回错误。
返回值
semget的返回值 失败的时候返回-1
成功的时候返回一个正整数,它是信号量的唯一标识,用来传给下面两个函数,对该信号量进行操作。
二、int semctl(int sem_id, int sem_num, int command, …)
semctl用于直接控制信号量的信息,例如初始化一个值或删除信号量。
第一个参数:int sem_id
用来表示对哪个信号量操作,它是由上面的semget函数返回的。
第二个参数:int sem_num
用来指定对哪个信号灯进行操作。如果创建信号量的时候设置了信号量的数目不为1,那就用这个来标示
0代表第一个信号量
1代表第二个信号量 以此类推
如果创建的信号量只有1个 ,那个这个参数传入0
第三个参数:int command
说明将要采取的动作。常用的有
SETCAL 用于给信号量赋初始值,通过第四个参数semun.val给定。
IPC_RMID 用于删除信号量
这两个符号都是宏定义的整数,更多命令可以查看它的手册。
第四个参数:union semun
如果需要第四个参数的话,就是这个。这个的声明至少有这几个成员
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
}
这个声明一般包含在sem.h里面,也有可能没有,没有的话需要自己声明。
返回值
根据command的不同,返回值也不同。对于 SETVAL和IPC_RMID 成功返回0 失败返回-1
三、int semop(int sem_id, struct sembuf * sem_ops, size_t num_sem_ops)
semop函数用于对信号量进行操作。
第一个参数 int sem_id
用来表示对哪个信号量操作,它是由上面的semget函数返回的。
第二个参数 struct sembuf *sem_ops
这是一个sembuf类型的结构体,这个结构体的声明,一般包含这几个成员
struct sembuf {
short sem_num; //要处理的信号量的下标
short sem_op; //要执行的操作
short sem_flg; //操作标志
}
第一个sem_num是信号量下标,用来指定对哪个信号灯进行操作。
如果创建信号量的时候设置了信号量的数目不为1,那就用这个来标示
0代表第一个信号量
1代表第二个信号量 以此类推
如果创建的信号量只有1个 ,那个这个参数传入0
第二个 说明对信号量改变多少 1表示加1 -1表示减1
第三个 sem_flg通常为SEM_UNDO,这样操作系统会跟踪该信号量,在不使用时可以自动释放。
semop调用是一个原子操作,是一次性完成的,防止多个信号量竞争现象的出现。
第三个参数 size_t num_sem_ops
说明操作次数,一般为1 也就是操作1次。
信号量函数的进一步封装
直接用上面三个函数不免有些复杂,我们可以对我们经常用的操作封装成一个函数,例如创建信号灯,初始化信号灯以及信号灯的P、V操作还有删除信号灯。
创建信号灯
由于创建信号灯只需要一句semget函数即可,也可以不封装成自定义函数。在需要信号灯的时候我们只需要传入用来生成信号量的标示(任意正整数)以及信号量数量。写成函数其实更好,这里演示的是直接使用。
1 2 3 4 |
int sem_id; sem_id = semget((key_t)1234, 2, 0666 | IPC_CREAT); if (sem_id == -1) fprintf(stderr, "Failed to create semapore\n"); |
创建好的信号量用sem_id来标示。
第二行 1234是我们随意指定的一个整数,当然也可以用其他数字。如果在另一个程序代码中想还是用这个程序的信号量,那么就要用相同的整数。第二个参数2表示创建了2个信号量。第三个参数一般使用这个。。。
设置信号量的初始值
由于给初始值的时候需要一个临时联合体union semun类型,不妨把它写到一个更上层的函数。设置初始值需要知道信号量的标示(创建时候semget返回的值 也就是上面的sem_id),还需要对该信号量数组中的第几个操作(例如上面是2那么下标可能是0或1 分别表示第一个和第二个),还需要初始值。所以这个函数需要三个参数。
1 2 3 4 5 6 7 8 9 10 |
int set_semvalue(int sem_id, int index, int value) { union semun sem_union; sem_union.val = value; if (semctl(sem_id, index, SETVAL, sem_union) == -1) return 0; else return 1; } |
删除信号量
1 2 3 4 5 |
void del_semvalue(int sem_id) { if (semctl(sem_id, 0, IPC_RMID) == -1) fprintf(stderr, "Failed to delete semapore\n"); } |
P操作
P操作是通过调用semop函数实现的。由于需要知道信号量标识以及第几个,需要两个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int P(int sem_id, int index) { struct sembuf sem_b; sem_b.sem_num = index; sem_b.sem_op = -1; sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semapore_p failed\n"); return 0; } return 1; } |
V操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int V(int sem_id, int index) { struct sembuf sem_b; sem_b.sem_num = index; sem_b.sem_op = 1; sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semapore_v failed\n"); return 0; } return 1; } |
使用信号量保证进程同步实例
(待续)