信号量分析
以struct和union为线索来观察信号量
第一部分 semid_ds ipc_perm
内核为每个信号量集合维护一个结构体semid_ds:
struct semid_ds {
struct ipc_perm sem_perm;
unsigned short sem_nsems; /* # of semaphore in set */
time_t sem_otime; /* last-semop() time */
time_t sem_ctime; /* last-change time */
…
};
semid_ds的一个结构体成员ipc_perm是XSI IPC为每一个IPC结构设置的, 定义如下:
struct ipc_perm {
uid_t uid; /* owner’s effective user id */
gid_t gui; /* owner’s effective group id */
uid_t cuid; /* creator’s effective user id */
gid_t cgid; /* creator’s effective group id */
mode_t mode; /* access mode */
…..
};
好, 有了以上两个结构体, 我们先认识semget
#include <sys/sem.h>
int semget (key_t key; int nsems, int flag);
返回值: 若成功则返回信号量ID, 若出错则返回-1
先说semget和这两个struct的关系:
如果是创建一个新的信号量集合, 内核为这个信号量集合维护一个结构体semid_ds, 同时对semid_ds的成员进行初始化:
ipc_perm结构赋初值, ipc_perm中的mode被设置为flag中的相应权限位.
sem_nsems设置为nsems
sem_otime设置为0.
sem_ctime设置为当前时间.
另外, 注意如果创建新集合, 则必须指定nsems, 如果引用一个集合, 则nsems设定为0;
一句话概括, 就是semget创建信号量集合的时候初始化了semid_ds.
第二部分 semun和一个无名结构体
semctl参数中使用了一个union, 定义如下
union semun {
int val; /* for SETVAL */
struct semid_ds *buf; /* for IPC_STAT and IPC_SET */
unsigned short *array; /* for GETALL and SETALL */
};
还要认识一个无名结构体. (我总觉得无名结构体听起来就很cool). 每个信号量由这样一个结构体表示. 定义如下:
struct {
unsigned short semval; /* semaphore value, always >= 0 */
pid_t sempid; /* pid for last operation */
unsigned short semncnt; /* # processes awaiting semval>curval */
unsigned short semzcnt; /* # processes awaiting semval == 0 */
….
};
有必要说明一下, semget创建的是一个信号量集合, 所以semid_ds是针对这个信号量集合的. 而上面这个无名结构体是针对的一个信号量.
好, 有了以上的一个union和一个struct, 我们引出semctl的定义.
#include <sys/sem.h>
int semctl (int semid, int semnum, int cmd, … /* union semun arg */);
返回值: 有点复杂, 下面详细说明
先说semctl和semun的关系.
semctl的第四个参数为可选参数, 如果使用, 则应该为semun类型, 要注意的是semun必须显式的定义在用户的程序中. 具体用法和cmd有关系.
再说semctl和无名结构体的关系. 具体用法还是和cmd有关系.
所以, 我们必须介绍一下cmd的用法.
cmd包括十种命令, 而针对的信号量用semnum指定. 范围为[0, nsems-1]
IPC_STAT
读取semid_ds到arg.buf指向的struct中
IPC_SET
将arg.buf指向的struct设置到sem_perm.uid, sem_perm.gid和sem_perm.mode
IPC_RMID
删除信号量集合
GETVAL
返回semnum信号量的无名结构体的成员semval值
SETVAL
arg.val设置到由semnum信号量的无名结构体成员semval中
GETPID
返回无名结构体成员sempid
GETNCNT
返回无名结构体成员semncnt
GETZCNT
返回无名结构体成员semzcnt
GETALL
取该集合中所有信号量(无名结构体)的值, 存放在arg.array指向的数组中
SETALL
将arg.array指向的数组的值设置该集合所有信号量的值(无名结构体)
总结一下.
围绕union semun来说:
1. int val由SETVAL使用, semctl将会把arg.val设置到信号量的semval中.
2. struct semid_ds *buf由IPC_STAT和IPC_SET使用, 读取时将信号量集合的semid_ds读取到arg.buf中. 设置时使用arg.buf的三项内容.
3. unsigned short *array由GETALL和SETALL使用, 将读取和设置集合中的所有信号量
围绕无名结构体来说:
1. semval为R/W, 读取时直接为semop的返回值, 设置时将arg.val设置给semval
2. sempid, semncnt, semzcnt为只读, 读取时职位为semop的返回值.
一句话概括. semctl读取和设置整个信号量集合, 读取和设置信号量集合中的每个信号量, 删除信号量集合
注意. semget只是创建了信号量集合, 在使用之前必须使用semctl设置你要使用的信号量
第三部分 sembuf
semop的一个参数为struct sembuf类型, 定义如下:
struct sembuf {
unsigned short sem_num; /* member # in set [0, nsems-1] */
short sem_op; /* operation (negative, 0, or positive) */
short sem_flg; /* IPC_NOWAIT, SEM_UNDO */
};
#include <sys/sem.h>
int semop (int semid, struct sembuf semoparray[], size_t nops);
返回值: 若成功返回0, 若出错则返回
参数nops规定该数组中操作的数量(元素数)
先说明一下, 第二个参数之所以定义为当前形式, 而没有定义成struct sembuf *semoparray. 是因为semop可以执行数组中的nops个操作.
整个semop围绕sembuf来进行不同的操作.
成员sem_num指定了信号量
成员sem_op和sem_flg联合指定了semop的行为. 根据书上的描述, 如下:
1. sem_op为正, 则将sem_op加到semval上.
2. sem_op为负,
semval大于或等于sem_op的绝对值, 则从semval减去sem_op的绝对值.
semval小于sem_op的绝对值, 因为信号量为非负值, 则根据sem_flg有如下行为:
(a) 设定了IPC_NOWAIT, 则semop返回EAGAIN.
(b) 没设定IPC_NOWAIT, 则semncnt值加1, 然后进程挂起直到下列时间之一发生.
(i) semval变成大于或等于sem_op的绝对值. semncnt值减一.
(ii) 信号量被删除, semop返回EIDRM
(iii) 进程捕获到一个信号, 并从信号处理程序返回.semncnt减1, semop返回EINTR.
3. sem_op为0的情况
semval为0, 正常返回.
semval非0, 则:
(a) 指定了IPC_NOWAIT, sem_op返回EAGAIN
(b) 未指定IPC_NOWAIT, semzcnt加1, 进程挂起, 直到下列事件之一发生
(i) semval变为0, semzcnt减1
(ii) 信号量被删除, semop返回EIDRM
(iii) 进程捕获到一个信号, 并从信号处理程序返回,semzcnt减1,semop返回EINTR
4. 如果设置了SEM_UNDO, 则在调用semop时, 对semval的操作将会记录到信号量调整值上, 当前进程退出后, 内核将按调整值对信号量进行处理. 但是如果使用带有SETVAL或SETALL的semctl设置某一信号量, 则在所有进程中, 该信号量的调整值都设置为0.
书上说的很严谨, 但是带来的问题就是罗嗦. 其实我们简单的理解, 就是, semop用来获得和释放资源, 释放资源时比较简单, 获得资源时, 如果资源不足, 则根据IPC_NOWAIT标志, 挂起或者直接返回. 取消挂起有三个条件, 有足够的资源了, 信号量被删除了, 捕获并处理完了一个信号. 进程醒来后semop返回相应的值.
一句话概括, semop根据semoparray进行nops个获取和释放资源的动作.
第四部分 信号量和记录锁
书中最后对比了信号量和记录锁. 给出的结论为如果只需锁一个资源, 并且不需要使用XSI信号量的所有花哨(fancy)的功能, 则宁可使用记录锁, 尽管记录锁比信号量耗时, 但是记录锁使用简单, 且进程终止时系统会处理任何遗留下来的锁.