信号量
信号量原语
进程同步的主要方式之一。具体概念参考[《《操作系统概念精要》基本概念整理之进程同步篇(二)》],(https://www.jianshu.com/p/45c546d98e1c),这里不再赘述。
一般只有wait()和signal()操作,称为P,V操作。
Linux下的System V信号量的API都定义在 <sys/sem.h>中,包括三个系统调用,semget(), semop(), semctl()。他们被设计为操作一组信号量,即信号量集,而不是一个单独的信号量。因此这些接口看上去会被我们期望的要复杂一些。
semget系统调用
#include <sys/sem.h>
// 创建一个新的信号量集,或者获取一个已经存在的信号量集
// key用来标识一个全局唯一的信号量集, // 使用信号量通信的两个进程需要把这个值设置为相同的
// num_sems 指定要创建的/获取信号量集中的信号量的数目
// 如果是创建那个num_sems必须设置,如果获取可以为0
// sem_flags是一组标志,低端的9个bit 是该信号量的权限
// 其格式和含义与open的mode的参数相同, // 此外它还可以和IPC_CREAT标志按位运算创建新的信号量集合
int semget(key_t key, int num_sems, int sem_flags);
semget成功时返回一个正整数值,它是信号量集的标识符。semget失败返回-1,并设置errno。
在创建信号量集合时,与之关联的内核数据结构体为semid_ds江北创建并初始化。 semid_ds的结构的定义为:
struct ipc_perm {
key_t key; // 键值
uid_t uid; // 所有者的有效用户id
gid_t gid; // 所有者的有效组id
uid_t cuid;
git_t cgid;
mode_t mode; //访问其他填充字段
};
struct semid_ds {
struct ipc_perm sem_perm; // 信号量的操作权限
unsigned long int sem_nsems; // 该信号量集中的信号量数目
time_t sem_otime; // 最后一次调用semop的时间
time_t sem_ctime; // 最后一次调用semctl的时间
};
semop 系统调用
semop系统调用改变信号量的值,即执行P,V操作。
一般的信号量的P,V操作会修改内核中的这些变量:
unsigned short int semval; // 信号量的值
unsigned short int semzcnt; // 等待信号量变为0的进程数量
unsigned short int semncnt; // 等待信号量值增加的进程数量
pid_t sempid; // 上一次执行semop操作的进程id
semop的定义如下:
#include<sys/sem.h>
// sem_id参数是由semget 调用返回的信号量标识符
// struct sembuf {
// unsigned short int sem_num; // 信号量集中信号量的编号
// short int sem_op; // 成员操作类型, 可选值为正整数,0和负整数
// short int sem_flg; // sem_flg和sem_op共同操作决定最后的操作
// }
int semop(int sem_id, struct sembuf* sem_op, size_t num_sem_ops);
sem_num,成员是信号量中信号量的编号,0表示是信号量集中的第一个信号量。sem_op成员直到操作类型,可选值为正整数,0和 负整数。每种类型的操作的行为有收到sem_flag成员的影响,sem_flg可选值为IPC_NOWAIT和SEM_UNDO。 IPC_NOWAIT表示无论信号量操作是否成功,semop的调用都将立即返回,这类似于非阻塞I/O操作。SEM_UNDO的含义是,当进程退出时,取消正在进行的semop操作。
具体的来说,sem_op和sem_flg按如下方式来影响semop最后的执行结果:
- 如果sem_op大于0,则semop将被操作的信号量的值semval 增加sem_op。该操作要求调用进程在对被操作信号量集上拥有写权限。
- 如果sem_op等于0,表示这是一个“等待0”操作。该操作要求调用进程对呗操作信号量拥有读权限。如果此时信号量的值是0,则调用立即成功返回。如果信号量的值不是0,则semop操作失败或者阻塞进程以等待信号量变为0。
在这种情况下需要看下,sem_flg的设置。如果IPC_NOWAIT标志被指定,semop立即返回一个错误,并设置errno为EAGAIN。如果未指定IPC_NOWAIT,则信号量的值semzcnt 加1,进程被投入睡眠,知道下列3个条件之一发生:- 信号量的值变为0,此时系统将该信号量semzcnt 减1.
- 被操作的信号量所在的信号量集被进程移除,此时,semop调用失败返回,errno被设置为EIDRM。
- 调用被信号中断,此时,semop调用失败返回,errno被设置EINTR,同时系统将信号量的semzcnt减1。
- sem_op小于0,则表示对信号量进行减操作。即期望获得信号量,该操作要求调用进程对呗操作信号量集拥有写权限。如果信号的值semval大于或者等于sem_op的绝对值,则semop操作成功,调用进程立即获得信号量,并且系统将该信号量的semval减去sem_op的绝对值。如果信号量的值semval小于sem_op的绝对值,则,semop失败返回或者阻塞等待信号量可用。在这种情况下,IPC_NOWAIT的标志被指定时,semop立即返回一个错误,并设置errno为EAGAIN。如果为指定IPC_NOWAIT标志,则信号量的semncnt加1,进程被投入睡眠知道下面三个条件满足:
1. 信号量的值semval大于等于sem_op的绝对值,此时信号量的semncnt减1,并将semval减去sem_op的绝对值。
2. 被操作信号量所在的信号量集被进程删除,调用返回失败,errno被设置为EIDRM;
3. 调用被信号中断,此时semop调用失败返回,errno被设置为EINTR,同时系统将semncnt值减1。
semop系统调用的第三个参数是num_sem_ops指定要执行的操作个数,即,sem_ops数组中元素的个数。sem_op对数组sem_ops中每个元祖顺序执行操作,并且该操作为原子操作.
semctl 系统调用
semctl系统调用允许调用者对信号量进行直接控制。
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);
sem_id参数是由semget 返回的信号量集标识符。
sem_num, 指定被操作的信号量在信号量集中的编号。
command 参数执行要执行的命令。
有的命令需要调用者传递第四个参数,第四个是用户自定义的参数,但<sys/sem.h>中定义了它的推荐格式。
union semun {
int val; // 用于SETVAL命令
struct semid_ids* buf; // 用于IPC_STAT和IPC_SET命令
unsigned short* array; // 用于GETALL和SETALL命令
struct seminfo* _buf; // 用于IPC_INFO命令
};
struct seminfo {
int semmap; // Linux 内核没有使用
int semmni; // 系统最多可以拥有的信号量集数目
int semmns; // 系统最多可以拥有的信号量的数目
int semmnu; // Linux 内核没有使用
int semmsl; // 一个信号量集最多允许包含的信号量数目
int semopm; // semop一次最多能执行的sem_op操作数目
int semume; // Linux内核没有使用
int semusz; // sem_undo 结构体的大小
int semvmx; // 最大允许的信号量值
int semaem; // 最多允许的UNDO的次数
};
以下是semctl的command的参数:
信号量semctl.png使用信号量进行通信:
代码来源于:Linux高性能服务器编程
// semget的调用者可以给其key传递一个特殊的键值IPC_PRIVATE(值为0)
// 这样无论信号量是否存在,semget都将创建一个新的信号量。使用该键值
// 创建的信号量
// 并非像它的名字那样是进程私有的。其他进程,尤其子进程,也有办法来访问这个信号量。
// 下面的例子就是在父子进程间使用一个IPC_PRIVATE信号量来进行同步。
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
union semun {
int val; // 用于setvalue;
struct semid_ds* buf; // 用于ipc_stat 和ipc_set 命令
unsigned short* array; // 用于getall 和setall 命令
struct seminfo* __buf; // 用于ipc_info命令
};
// op为-1时执行p操作,op为1执行v操作。
// semop操作
void pv( int sem_id, int op) {
struct sembuf sem_b;
sem_b.sem_num = 0; // 信号集中编号为1的信号量
sem_b.sem_op = op; // 执行op操作, 大于0,执行+1操作, 小于0 执行-1 操作
sem_b.sem_flg = SEM_UNDO; // 信号量操作进程退出时取消正在进行op操作。
semop(sem_id, &sem_b, 1); // 操作信号量的个数只有一个
}
int main(int argc, char* argv[]) {
// 创建一个信号量集,它创建一个新的信号集, 权限为读写
int sem_id = semget(IPC_PRIVATE, 1, 0666);
// semctl中第四个参数的格式
union semun sem_un;
sem_un.val = 1;
// SETVAL社会semun.buf的数据成员复制到信号量集关联的内核数据结构中,
// 同时内核数据中的semid_ds。sem_ctime被更新。
semctl(sem_id, 0, SETVAL, sem_un);
// 复制一个新进程
pid_t pid = fork();
if (pid < 0) {
return -1;
} else if (pid == 0) {
printf("child try to get binary sem\n");
// 在父子进程共享IPC_PRIVATE 信号量的关键在于二者都可以操作该信号量的标识符
// sem_id;
pv (sem_id, -1);
printf("child get the sem and would releases it after 20 seconds\n");
sleep(20);
pv (sem_id, 1);
exit(0);
} else {
printf("parents try to get binary sem\n");
pv (sem_id, -1);
printf("parent get the sem and would releases it after 20 seconds\n");
sleep(20);
pv (sem_id, 1);
}
waitpid(pid, NULL, 0);
// 立即移除信号集,唤醒所有等待该信号集的进程
semctl(sem_id, 0, IPC_RMID, sem_un);
return 0;
}
网友评论