目录
一、信号量介绍
信号量是用来解决进程线程之间同步与互斥问题的一种通信机制,比如共享内存就需要同步机制,共享内存的介绍可查看我的博客。
链接: Linux进程间通信2——SystemⅤ共享内存
在多任务操作系统环境下,多个进程/线程会同时运行。多个任务可能为了完成同一目标相互协作,这就形成了任务之间的同步关系。同样,不同任务之间为争夺有限的系统资源(硬件或软件资源)就会进入竞争状态,这就形成互斥关系。
任务之间的互斥与同步关系存在的根源在于临界资源,即同一时刻只允许有限个(通常只有一个)任务可以访问(读)或修改(写)资源。这些资源比如处理器、内存、共享代码段、全局变量等。访问临界资源的代码称为临界区。
信号量机制包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(P/V操作)。信号量的值对应某一种资源的数量,若值等于0表示当前没有可用资源。
补充:原子操作指一个独立而不可分割的操作,执行完毕之前不会被任何其它任务或事件中断。
(1)P操作
如果有可用资源,则占用一个资源,否则该进程被阻塞,进入等待队列直到系统将资源分配给该进程。
结构描述如下:
if(是否有资源)
{
执行后续代码;
信号量-1;
}
else
{
阻塞等待,直到有资源才唤醒为止
}
(2)V操作
如果该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程,否则释放一个资源。
if(有线程在等待该资源)
{
唤醒第一个等待的线程,让其继续运行
}
else
{
信号量+1;
}
二、信号量接口函数
(1)创建获得已存在的信号量 semget()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg)
参数:
key:信号量的键值,多个进程通过同一个键值访问同一个信号量。key可以通过ftok函数得到,也可直接用宏IPC_PRIVATE,表示私有信号量。
ftok获取键值函数:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数:
pathname :路径(已存在的)
proj_id :可填0~255的一个数值,也可填一个字符
说明:当两个参数分别相同时,获取的key值相同。所以需要访问同一个信号量的进程才填相同参数
返回值:成功则返回key值,失败返回-1
semget第二个参数nsems表示要创建的信号量个数,注意不是指信号量的值
第三个参数semflg表示权限位
IPC_CREAT ----创建信号量,即使已存在函数也不会报错
IPC_EXCL ----创建的信号量若已存在则函数返回报错
常使用例子:IPC_CREAT | 0666
或者:IPC_CREAT | IPC_EXCL |0666
0666指信号量的权限,由三个八进制组成,这和文件权限相似
返回值:成功则返回信号量标识符(ID),出错返回-1
(2)初始化信号量 semctl()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, union semun arg);
参数:
semid :信号灯标识符id
semnum :要控制的信号灯编号,从0开始
cmd :控制命令
常用宏如下:
IPC_RMID 表示删除信号量 ,则第四个参数可不填
SETVAL 将信号量值设置为arg结构变量中的val值
GETVAL :返回信号量当前值
arg 为union semun结构,某些系统会不给出该结构定义,需要程序员自己定义
union semun {
int val; //val值
struct semid_ds *buf; //缓冲区
unsigned short *array; //数组
struct seminfo *__buf; //缓冲区 (Linux-specific)
};
返回值:成功则根据cmd返回不同的值,IPC_RMID 、SETVAL返回0
GETVAL返回信号量当前值。出错返回-1.
(3)信号量P/V操作 semop()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数:
semid :信号量标识符id
sops : 指向进行操作的结构体数组的首地址,该结构体数组中每个 sembuf 结构体对应一个特定信号的操作。
struct sembuf{
unsigned short sem_num; //信号在信号集中的索引,0代表第一个信号,1代表第二个信号
short sem_op; //操作类型,通常是两个数,一个是-1,即P(等待)操作,
//另一个是+1,即V(发送信号)操作。
short sem_flg; //操作标志,通常为SEM_UNDO,使操作系统跟踪信号,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
nsops :将要进行操作的信号的个数
返回值:成功返回0,出错返回-1.
(4)删除信号量
用semctl()函数中cmd参数的IPC_RMID即可删除
例如:
semctl(sem_id,0,IPC_RMID,sem_union);
对共享内存实现同步
采用信号量作为同步机制完善基于共享内存的进程间通信。
两个进程即“生产者”、“消费者”
producer.c
/*===============================================
* 文件名称:producer.c
* 创 建 者: xm
* 创建日期:2022年08月12日
* 描 述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
union semun {
int val; //val值
struct semid_ds *buf; //缓冲区
unsigned short *array; //数组
struct seminfo *__buf; //缓冲区 (Linux-specific)
};
int sem_p(int semid)//将P操作封装
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=-1;
sem_b.sem_flg=SEM_UNDO;
if(semop(semid,&sem_b,1)==-1)
{
perror("P operation ");
return -1;
}
return 0;
}
int sem_v(int semid)//V操作
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=1;
sem_b.sem_flg=SEM_UNDO;
if(semop(semid,&sem_b,1)==-1)
{
perror("V operation ");
return -1;
}
return 0;
}
int main(int argc, char *argv[])
{
union semun sem_union;
key_t key=ftok(".",'a');//获取键值
int shmid=shmget(key,1024,IPC_CREAT | 0666);//创建共享内存
if(shmid==-1)
{
perror("shmget");
exit(-1);
}
int semid=semget(key,1,IPC_CREAT | 0666);//创建一个信号量
sem_union.val=0;//开始时共享内存中没有数据资源
semctl(semid,0,SETVAL,sem_union);//初始化信号量
void *p=shmat(shmid,NULL,0);//共享内存映射地址
if(p==(void *)-1)
{
perror("shmat");
exit(-1);
}
system("ipcs -m");
printf("key=%#x,id=%d\n",key,shmid);//查看和总表的共享内存是否一致
char *memory=(char *)p;
while(1)//共享内存访问
{
printf("input>>");
fgets(memory,32,stdin);//从终端读取数据并存入共享内存
sem_v(semid);
if(strcmp(memory,"quit\n")==0) //若输入quit则结束访问
{
break;
}
}
if(semctl(semid,0,IPC_RMID,sem_union)==-1)//删除信号量
{
perror("semctl");
exit(-1);
}
if(shmdt(p)==-1)//取消映射
{
perror("shmdt");
exit(-1);
}
return 0;
}
customer.c
/*===============================================
* 文件名称:customer.c
* 创 建 者: xm
* 创建日期:2022年08月12日
* 描 述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
union semun {
int val; //val值
struct semid_ds *buf; //缓冲区
unsigned short *array; //数组
struct seminfo *__buf; //缓冲区 (Linux-specific)
};
int sem_p(int semid)//将P操作封装
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=-1;
sem_b.sem_flg=SEM_UNDO;
if(semop(semid,&sem_b,1)==-1)
{
perror("P operation ");
return -1;
}
return 0;
}
int sem_v(int semid)//V操作
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=1;
sem_b.sem_flg=SEM_UNDO;
if(semop(semid,&sem_b,1)==-1)
{
perror("V operation ");
return -1;
}
return 0;
}
int main(int argc, char *argv[])
{
union semun sem_union;
key_t key=ftok(".",'a');//获取键值
int shmid=shmget(key,1024,IPC_CREAT | 0666);//创建(获得已有)共享内存
if(shmid==-1)
{
perror("shmget");
exit(-1);
}
int semid=semget(key,1,IPC_CREAT |0666);//创建(获得已有)一个信号量
void *p=shmat(shmid,NULL,0);//共享内存映射地址
if(p==(void *)-1)
{
perror("shmat");
exit(-1);
}
system("ipcs -m");
printf("key=%#x,id=%d\n",key,shmid);//查看和总表的共享内存是否一致
char *memory=(char *)p;
while(1)//共享内存访问
{
sem_p(semid);
printf("output>>\n");
fputs(memory,stdout);//从终端读取数据并存入共享内存
if(strcmp(memory,"quit\n")==0) //若输入quit则结束访问
{
break;
}
memset(memory,0,strlen(memory));//清空
}
if(shmdt(p)==-1)//取消映射
{
perror("shmdt");
exit(-1);
}
if(shmctl(shmid,IPC_RMID,NULL)==-1)//删除共享内存
{
perror("shmctl");
exit(-1);
}
return 0;
}
只有在生产者完成向共享内存数据写入后,消费者才能读取共享内存数据。即使消费者先读取数据也会被阻塞,直到生产者写入数据后才唤醒消费者。
运行截图: