信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
1、特点
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
支持信号量组。
2、原型
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。
在semop函数中,sembuf结构的定义如下:
struct sembuf
{
short sem_num; // 信号量组中对应的序号,0~sem_nums-1
short sem_op; // 信号量值在一次操作中的改变量
short sem_flg; // IPC_NOWAIT, SEM_UNDO
}
其中 sem_op 是一次操作中的信号量的改变量:
若sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则唤醒它们。
若sem_op < 0,请求 sem_op 的绝对值的资源。
如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
当相应的资源数不能满足请求时,这个操作与sem_flg有关。
sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN。
sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
此信号量被删除,函数smeop出错返回EIDRM;
进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
若sem_op == 0,进程阻塞直到信号量的相应值为0:
当信号量已经为0,函数立即返回。
如果信号量的值不为0,则依据sem_flg决定函数动作:
sem_flg指定IPC_NOWAIT,则出错返回EAGAIN。
sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
此信号量被删除,函数smeop出错返回EIDRM;
进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR
在semctl函数中的命令有多种,这里就说两个常用的:
SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
//int semget(key_t,int nsems,int semflg);
//int semctl(int semid,int semnum,int cmd,...);
//int semop(int semid,struct sembuf *sops,unsigned nsops);
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 pGetKey(int id)//拿锁操作函数
//P操作:
//若信号量值为1,获取资源并将信号量值-1
//若信号量值为0,进程挂起等待
{
struct sembuf set;
set.sem_num = 0;//序号
set.sem_op = -1;//操作
set.sem_flg = SEM_UNDO;
semop(id,&set,1);
printf("getkey\n");
}
void vPutBackKey(int id)//放锁操作函数
//V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
{
struct sembuf set;
set.sem_num = 0;//序号
set.sem_op = 1;//操作
set.sem_flg = SEM_UNDO;
semop(id,&set,1);
printf("put back the key\n");
}
int main(int argc,char const *argv[]) //传参
{
key_t key;
int semid;
key = ftok(".",2); //获取键值
//信号量集合中有一个信号量
semid = semget(key,1,IPC_CREAT|0666); //获取/创建信号量
union semun initsem;
initsem.val = 0;
//操作第0个信号量
semctl(semid,0,SETVAL,initsem);//初始化信号量
//SETVAL设置信号量的值,设置为initsem
int pid = fork();
if(pid > 0){
pGetKey(semid); //去拿锁,开始没有锁,等待先执行子进程
printf("this is father\n");
vPutBackKey(semid); //锁放回去
semctl(semid,0,IPC_RMID);//销毁锁
}
else if(pid == 0){
printf("this is child\n");
vPutBackKey(semid); //放锁
}else{
printf("fork error!\n");
}
return 0;
}
共享内存+信号量的使用
函数原型
#include <sys/shm.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
例子
此例子,使用了【共享内存+信号量+消息队列】的组合来实现服务器进程与客户进程间的通信。
共享内存用来传递数据;
信号量用来同步;
消息队列用来 在客户端修改了共享内存后 通知服务器读取。
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;
//SETVAL设置信号量,设置成tmp,tmp.val等于value,传参有value个信号量。
if(semctl(sem_id, 0, SETVAL, tmp) == -1)//初始化信号量,失败返回-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);//挂载成功返回内存指针,失败返回-1;
if((int)shm == -1) //强制类型转换,判断返回值
{
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); //p操作
printf("%s\n",shm); //读取共享内存里面的信息
sem_v(semid); //v操作
}
}
// 断开连接
shmdt(shm);
//注意最后需要处理创建的共享内存、消息队列、信号量
/*删除共享内存、消息队列、信号量*/
shmctl(shmid, IPC_RMID, &buf1);
msgctl(msqid, IPC_RMID, &buf2);
del_sem(semid);
return 0;
}
client.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((int)shm == -1)
{
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);//将msg所携带的信息发送
break;
case 'q':
msg.mtype = 888;
msg.mtext = 'q';
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
flag = 0;//改变flag退出程序
break;
default:
printf("Wrong input!\n");
/*清空标准输入缓冲区*/
while((c=getchar())!='\n' && c!=EOF);
}
}
// 断开连接
shmdt(shm);
return 0;
}