1. 什么是信号量
前面学过的System V IPC两种进程通信方式:共享内存和消息队列都是用于在进程间对共享区域的数据的交换传递来实现进程间通信,如果有多个进程同时向共享内存写数据可能就会出现数据混乱的问题。
信号量也是System V IPC的一种通信方式,和其他IPC方式不同的是,信号量主要是为了防止多个进程间访问共享数据时,产生竞争等一系列问题,信号量就是用来进程同步的。
linux下有两种信号量:
-
内核信号量,一般由内核控制使用
-
用户信号量,用户信号量分为POSIX信号量和System V信号量
这里我们主要讨论System V信号量。
2. 信号量IPC对象
对于信号量,系统也在内核空间维护了一个IPC对象,即semid_ds结构体,具体定义如下:
struct semid_ds
{
struct ipc_perm sem_perm; /* 信号量集的操作权限 */
struct sem *sem_base; /* 某个信号量数组的指针,当前信号量集中的每个信号量对应其中一个sem结构的数组元素 */
ushort sem_nsems; /* sem_base 数组的个数 其中ushort为unsigned short类型 */
. . . . . .
};
从上可以看到这个IPC对象主要有sem_perm结构体和sem_base结构体,其中sem_perm结构体是用于对信号量的操作权限,而sem_base结构体是可以认为是一个信号量数组。
sem_base数组中最多有25个元素,每个元素都是一个信号量,为了方便理解,可以认为一个信号量集合,也就是说一个信号量集合最多只有25个信号量。,且数据类型为struct sem。
信号量集合内核示意图:
以下是内核维护的信号量struct sem结构体部分信息:
struct sem {
int semval; /* 信号量当前的值 */
int sempid; /* 最后一次操作的进程pid */
struct list_head sem_pending; /* pending single-sop operations */
. . . . . .
};
信号量可以把它理解为类似一个计数器这样的东西,它可以控制进程对资源的访问,当进程在占用资源时,信号量值就会减少,当进程释放资源时,信号量值就会增加。可以使用函数来进行控制,这种函数要么执行一次完整的操作,要么干脆就不执行,这就是原子操作(原子操作的意思就是这个操作是一个完整的操作,具有不可再分割性)。如果资源用完了,计数器计数就会减为0,此时如果进程再去访问资源就会阻塞。
3. 创建或获取信号量—semget函数
函数 semget 主要是用来创建和获取 ipc 内核对象(信号量),同时返回ipc对象标识符id。
int semget(key_t key, int nsems, int semflg);
返回值:成功返回IPC对象(信号量)的标识符id,失败返回 -1。
参数说明:
key: 用法跟之前学习的IPC(共享内存,消息队列)的键值key是一样的。
nsems:表示你要创建信号量的个数,如果已创建信号量,要获取哪个信号量只需要传信号量的索引号了。
semflg: 指定信号量的IPC_CREAT,IPC_EXCL权限组合。semflg = IPC_CREAT,如果该信号不存在就创建一个信号量,否则获取。IPC_EXCL只有在信号量不存在,新的信号量才创建,否则出错。
4. 设置或获取信号量值—semctl函数
semctl函数主要是用于设置或获取信号量值
int semctl(int semid, int semnum, int cmd, union semun);
返回值说明:成功返回一个正数,失败返回-1并设置errno
参数说明:
semnum:是指定信号集中的信号量,信号量编号从0开始
cmd:使用命令来设置或获取信号量值,关于IPC_STAT,IPC_SET,IPC_RMID三个选项前面已经介绍了,这三个选项是操作semun联合体中的buf成员的。
以下是用于获取或设置信号量的选项:
- SETVAL:通常用来设置某个信号量值,通过val设置
- SETALL:用来设置全部信号量值,通过array设置
- GETVAL :用来获取某个信号量值,存放到val
- GETALL :用来获取全部信号量的值,存放到array
semun:用来保存要设置信号量值或者用来保存获取到的信号量值。semun是一个联合体。因此该参数可以传入一个short类型的数组,也可以传入一个int类型单个信号量,或者是一个semid_ds类型的信号量集合
5. 请求或释放信号量—semop函数
semop函数的主要用于请求或释放信号量资源
int semop(int semid, struct sembuf *sops, unsigned nsops);
返回值说明:成功返回0,失败返回-1设置errno
参数说明:
semid:IPC对象(信号量集合)标识符id
sops: 是一个指向信号量结构体数组,数组元素是 sembuf 结构体,该结构体封装了请求还是归还信号量,以及操作那个信号量,以及操作信号量的行为特性。
nsops:表示信号量 sembuf 结构体数组大小,也就是数组元素个数。
struct sembuf {
unsigned short sem_num; /* 要操作的信号量下标 */
short sem_op; /* > 0 归还资源数,< 0 请求资源数 */
short sem_flg; /* 可选项,操作的行为 */
}
sem_num:表示要操作哪个信号量
sem_op:
如果sem_op > 0,表示进程释放
信号量控制的资源,则信号量加上sem_op的值。
如果sem_op < 0,表示进程申请
信号量控制的资源,则信号量减去sem_op的绝对值,如果信号量值小于sem_op的绝对值(就是进程申请信号量控制的资源不足)。若指定了IPC_NOWAIT,则semop出错返回EAGAIN,若未指定IPC_NOWAIT,则阻塞等待。
如果sem_op = 0,表示进程阻塞等待,直到信号量值为0,然后函数返回。如果信号量值一直为非0值,若指定了sem_flg = IPC_NOWAIT,则出错返回EAGIN。如果未指定sem_flg = IPC_NOWAIT,那么该进程阻塞等待以下几个事件发生:
- 直到信号量值为0
- 从系统中删除此信号,进程解除阻塞,然后函数返回出错
- 进程捕捉到一个信号,并从信号处理函数返回,进程解除阻塞,然后函数返回出错
sem_flg:可选项,一般为 0 。
- sem_flg = IPC_NOWAIT,无论请求的资源有没有,立即返回。如果没有资源,errno 设置为 EAGAIN。
- sem_flg = SEM_UNDO,表示进程结束时,相应的操作将被取消。也就是说,如果设置了SEM_UNDO,当进程结束后没有释放共享资源就退出时,内核将会代替释放。
6. 信号量操作示例
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
void print_sem(int semid)
{
//打印信号量
int ret;
unsigned short arr2[2] = {0};
ret = semctl(semid , 2 , GETALL , arr2);
if(ret < 0){
perror("semtcl error:");
}
printf("sem1 = %d , sem2 = %d\n" , arr2[0] , arr2[1]);
}
int main(void)
{
int ret;
int semid = 0;
key_t key = 0x00112233;
//创建信号集,2个信号
semid = semget(key , 2 , IPC_CREAT | IPC_EXCL | 0664);
if(semid < 0){
perror("semget error:");
}
//查看创建的信号量IPC对象
system("ipcs -s");
puts("----------------------");
//第一个信号量的值为1,第二个信号量的值为2
unsigned short arr1[2] = {1 , 2};
//通过array设置两个信号量
ret = semctl(semid , 0 , SETALL , arr1);
if(ret < 0){
perror("semtcl error:");
}
//表示请求第2个信号量,请求2个资源
struct sembuf buf1 = {1 , -2 , 0};
semop(semid , &buf1 , 1);
//打印信号量
print_sem(semid);
//释放第2个信号量,释放2个资源
struct sembuf buf2 = {1 , 2 , 0};
semop(semid , &buf2 , 1);
//打印信号量
print_sem(semid);
//删除信号量集合
semctl(semid , 0 , IPC_RMID);
puts("----------------------");
//查看信号量IPC对象
system("ipcs -s");
return 0;
}
程序运行结果: