信号量分析

以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)的功能, 则宁可使用记录锁, 尽管记录锁比信号量耗时, 但是记录锁使用简单, 且进程终止时系统会处理任何遗留下来的锁.

原文地址:https://www.cnblogs.com/lixiaofei1987/p/3200729.html