前言
信号量与信号量集的概念如下。
信号量:是信号量集中的一个元素,就像整型数组中的一个元素。
信号量集:由若干个信号量组成的集合,就像整型数组是由多个整数组成的一样。
每个信号量都有它的值:非负整数,就像数组中的每个元素都有它的值。同时每个信号量也有它在这个信号量集中的编号,就像数组中的每个元素都有下标一样。数组下标从 0 开始,信号量的编号也从 0 开始。总之,信号量集和数组很类似。
信号量(也叫信号灯)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。
信号量是进程/线程同步的一种方式,有时我们需要保护一段代码,使它每次只能被一个执行进程/线程运行,这种工作就需要一个二进制开关;有时需要限制一段代码可以被多少个进程/线程执行,这就需要用到关于计数信号量。
信号量开关是二进制信号量的一种逻辑扩展,两者实际调用的函数都是一样的。
信号量分为以下三种:
- System V 信号量,在内核中维护,可用于进程或线程间的同步,常用于进程的同步。
- Posix 有名信号量,一种来源于 POSIX 技术规范的实时扩展方案 (POSIX Realtime Extension) ,可用于进程或线程间的同步,常用于线程。
- Posix 基于内存的信号量,存放在共享内存区中,可用于进程或线程间的同步。
为了获得共享资源,进程需要执行下列操作:
① 测试控制该资源的信号量。
② 若信号量的值为正,则进程可以使用该资源。然后将信号量值减 1 ,表示它使用了一个资源单位。此进程使用完共享资源后对应的信号量应该加1,以便其他进程使用。
③ 若对信号量进行减 1 时,信号量的值为 0,则进程进入阻塞休息状态,直至信号量值大于 0。进程被唤醒,返回第 ① 步。
为了正确的实现信号量,信号量值的测试及减 1 操作应当是原子操作(原子操作是不可分割的,在执行完毕前不会被任何其他任务或事件中断),为此信号量通常是在内核中实现的。
一、System V IPC 机制:信号量
函数原型为:
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
int semget(key_t key, int nsems, int flag);
int semop(int semid, struct sembuf *sops, size_t nops);
int semctl(int semid, int semnum, int cmd, union semun arg);
1. semget 函数
函数 semget 创建一个信号量集或访问一个已存在的信号量集。
参数 key 是唯一标识一个信号量的关键字,如果为 IPC_PRIVATE (值为 0,创建一个只有创建者进程才可以访问的信号量),表示创建一个只由调用进程使用的信号量,非 0 值的 key (可以通过 ftok 函数获得)表示创建一个可以被多个进程共享的信号量值。
参数 nsems 指定需要使用的信号量数目。如果是创建新集合,则必须指定 nsems 。如果引用一个现存的集合,则将 nsems 指定为 0。
参数 flsg 是一组标志,其作用与 open 函数的各种标志很相似。它低端的 9 个位是该信号量的权限,其作用相当于文件的访问权限。此外,它们还可以与键值 IPC_CREAT 按位或操作,以创建一个新的信号量。即使在设置了 IPC_CREAT 标志后给出的是一个现有的信号量的关键字,也并不是一个错误。我们也可以通过 IPC_CREAT 和 IPC_EXCL 标志的联合使用确保自己将创建出一个新的独一无二的信号量出来,如果该信号量已经存在,就会返回一个错误。
返回值:成功时,函数返回一个被称为信号量集标识符的整数,semop 和 semctl 会使用它;出错时,返回 -1。
2. semop 函数
函数 semop 用于改变信号量对象中各个信号量的状态。
参数 semid 是由 semget 返回的信号量标识符。
参数 sops 是指向一个结构体数组的指针,可以指向单个或多个结构体变量,每个数组元素至少包含以下几个成员:
struct sembuf{
short sem_num; //要操作的信号量在信号量集中的编号,第一个信号量的编号是 0
short sem_op; //sem_op 成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是 -1,也就是 p(减 1 )操作,它等待信号量变为可用;一个是 +1,也就是 v(加 1 )操作,它发送信号通知信号量现在可用
short sem_flg; //通常设为 SEM_UNDO,程序结束,信号量为 semop 调用前的值
};
这段话很关键:它可以一次对一个信号量进行操作,此时数组长度为 1,也可以一次操作多个信号量。如果一次操作多个信号量,每个信号量按照这个数组各个元素指定的编号和值进行改变。
参数 nops 为 sops 指向的 sembuf 结构数组的长度。
返回值:成功时,返回 0;失败时,返回 -1 。
3. semctl 函数
函数 semctl 用来直接控制信号量信息。
参数 semid 是由 semget 返回的信号量标识符。
参数 semnum 为要进行操作的集合中信号量的编号,当要操作到成组的信号量时,从 0 开始。一般取值为 0,表示这是第一个也是唯一的一个信号量。
参数 cmd 为执行的操作。
通常为:
IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)、
GETVAL(根据 semun 指定的编号返回相应信号的值,此时该函数返回值就是用户要获得的信号量的值,不再是 0 或 -1)、
SETVAL(根据 semun 指定的编号设定相应信号的值)、
GETALL(获取所有信号量的值,此时第 2 个参数为 0,并且会将所有信号的值存入 semun.array 所指向的数组的各个元素中,此时需要用到第 4 个参数 union semun arg)、
SETALL(将 semun.array 指向的数组的所有元素的值设定到信号量集中,此时第 2 个参数为 0,此时需要用到第 4 个参数 union semun arg)等。
参数 arg 是一个 union semun 类型(具体的需要由程序员自己定义),它至少包含以下几个成员:
union semun{
int val; /*Value for SETVAL*/
struct semid_ds *buf; /*Buffer for IPC_STAT,IPC_SET*/
unsigned short *array; /*Array for GETALL,SETALL*/
}
返回值:成功时,返回 0;失败时,返回 -1。
二、Posix 有名信号量
Posix 有名信号量可用于进程和线程间通信,通常用于多线程的同步和互斥,其函数原型为:
#include <semaphore.h>
sem_t *sem_open(const char *pathname, int flags, mode_t mode, int init);
int sem_close(sem_t *psem);
sem_wait(sem_t *psem); //P 操作(减一)如果信号量已减为0,则会阻塞,知道条件满足为止
sem_post(sem_t *psem); //V 操作(加一)
P 操作和 V 操作都是原子性的,如果操作的条件不满足,就会阻塞。
注意:在编译过程中必须加上 -lpthread 或 -lrt 选项。