一、信号量简介
信号量是一种进程间同步机制。信号量的常见用途就是同步对一块共享内存的访问,以防止一个进程在访问内存的时候另一个内存在更新这块内存的数据。
常见的同步就是互斥锁了,互斥锁常用于线程(也可用于进程),互斥锁有两种状态——上锁和未上锁状态。当一个线程访问共享资源时,线程对共享资源上锁,防止别的线程访问,当共享资源访问结束再对共享资源解锁,以便另一个线程对该共享资源上锁并访问。由此可见,互斥锁解决资源冲突的方式从共享资源的访问权限来控制的。线程使用互斥锁是比较简单的,但是对于两个进程间,如果给一个公共资源上锁,我觉得是有一些难度的。
信号量则是从进程是否可以执行的角度去解决资源冲突的。一个信号量是由内核维护的整数,其值大于等于0。
信号量的三种状态:①信号量当前值的基础上加 1 ;② 信号当前值的基础上减 1 操作;③信号量此时值为 0 ,某一进程想执行减 1 操作,于是该进程被阻塞。
假设,我对信号量的操作,只有加 1 和减 1 ,于是有如下现象:进程A创建信号量,将信号量初始化为0,此时进程B想对信号量执行减1操作,于是进程B被阻塞,若进程A执行到某处给信号量执行了加1操作,然后进程A主动放弃CPU,然后A再对信号量执行减1操作,此时由于B给信号量已经执行了减 1 操作,于是进程A 被阻塞,进程B开始执行。由此可见,通过内核提供的一个整数,进程之间实现了同步,以下附上两个进程之间的执行流程图(重在理解):
注意:信号量是内核提供的一种机制,当减小一个信号量的值时,内核会将所有试图将信号量的值降低到 0 之下的进程阻塞。而进程间实现共享时,是进程间约定好的,然后通过信号量实现。
二、API介绍
1、创建或打开一个信号量集
#include <sys/types.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflag);
参数:
- key:可以传入 IPC_PRIVATE 或 ftok() 产生唯一key,但是一般做进程间同步需要指定一个任意正整数,作为信号量的唯一标识。
- nsems:指定信号量集合中信号量的数量。
- semflg:新信号量集上的权限(同创建文件时权限一样,传八进制)还有可选参数 IPC_CREATE (如果key相关的信号量集合不存在就创建)、IPC_EXCL(指定key关联的信号集合存在则返回 EEXIST 错误)
- 该函数调用成功后返回信号量集合,或既有信号量集合的标识符。
2、信号量控制
#include <sys/types.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, union semun arg);
参数:
- semid:信号量集的标识符
- semnum:信号量集合中的具体信号(信号量集合看做一个数组,该参数表示信号量集合数组中第几个元素)
- cmd:制定需要执行的操作。
cmd参数设置:- IPC_RMID :立即删除信号量集及其关联的semid_ds数据结构。所有因在semop()调用中等待这个信号量而阻塞的进程都会被立即唤醒。semop()会报告EIDRM错误。
- IPC_STAT:在arg.buf指向的缓冲器中放置一份与这个信号量集相关联的semid_ds数据结构副本。
- IPC_SET:使用arg.buf指向的缓冲器中的值来更新与这个信号集相关联的semid_ds数据结构中选中的字段。
- GETVAL:semctl()返回由semid指定的信号集中第semnum个信号量的值
- SETVAL:将由semid指定的信号集中第semnum个信号量的值初始化为arg.val
- GETALL:获取由semid指向的信号量集中所有信号量的值并将它们放在arg.array指向的数组中。
- SETALL:使用arg.array指向的数组中的元素初始化semid指向的集合中的所有的喜好量。
- GETPID:返回上一个在该信号量上执行semop()的进程ID,这个值被称为semid值,若没有进程在该信号量上执行过semop(),那么就返回0
- GETNCNT:返回当前等待该信号量的值增长的进程数,这个值被称为semncnt值。
- GETZCNT:返回当前等待该信号量的值变为0的进程数,这个值称为semzcnt值。
- union semnum 枚举
- 有些系统可能没有定义该枚举类型(但是还需要传入该枚举值,系统是否定义,用宏‘_SEM_SEMUN_UNDEFINED’值为1就是没定义)那就需要程序员自己定义:
#include <sys/types.h>
#include <sys/sem.h>
union semun{
int val; //SETVAL
struct semid_ds* buf; //IPC——STAT、IPC_SET
unsigned short* array; //GETALL或SETALL
#if defined(__linux__)
struct seminfo* info; //进程信息
#endif
};
struct semid_ds 结构体参数:
struct semid_ds{
struct ipc_perm sem_prem; //权限
time_t sem_otime; //上次操作semop()的时间
time_t sem_ctime; //上次修改时间
unsigned long sem_nsems; //信号量集合中的个数
};
sem_prem:创建信号量集合时初始化该结构各字段,使用IPC_SET可以更新uid、gid、mode字段。
- struct ipc_perm{
- key_t __key; //
- uid_t uid; //用户id
- gid_t gid; //用户组id
- uid_t cuid; //创建者用户id
- gid_t cgid; //创建者用户组id
- unsigned short mod; //权限
- unsigned short __seq; //链表序号
- }
3、信号量操作
#include <sys/types.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf* sops, unsigned int nsops);
参数介绍
- semid 得到的信号量集合id
- struct sembuf
struct sembuf{
unsigned short sem_num; //信号量集中要操作的信号量(下标)
short sem_op; //具体操作,加 1 或 减 1 (设置值大于或小于)
short sem_flg; //操作的flag IPC_NOWAIT 或 SEM_UNDO(信号量撤销值)
}
另一个API只是比上一个API多了时间而已,最后一个参数传NULL,同上一个API用法一样。
int semtioedop(int semid, struct sembuf* sops, unsigned int nsops, struct timespec* timeout);
三、信号量作用举例
我创建两个进程(一个父进程,一个子进程),每个进程打印三句话。
第一个例子没有使用信号量,这样当一个进程执行过程中可能失去cpu时间片,第二个进程将开始执行。比如可能在第一个进程只输出一句话后第二个进程就开始执行,这样会造成一些数据的混乱。
第二个例子使用了信号量同步两个进程间的执行,使他们可以有序执行。
1、先给第一个不使用信号量的例子
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv){
int ret = 0;
char* charBuf = "TEST";
pid_t forkPid = 0;
/* 测试 */
/* 创建父子进程输出字符串 */
forkPid = fork();
if(forkPid == 0){
/* 子进程 */
for(int i = 0; i < 10; ++i){
//开始输出;
printf("%s\n", charBuf);
printf("%s\n", "子");
printf("%s\n", charBuf);
putchar('\n');
sleep( rand() % 5 ); //随机休眠
}
return 0;
}
else if(forkPid > 0){
/* 父进程 */
for(int i = 0; i < 10; ++i){
//开始输出;
printf("%s\n", charBuf);
printf("%s\n", "父");
printf("%s\n", charBuf);
putchar('\n');
//休眠
sleep( rand() % 5 ); //随机休眠
}
//父进程等待子进程退出
wait(NULL);
return 0;
}
}
gcc
执行结果会出现以下情况
TEST
TEST
子
父
TEST
TEST
TEST
TEST
父
TEST
子
TEST
2、使用信号量确保一个进程中执行完需要同步的部分后另一进程开始执行
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
#define _MY_KEY_ 1
//程序员手动创建 信号量 联合体
union semun {
int val;
struct semid_ds* buf;
unsigned short* array;
};
int main(int argc, char* argv){
int ret = 0;
int semSetId = 0;
key_t myKey = 1;
char* charBuf = "TEST";
pid_t forkPid = 0;
union semun semUnion;
/* 创建一个信号量集 */
semSetId = semget(myKey, _MY_KEY_, IPC_CREAT | 0770 ); //信号量不存在,则创建一个信号量并指定其权限
if(semSetId < 0 && semSetId != EINVAL){
perror("semget error: ");
return -1;
}
else if(semSetId == EINVAL){
printf("信号量已存在,继续执行!!!\n");
}
/* 初始化信号量集 */
semUnion.val = 1; // 初始化信号量联合体
ret = semctl (semSetId, 0, SETVAL, semUnion); //将semSetId信号量集中第 0 个元素设置为 semUnion.val
if(ret != 0){
perror("semctl error:");
return -1;
}
/* 测试 */
/* 创建父子进程输出字符串 */
forkPid = fork();
if(forkPid == 0){
/* 子进程 */
for(int i = 0; i < 10; ++i){
//对信号量做减 1 操作
struct sembuf semBuf;
semBuf.sem_num = 0;
semBuf.sem_op = -1;
semBuf.sem_flg = SEM_UNDO;
ret = semop(semSetId, &semBuf, 1); //阻塞了另一个进程
if (ret != 0){
perror("semop error:");
return -1;
}
//开始输出;
printf("%s\n", charBuf);
printf("%s\n", "子");
printf("%s\n", charBuf);
putchar('\n');
//输出完成后解除另一个进程的阻塞问题并进入休眠
semBuf.sem_op = 1;
ret = semop(semSetId, &semBuf, 1); //另一进程解除阻塞
if (ret != 0){
perror("semop error:");
return -1;
}
sleep( rand() % 5 ); //随机休眠
}
return 0;
}
else if(forkPid > 0){
/* 父进程 */
for(int i = 0; i < 10; ++i){
//对信号量做减 1 操作
struct sembuf semBuf;
semBuf.sem_num = 0;
semBuf.sem_op = -1;
semBuf.sem_flg = SEM_UNDO;
ret = semop(semSetId, &semBuf, 1); //阻塞了另一个进程
if (ret != 0){
perror("semop error:");
return -1;
}
//开始输出;
printf("%s\n", charBuf);
printf("%s\n", "父");
printf("%s\n", charBuf);
putchar('\n');
//输出完成后解除另一个进程的阻塞问题并进入休眠
semBuf.sem_op = 1;
ret = semop(semSetId, &semBuf, 1); //阻塞了另一个进程
if (ret != 0){
perror("semop error:");
return -1;
}
//休眠
sleep( rand() % 5 ); //随机休眠
}
//父进程等待子进程退出
wait(NULL);
semctl(semSetId, 0, IPC_RMID); //将信号量集关闭并删除.
return 0;
}
}
执行结果一定如下(三个一组,再加一个换行这样的形式输出):
TEST
子
TEST
TEST
父
TEST
TEST
子
TEST
TEST
父
TEST
三、system V信号量使用套路总结
假设我只是用一个信号量
#include <sys/types.h>
#include <sys/sem.h>
//创建一个信号集
/**
*两个进程间都用该信号量,这两个进程的key值一定要一样
*num表示信号量集中信号量的个数
*semflag 一般写IPC_CREAT | 0770,前者是假如信号集不存在就创建
*(两个进程间使用,只创建一个信号集),存在则不创建,第二个参数是权限
*类似创建用open()函数创建文件时指定权限一样。
*/
semid = semget(key, num, flag);
//设置并初始化一个信号量
union semun semUnion;
semUnion.val = 1;
semctl(semid, 0, SETVAL, semUnion);
//操作
struct sembuf semBuf;
semBuf.sem_num = 0;
semBuf.sem_op = -1; //设置阻塞其它进程
semBuf.sem_flg = SEM_UNDO;//信号量撤销。解决在解除阻塞前。进程意外退出的情况
semop(semSetId, &semBuf, 1); //设置开始生效
semBuf.sem_op = 1; //设置解除阻塞一个进程
semop(semSetId, &semBuf, 1); //设置开始生效
//删除操作
semctl(semSetId, 0, IPC_RMID); //将信号量集删除