对信号量的操作也就是PV操作。两者含义如下:
- P(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。
- V(SV),如果有其他进程因为等待SV而挂起,则唤醒;如果没有,则SV加1。
接下来的三个系统调用都是操作一组信号量,也就是信号集,而不是单个信号量。
一、semget系统调用
semget系统调用创建一个新的信号量集,或者获取一个已经存在的信号量集。
#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
其中参数如下:
参数 | 含义 |
---|---|
key | 用来标识一个全局唯一的信号量集(就像文件名全局唯一表示一个文件一样),要通过信号量通信的进行需要使用相同的键值来创建/获取信号量 |
num_sems | 指定要创建/获取的信号量集中信号量的数目。如果创建信号量集,则该值必须指定,要获取已经存在的信号量集的大小,则可以为0 |
sem_flags | 指定一组标志,其低端的9个比特是该信号量的权限 |
semget不会因为信号量已经存在产生错误,但是如果我们使用IPC_CREAT和IPC_EXCL来确保创建一组新的唯一的信号量集时,可能会产生错误并设置errno为EEXIST。semget成功时返回信号量集的标识符,失败是返回-1并设置errno。
使用semget创建信号量集,则与之关联的内核数据结构体semid_ds也会被创建并初始化。其定义如下:
#include <sem.h>
struct ipc_perm{
key_t key; /*键值*/
uid_t uid; /*所有者的有效用户ID*/
gid_t gid; /*所有者的有效组ID*/
uid_t cuid; /*创建者的有效用户ID*/
gid_t cgid; /*创建者的有效组ID*/
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的时间*/
};
semget对semid_ds结构体的初始化包括:
- 将sem_perm.suid和sem_perm.uid设置为调用进程的有效用户ID。
- 将sem_perm.cgid和sem_perm.gid设置为调用进程的有效组ID。
- 将sem_perm.mode的最低9位设置为sem_flags参数的最低9位。
- 将sem_nsems设置为num_sems。
- 将sem_otime设置为0。
- 将sem_ctime设置为当前系统时间。
二、semop系统调用
semop系统调用改变信号量的值,即执行PV操作。
首先下面是一些与每个信号量关联的一些重要的内核变量:
unsigned short semval; /*信号量的值*/
unsigned short semzcnt; /*等待信号量值变为0的进程数量*/
unsigned short semncnt; /*等待信号量值增加的进程数量*/
pid_t sempid; /*最后一次执行semop操作的进程ID*/
semop的定义如下:
#include <sys/sem.h>
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
- 1、sem_id是由semget调用返回的信号量集标识符,用于指定被操作的目标信号量集。
- 2、sem_ops指向一个sembuf类型的数组,其定义为:
struct sembuf{
unsigned short int sem_num;
/*sem_num指的是信号量集中信号量的编号,0代表第一个信号量*/
short int sem_op;
/*sem_op指定操作类型,可选值为正整数,0,负整数*/
short int sem_flg;
/*sem_flg可选值为IPC_NOWAIT和IPC_UNDO*/
};
sem_op和sem_flg相互影响。具体来说如下:
-
如果sem_op大于0,则semop将被操作的信号量semval增加sem_op。该操作要求调用进程对被操作信号量集拥有写权限。
- 此时若设置了SEM_UNDO标志,则系统将更新进程的semadj变量。
-
如果sem_op等于0,表示这是一个等到”0“的操作。该操作要求进程对被操作信号量拥有读权限。
- 如果此时信号量为0,则调用立即成功返回。
- 如果此时信号量不是0,则semop失败返回或者阻塞进程以等待信号量变成0
- 在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errno为EAGAIN。
- 在这种情况下,当IPC_NOWAIT标志未被指定时,信号量的semzcnt值加1,进程被投入睡眠直到以下三种情况发生:
- 信号量的值semval变为0,此时系统将该信号量的semzcnt减1,
- 被操作信号量所在的信号量集被进程移除,此时semop调用失败返回,errno被设置为EIDRM
- 调用被信号中断,此时semop调用失败返回,errno被设置为EINTR,同时将该信号量的semzcnt减1
-
如果sem_op小于0,则表示对信号量进行减操作。该操作要求调用进程对被操作信号量拥有写权限。
- 如果信号量的值semval大于等于sem_op的绝对值,则semop成功,调用进程立即获得信号量,并且系统将该信号量的semval的值将去sem_op的绝对值。此时如果设置了SEM_UNDO标志,则系统将更新semadj变量。
- 如果信号量的值semval小于sem_op的绝对值,则semop失败返回或者阻塞以等待信号量可用。
- 在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errno为EAGAIN,
- 在这种情况下,如果未指定IPC_NOWAIT标志,则信号量的semncnt值加1,进程被投入睡眠直到以下三种情况发生:
- 信号量的值semval变得大于等于sem_op的绝对值,同时SEM_UNDO标志被设置,系统更新semadj变量,
- 被操作信号量所在信号集被进程移除,此时semop调用失败返回,errno被设置为EIDRM
- 调用被信号中断,此时semop调用失败返回,errno被设置为EINTR,同时系统将该信号量的semncnt减1
-
3、num_sem_ops指定要执行操作的个数,即sem_ops数组中元素的个数。semop对数组sem_ops中的每个成员按照数组顺序依次执行操作,并且该过程为原子操作,主要是为了避免出现竞态条件。
semop成功时返回0,失败返回-1并设置errno,失败的时候,sem_ops数组中指定的所有操作都不被执行。
三、semctl系统调用
semctl系统调用允许调用者对信号量进行直接控制,定义如下:
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);
- sem_id是由semget调用返回的信号量集标识符,用以被指定的信号量集。
- sem_num指定被操作的信号量在信号量中的编号。
- command参数指定要执行的命令,command参数在P248。
- 有的命令需要第四个参数,sys/sem.h推荐的格式如下:
union semun{
int val; /*用于SETVAL命令*/
struct semid_ds* 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;/*最大允许的信号量值*/
/*最多允许的UNDO次数(带SEM_UNDO标志的semop操作的次数)*/
int semaem;
};
四、特殊键值IPC_PREVATE
semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE(值为0),这样无论该信号量是否已经存在,semget都将创建一个新的信号量,注意的是,使用该键值创建的信号量并非是私有的。
《Linux高性能服务器编程》学习笔记