Linux系统编程系列(16篇管饱,吃货都投降了!)
16、 Linux系统编程系列之线程池
一、什么是信号量组
信号量组是信号量的一种, 是system-V三种IPC对象之一,是进程间通信的一种方式。
二、信号量组的特性
信号量组不是用来传输数据的,而是作为“旗语”,用来协调各进程或者线程工作的。信号量组可以一次性在其内部设置多个信号量,而信号量本质上是一个数字,用来表征一种资源的数量,当多个进程或者线程争夺这些稀缺资源的时候,信号量用来保证他们合理地,秩序地使用这些资源,而不会陷入逻辑谬误之中。
三、信号量组的使用场景
1、生产者-消费者模式
2、进程间同步
3、进程间通信
四、函数API接口
1、创建或者打开SEM对象
// 创建或打开SEM对象 int semget(key_t key, int nsems, int semflg); // 接口说明: 参数key:SEM对象键值 参数nsems:信号量组内的信号量元素个数 参数semflg:创建选项 IPC_CREAT:如果该key对应的信号量不存在,则创建 IPC_EXCL:如果该key对应的信号量已存在,则报错 mode:信号量的访问权限 创建信号量时,还受到以下系统信息的影响: 1、SEMMNI:系统中信号量的总数最大值 2、SEMMSL:每个信号量中信号量元素的个数最大值 3、SEMMNS:系统中所有信号量中的信号量元素的总数最大值
2、P/V操作
对于信号量而言,最重要的作用是用来表征对应资源的数量,所谓的P/V操作就是对资源数量进行 +n/-n 操作,既然只是个加减法,那么为什么不使用普通的整型数据呢,原因是:
(1)、整型数据的加减操作不具有原子性,即操作可能被中断
(2)、普通加减法无法提供阻塞特性,而申请资源不可得时应该进入阻塞
// PV操作 int semop(int semid, struct sembuf *sops, size_t nsops); // 接口说明 参数semid:SEM对象ID 参数sops:PV操作结构体sembuf数组 参数nsops:PV操作结构体数组元素个数 返回值:成功 0,失败 -1 PV操作结构体定义如下: struct sembuf { unsigned short sem_num; // 信号量元素序号(数组下标) short sem_op; // 操作参数 short sem_flg; // 操作选项 } 根据sem_op的数值,信号量操作分成3种情况: (1)当sem_op大于0时: 当进行V操作(释放),即信号量元素的值(semval)将会被加上sem_op的值。如果SEM_UNDO被设置了,那么该V操作将会被系统记录,V操作永远不会导致进程阻塞。 (2)当sem_op等于0时:进行等零操作,如果此时semval恰好为零,则semop()立即成功返回,否则如果IPC_NOWAIT被设置,则立即出错返回并将errno设置为EAGAIN,否则将使得进程进入睡眠,直到以下情况发生: [1]semval变为0 [2]信号量被删除 (将导致semop()出错退出,错误码为EIDRM) [3]收到信号 (将导致semop()出错退出,错误码为EINTR) (3)当sem_op小于0时(申请资源):进行P操作,即信号量元素的值(semval)将会被减去sem_op的绝对值。如果semval大于或等于sem_op的绝对值,则semop()立即成功返回,semval的值将减去sem_op的绝对值,并且如果SEM_UNDO被设置了,那么该P操作将会被系统记录。 如果semval小于sem_op的绝对值并且设置了IPC_NOWAIT,那么semop()将会出错返回且将错误码置为EAGIN,否则将使得进程进入睡眠,直到以下情况发生: [1]semval的值变得大于或者等于sem_op的绝对值 [2]信号量被删除 (将导致semop()出错退出,错误码为EIDRM) [3]收到信号 (将导致semop()出错退出,错误码为EINTR)
3、删除SEM对象
// 删除SEM对象 int semctl(int semid, int semnum, int cmd, ...); // 接口说明 semid:信号量组的ID semnum:信号量组内的元素序号(从0开始) cmd;操作命令字 IPC_STAT:获取信号量组的一些信息,放入结构体semid_ds中 IPC_SET:将结构体semid_ds中指定的信息,设置到信号量组中 IPC_RMID:删除指定的信号量组 GETALL:获取所有信号量元素的值 SETALL:设置所有信号量元素的值 GETVAL:获取第semnum个信号量元素的值 SETVAL:设置第semnum个信号量的值
五、信号量组使用步骤
1、使用ftok(),获取IPC通信对象KEY值
2、使用semget(),获取SEM对象ID,并判断是否需要进行初始化
3、使用semop(),进行P/V操作,操作信号量组
4、使用命令或者函数删除信号量组
六、案例
使用信号量组结合共享内存的方式完成两个进程的数据收发。
// 信号量组结合共享内存的案例 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/shm.h> #include <errno.h> // 编译时分两个版本,一个直接编译,另外一个把A的宏定义注释,把B的宏定义展开 #define A 1 //#define B 1 // 编译第二版本时,请去掉前面的注释,同时注释A的宏定义 // 注意A进程的P信号量与B进程的V信号量相对应,所以要修改信号量序号的下标 #if A #define DATA_P_NUM 0 #define DATA_V_NUM 1 #define SPACE_P_NUM 2 #define SPACE_V_NUM 3 #elif B #define DATA_P_NUM 1 #define DATA_V_NUM 0 #define SPACE_P_NUM 3 #define SPACE_V_NUM 2 #endif #define SEM_NUM 4 // 4个信号量 #define SEM_KEY 0x01 #define SHM_KEY 0x02 #define SHM_SIZE 4096 int sem_id = -1; // 映射的虚拟地址 char *shm_addr = NULL; // 信号量组初始化 int sem_init(void) { // 1、获取IPC对象的KEY值 key_t sem_key = ftok("./", SEM_KEY); if(sem_key == -1) { perror("ftok fail"); return -1; } // 2、获取SEM对象的ID, 申请4个信号量 sem_id = semget(sem_key, SEM_NUM, IPC_EXCL | IPC_CREAT | 0666); // 如果已经存在就不需要初始化,直接获取 if(sem_id == -1 && errno == EEXIST) { // 直接获取SEM对象ID sem_id = semget(sem_key, SEM_NUM, IPC_CREAT | 0666); if(sem_id == -1) { perror("semget fail"); return -1; } } // 不存在则需要在获取SEM对象ID后进行初始化 else if(sem_id > 0) { sem_id = semget(sem_key, SEM_NUM, IPC_CREAT | 0666); if(sem_id == -1) { perror("semget fail"); return -1; } // 初始化 semctl(sem_id, DATA_P_NUM, SETVAL, 0); // 初始值为0 semctl(sem_id, DATA_V_NUM, SETVAL, 0); // 初始值为0 semctl(sem_id, SPACE_P_NUM, SETVAL, 1); // 初始值为1 semctl(sem_id, SPACE_V_NUM, SETVAL, 1); // 初始值为1 } else { perror("semget fail"); return -1; } } // 共享内存初始化 int shm_init(void) { // 1、获取KEY值 key_t shm_key = ftok("./", 1); if(shm_key == -1) { perror("ftok fail"); return -1; } // 2、指定共享内存,获取共享内存对象ID int shm_id = shmget(shm_key, SHM_SIZE, IPC_CREAT | 0666); if(shm_id == -1) { perror("shmget fail"); return -1; } // 3、映射共享内存 shm_addr = (char*)shmat(shm_id, NULL, 0); if(shm_addr == (void*)-1) { perror("shmat fail"); return -1; } } int main(int argc, char *argv[]) { int ret = 0; ret = sem_init(); if(ret == -1) { return -1; } ret = shm_init(); if(ret == -1) { return -1; } // 接收数据, 数据-1 struct sembuf Data_P = { .sem_flg = SEM_UNDO, .sem_num = DATA_P_NUM, .sem_op = -1 }; // 发送数据, 数据+1 struct sembuf Data_V = { .sem_flg = SEM_UNDO, .sem_num = DATA_V_NUM, .sem_op = 1 }; // 占用空间, 空间-1 struct sembuf Space_P = { .sem_flg = SEM_UNDO, .sem_num = SPACE_P_NUM, .sem_op = -1 }; // 释放空间 空间+1 struct sembuf Space_V = { .sem_flg = SEM_UNDO, .sem_num = SPACE_V_NUM, .sem_op = 1 }; pid_t pid = fork(); // 父进程负责发送数据 if(pid > 0) { while(1) { // 申请空间,P操作 printf("wait Space_P...\n"); semop(sem_id, &Space_P, 1); printf("get Space_P\n"); printf("please input data: \n"); fgets(shm_addr, SHM_SIZE, stdin); // 释放数据,V操作 semop(sem_id, &Data_V, 1); printf("set Data_V, send data success\n"); } } // 子进程负责接收数据 else if(pid == 0) { while(1) { // 申请数据,P操作 printf("wait Data_P...\n"); semop(sem_id, &Data_P, 1); printf("read Data: %s", shm_addr); memset(shm_addr, 0, SHM_SIZE); // 释放空间,V操作 semop(sem_id, &Space_V, 1); printf("set Space_V\n"); } } else { perror("fork fail"); return -1; } return 0; }
注:编译时,编译两个版本,一个直接编译,另外一个需要注释A的宏定义,然后展开B的宏定义后才能编译第二个版本。
分析:具体的PV操作这里不讲解,为什么要申请4个信号量,这个要讲明白的话,很难,有空再出另外一篇博客讲,敬请留意。
七、总结
信号量组只能作为一种信号,不能用来传递数据,多用于使用P/V操作的场景,可以同时操作多个信号量,但是要实现传递数据,必须配合其他通信方式,如共享内存。可以结合案例来加深对信号量组的理解。