信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同(前面的管道,消息队列,共享内存,信号都可以携带数据),它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
原理
临界资源(各进程采取互斥的方式,实现共享的资源称作临界资源。)
多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
可以将信号量比喻成一个盒子,初始化时在盒子里放入N把钥匙,钥匙先到先得,当N把钥匙都被拿走完后,再来拿钥匙的人就需要等待了,只有等到有人将钥匙归还了,等待的人才能拿到钥匙;
临界资源就像一间房间,用锁锁起来,只有持有钥匙的人才能进去访问,当盒子没有钥匙的时候,外面的人需要等里面的人出来之后归还钥匙,才可以进去访问。
特点
(1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
(2)信号量基于操作系统的PV 操作,程序对信号量的操作都是原子操作。
(3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
(4)支持信号量组。
注意:
PV操作是一种实现进程互斥与同步的有效方法。PV操作与信号量的处理相关,P(拿钥匙)表示通过的意思,V(放回钥匙)表示释放的意思。
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程)。
创建
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
常用API
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int nsems, int semflg);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf *sops, unsigned nsops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
semget函数
创建或获取一个信号量组:
函数原型
int semget(key_t key, int nsems, int semflg);
参数
key:所创建或打开信号量集的键值。
nsems:信号量的个数,通常为1;如果是引用一个现有的集合,则将nsems指定为 0,该参数只在创建信号量集时有效。
(也可以设置多个,比如设置成2,理解成一个管理a事件,另一个管理b事件)
semflg:调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过 | 表示。
返回值
若成功返回信号量集ID,失败返回-1
semop函数
对信号量组进行操作,改变信号量的值:
函数原型
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数
semid:semget()返回的信号量标识符。
*sops:结构的定义如下:
struct sembuf{
short sem_num; // 除非使用一组信号量,否则它为0
short sem_op; // 信号量在一次操作中需要改变的数据,
//通常是两个数,一个是-1,即P(拿钥匙)操作,一个是+1,即V(放回)操作。
short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,
// 并在进程没有释放该信号量而终止时,操作系统释放信号量
};
nsops:信号操作结构的数量,恒大于或等于1。
返回值
成功返回0,失败返回-1
semctl函数
控制信号量的相关信息
函数原型
int semctl(int semid, int semnum, int cmd, ...);
参数
semid:semget()返回的信号量标识符。
semnum:是操作信号在信号集中的编号,第一个信号的编号是0。
cmd:cmd通常是下面的两个值:
SETVAL:
用来把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:
用于删除一个已经无需继续使用的信号量标识符。
cmd中其他可以使用的命令如下:
IPC_STAT//读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET//设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID//将信号量集从内存中删除。
GETALL//用于读取信号量集中的所有信号量的值。
GETNCNT//返回正在等待资源的进程数目。
GETPID//返回最后一个执行semop操作的进程的PID。
GETVAL//返回信号量集中的一个单个的信号量的值。
GETZCNT//返回正在等待完全空闲的资源的进程数目。
SETALL//设置信号量集中的所有的信号量的值。
SETVAL//设置信号量集中的一个单独的信号量的值。
如果有第四个参数(需要用户自行定义),它通常是一个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 */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
作用是在信号量第一次使用前对它进行设置。
对多个信号量初始化
semid =semget(key,3,IPC_CREAT|0666)
union semun arg;
unsigned short vals[3]={0};
//假设初始化信号量集的值都为0
arg.array = vlas
semctl(sem_id, 0, SETALL, arg)
//使用SETALL
参考生产者和消费者
示例1(父子进程)
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
//p操作
//将信号量值-1
void pGet(int id)
{
struct sembuf set;
set.sem_num=0;
set.sem_op=-1;
set.sem_flg =SEM_UNDO;
semop(id,&set,1);
printf("get\n");
}
//v操作
//将信号量值+1
void vPut(int id)
{
struct sembuf set;
set.sem_num=0;
set.sem_op=1;
set.sem_flg =SEM_UNDO;
semop(id,&set,1);
printf("put\n");
}
//父进程获取信号量将被阻塞,直到子进程将信号量+1
int main(int argc,char **argv)
{
key_t key;
int semid;
key = ftok(".",8);// 获取key值
semid =semget(key,1,IPC_CREAT|0666);// 创建信号量集,其中只有一个信号量
union semun initsem;
initsem.val = 0;//设置SETVAL的值(一开始没有钥匙)
semctl(semid,0,SETVAL,initsem);//SETVAL:用来把信号量初始化为一个已知的值。
int pid = fork();//建立进程
if(pid>0){
pGet(semid);//父进程将信号量-1,因为一开始为0,会堵塞到子进程将信号量+1才会继续执行
printf("this is father\n");
vPut(semid);
semctl(semid,0,IPC_RMID);//删除信号量
}
else if(pid == 0){
sleep(1);
printf("this is child\n");
vPut(semid);//子进程将信号量+1
putchar('\n');
}else{
printf("fork error\n");
}
}
如果没有pv操作,父进程肯定时先执行完的,因为子进程sleep了2秒
示例2 (不同进程访问共享内存—读者写者问题–写者优先)
- 先实现一个共享资源只能有一个进程(一个读者或者一个写者)访问
semaphore rw=1;//用于实现对共享数据的互斥访问
写进程:
writer(){
while(1){
P(rw);
"写操作"
V(rw);
}
}
读进程:
reader(){
while(1){
P(rw);
"读操作"
V(rw);
}
}
- 实现多个读者可以一起访问共享资源,写者想访问的时候需要等待读者全部访问结束才能访问。(也就是第一个读者进来的时候执行p操作,直到最后一个读者出去的时候在执行v操作)
semaphore rw=1;//用于实现对共享数据的互斥访问
int count=0;//记录当前几个读进程在访问共享数据
写进程:
writer(){
while(1){
P(rw);
"写操作"
V(rw);
}
}
读进程:
reader(){
while(1){
if(count==0){
P(rw);
}
count++;
"读操作"
count--;
if(count==0){
V(rw);
}
}
}
上面的代码时有bug的,如果读进程A执行了P(rw)操作,但是在执行"count ++"之前(此时count还是0),读进程B进来也是会执行P(rw)操作,就会被堵塞。 所以我们需要对count进行上锁
semaphore rw=1;//用于实现对共享数据的互斥访问
int count=0;//记录当前几个读进程在访问共享数据
semaphore mutex=1;//对count变量的互斥访问
写进程:
writer(){
while(1){
P(rw);
"写操作"
V(rw);
}
}
读进程:
reader(){
while(1){
P(mutex);
if(count==0){
P(rw);
}
count++;
V(mutex);
"读操作"
P(mutex);
count--
if(count==0){
V(rw);
}
V(mutex);
}
}
- 假设读者源源不断,那么count会一直累加,写者会一直等待。所以需要再新增一个信号量来实现写者优先。
semaphore rw=1;//用于实现对共享数据的互斥访问
int count=0;//记录当前几个读进程在访问共享数据
semaphore mutex=1;//对count变量的互斥访问
semaphore w=1;//用于实现写者优先
写进程:
writer(){
while(1){
P(w);
P(rw);
"写操作"
V(rw);
V(w);
}
}
读进程:
reader(){
while(1){
P(w);
P(mutex);
if(count==0){
P(rw);
}
count++;
V(mutex);
V(w);
"读操作"
P(mutex);
count--;
if(count==0){
V(rw);
}
V(mutex);
}
}
代码实现
//semWrite.c 写者
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include<string.h>
#include <sys/sem.h>
#include <sys/shm.h>
#define rw 0//用于实现对共享数据的互斥访问
#define mutex 1//对count变量的互斥访问
#define w 2//用于实现写者优先
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void p(int id,int num)
{
struct sembuf set;
set.sem_num=num;
set.sem_op=-1;
set.sem_flg=SEM_UNDO;
semop(id,&set,1);
}
void v(int id,int num)
{
struct sembuf set;
set.sem_num=num;
set.sem_op=1;
set.sem_flg=SEM_UNDO;
semop(id,&set,1);
}
int main()
{
key_t key;
key =ftok(".",'q');
int shmid;
int semid;
shmid=shmget(key,1024*4,IPC_CREAT|0666);
if(shmid==-1){
printf("shmget error\n");
}
char *shmAddr;
shmAddr=shmat(shmid,0,0);
memset(shmAddr,0,1024*4);
int i;
semid=semget(key,3,IPC_CREAT|0666);
union semun initsem;
unsigned short arr[3]={1,1,1};//设置信号集的值为1
initsem.array=arr;
semctl(semid,0,SETALL,initsem);
printf("rw:%d\n",semctl(semid,0,GETVAL));
printf("mutex:%d\n",semctl(semid,1,GETVAL));
printf("w:%d\n",semctl(semid,2,GETVAL));
for(i=0;i<10;i++){
p(semid,w);
p(semid,rw);
memset(shmAddr,0,1024*4);
sprintf(shmAddr,"from is write msg %d",i);
printf("write over\n");
v(semid,rw);
v(semid,w);
sleep(1);
}
shmdt(shmAddr);
printf("write pro over!\n");
return 0;
}
//semRead 读者
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include<string.h>
#include <sys/sem.h>
#include <sys/shm.h>
#define rw 0
#define mutex 1
#define w 2
int count = 0;
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void p(int id,int num)
{
struct sembuf set;
set.sem_num=num;
set.sem_op=-1;
set.sem_flg=SEM_UNDO;
semop(id,&set,1);
}
void v(int id,int num)
{
struct sembuf set;
set.sem_num=num;
set.sem_op=1;
set.sem_flg=SEM_UNDO;
semop(id,&set,1);
}
int main()
{
key_t key;
key =ftok(".",'q');
int shmid;
int semid;
shmid=shmget(key,1024*4,0);//打开共享内存
if(shmid==-1){
printf("shmget error\n");
}
char *shmAddr;
shmAddr=shmat(shmid,0,0);//连接共享内存
int i;
semid=semget(key,3,0);//获取信号量集
for(i=0;i<10;i++){
p(semid,w);
p(semid,mutex);
if(count==0){
p(semid,rw);
}
count++;
v(semid,mutex);
v(semid,w);
printf("get msg:%s\n",shmAddr);
p(semid,mutex);
count--;
if(count==0){
v(semid,rw);
}
v(semid,mutex);
sleep(1);
}
shmdt(shmAddr);//断开连接
printf("read pro over!\n");
shmctl(shmid,IPC_RMID,0);//关闭共享内存
semctl(semid,0,IPC_RMID);
return 0;
}
示例3(消息队列,信号量,共享内存)
server.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/shm.h> // shared memory
#include<sys/sem.h> // semaphore
#include<sys/msg.h> // message queue
#include<string.h> // memcpy
// 消息队列结构
struct msg_form {
long mtype;
char mtext;
};
// 联合体,用于semctl初始化
union semun
{
int val; /*for SETVAL*/
struct semid_ds *buf;
unsigned short *array;
};
// 初始化信号量
int init_sem(int sem_id, int value)
{
union semun tmp;
tmp.val = value;
if(semctl(sem_id, 0, SETVAL, tmp) == -1)
{
perror("Init Semaphore Error");
return -1;
}
return 0;
}
// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /*序号*/
sbuf.sem_op = -1; /*P操作*/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0;
}
// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /*序号*/
sbuf.sem_op = 1; /*V操作*/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0;
}
// 删除信号量集
int del_sem(int sem_id)
{
union semun tmp;
if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
{
perror("Delete Semaphore Error");
return -1;
}
return 0;
}
// 创建一个信号量集
int creat_sem(key_t key)
{
int sem_id;
if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
{
perror("semget error");
exit(-1);
}
init_sem(sem_id, 1); /*初值设为1资源未占用*/
return sem_id;
}
int main()
{
key_t key;
int shmid, semid, msqid;
char *shm;
char data[] = "this is server";
struct shmid_ds buf1; /*用于删除共享内存*/
struct msqid_ds buf2; /*用于删除消息队列*/
struct msg_form msg; /*消息队列用于通知对方更新了共享内存*/
// 获取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 创建共享内存
if((shmid = shmget(key, 1024, IPC_CREAT|0666)) == -1)
{
perror("Create Shared Memory Error");
exit(1);
}
// 连接共享内存
shm = (char*)shmat(shmid, 0, 0);
if(shm == NULL)
{
perror("Attach Shared Memory Error");
exit(1);
}
// 创建消息队列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
// 创建信号量
semid = creat_sem(key);
// 读数据
while(1)
{
msgrcv(msqid, &msg, 1, 888, 0); /*读取类型为888的消息*/
if(msg.mtext == 'q') /*quit - 跳出循环*/
break;
if(msg.mtext == 'r') /*read - 读共享内存*/
{
sem_p(semid);
printf("%s\n",shm);
sem_v(semid);
}
}
// 断开连接
shmdt(shm);
/*删除共享内存、消息队列、信号量*/
shmctl(shmid, IPC_RMID, &buf1);
msgctl(msqid, IPC_RMID, &buf2);
del_sem(semid);
return 0;
}
cllient.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/shm.h> // shared memory
#include<sys/sem.h> // semaphore
#include<sys/msg.h> // message queue
#include<string.h> // memcpy
// 消息队列结构
struct msg_form {
long mtype;
char mtext;
};
// 联合体,用于semctl初始化
union semun
{
int val; /*for SETVAL*/
struct semid_ds *buf;
unsigned short *array;
};
// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /*序号*/
sbuf.sem_op = -1; /*P操作*/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0;
}
// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /*序号*/
sbuf.sem_op = 1; /*V操作*/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0;
}
int main()
{
key_t key;
int shmid, semid, msqid;
char *shm;
struct msg_form msg;
int flag = 1; /*while循环条件*/
// 获取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 获取共享内存
if((shmid = shmget(key, 1024, 0)) == -1)
{
perror("shmget error");
exit(1);
}
// 连接共享内存
shm = (char*)shmat(shmid, 0, 0);
if(shm == NULL)
{
perror("Attach Shared Memory Error");
exit(1);
}
// 创建消息队列
if ((msqid = msgget(key, 0)) == -1)
{
perror("msgget error");
exit(1);
}
// 获取信号量
if((semid = semget(key, 0, 0)) == -1)
{
perror("semget error");
exit(1);
}
// 写数据
printf("***************************************\n");
printf("* IPC *\n");
printf("* Input r to send data to server. *\n");
printf("* Input q to quit. *\n");
printf("***************************************\n");
while(flag)
{
char c;
printf("Please input command: ");
scanf("%c", &c);
switch(c)
{
case 'r':
printf("Data to send: ");
sem_p(semid); /*访问资源*/
scanf("%s", shm);
sem_v(semid); /*释放资源*/
/*清空标准输入缓冲区*/
while((c=getchar())!='\n' && c!=EOF);
msg.mtype = 888;
msg.mtext = 'r'; /*发送消息通知服务器读数据*/
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
break;
case 'q':
msg.mtype = 888;
msg.mtext = 'q';
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
flag = 0;
break;
default:
printf("Wrong input!\n");
/*清空标准输入缓冲区*/
while((c=getchar())!='\n' && c!=EOF);
}
}
// 断开连接
shmdt(shm);
return 0;
}
注意:当scanf()输入字符或字符串时,缓冲区中遗留下了\n,所以每次输入操作后都需要清空标准输入的缓冲区。使用: while((c=getchar())!=‘\n’ && c!=EOF);
参考文章