信号量PV操作
临界资源:同一时刻,只允许被一个进程或线程访问的资源
临界区:访问临界资源的代码段
如:商场试衣间,十字路口
同步:控制,使程序执行的正确性有保证,使用信号量
信号量是一个特殊的变量,一般取正数值。它的值代表允许访问的资源数目,获取资源时,需要对信号量的值进行原子减一,该操作被称为 P 操作
当信号量值为 0 时,代表没有资源可用,P 操作会阻塞住。
释放资源时,需要对信号量的值进行原子加一,该操作被称为 V操作
信号量主要用来同步进程。信号量的值如果只取 0,1,将其称为二值信号量。
如果信号量的值大于 1,则称之为计数信号量。比如说有3个试衣间,供3个人使用,第4个人需要等待
PV操作就是对资源的获取和释放
原子性:保证依次通过,不会出现问题,是原子操作
举例子:
n代表火车票剩余的票数
每一个人买票, n-1
两个人都买票,应该n-2,实际上n-1
因为买票是并发的过程
计算机将n值减1需要一个过程,n=5读到处理器,准备减1,还没完成,同时另一个去访问,也把n=5读到处理器,准备减1,最后两个进程都把4写回去
n=4
此结果4显然有问题!!!
所以我们要进行控制,不要同时对n进行控制
我们需要用到原子操作的信号量,就可以解决这个问题了
通过PV操作0,1
例子2:
访问打印机
vi a.c
vi b.c
在同一个终端同时进行
需要通过信号量进行控制
我们设定左边的用户为A用户,右边的用户为B用户
首先A进行P操作,获取信号量的值(1),减1,为0,可通过
V还没执行之前,A是在使用中,此时如果B访问,执行p操作,遇到信号量为0,就阻塞住!!!(因为p操作把0-1=-1,直接自动就阻塞住了)
现在如下使用信号量解决问题
操作信号量的接口介绍:
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/ipc.h>
/*
semget()创建或者获取已存在的信号量
semget()成功返回信号量的 ID, 失败返回-1
key:两个进程使用相同的 key 值,就可以使用同一个信号量
nsems:内核维护的是一个信号量集,在新建信号量时,其指定信号量集中信号
量的个数,给个1,就是只创建1个信号量
semflg 可选: IPC_CREAT(信号量不存在,需要创建)
IPC_CREAT IPC_EXCL (全新创建,如果没有就创建,如果有就失败)
*/
int semget(key_t key, int nsems, int semflg);//创建,获取信号量
/*
semop()对信号量进行改变,做 P 操作或者 V 操作
semop()成功返回 0,失败返回-1
struct sembuf
{
unsigned short sem_num; //指定信号量集中的信号量下标
short sem_op; //其值为-1,代表 P 操作,其值为 1,代表 V 操作
short sem_flg; //SEM_UNDO
};
*/
int semop(int semid, struct sembuf *sops, unsigned nsops);//初始化
/*
semctl()控制信号量
semctl()成功返回 0,失败返回-1
cmd 选项: SETVAL IPC_RMID
union semun//这个联合体没有在头文件被定义,我们要自己定义
{
int val;
struct semid_ds *buf;
unsigned short *array
struct seminfo *_buf;
}; */
第一个参数:信号量的id,第二个参数:结构体 第三个参数:命令
int semctl(int semid, int semnum, int cmd, ...);
封装信号量的接口:
vi sem.h
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/sem.h>
union semun{
int val;//初始值
};
void sem_init();//创建/或者已存在的信号量
void sem_p();//p 减一
void sem_v();//v 加一
void sem_destroy();//销毁
vi sem.c
#include "sem.h"
static int semid = -1;
void sem_init()//创建/或者已存在的信号量
{
semid = semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);//约定都使用这个key_t值,1个信号量,全新创建,谁先执行,谁先创建,后一个人就直接使用
if ( semid == -1 )//已存在
{
semid = semget((key_t)1234,1,0600);//存在就直接获取,0600是为了占参数位的作用而已
}
else//初始化操作
{
union semun a;
a.val = 1;//信号量的初始值
if ( semctl(semid,0,SETVAL,a) == -1 )//只创建1个,所以下标为0,初始化失败
{
perror("semctl error");//#include <unistd.h>
}
}
}
void sem_p()//p 减一
{
struct sembuf buf;
buf.sem_num = 0;//成员下标,第0个信号量
buf.sem_op = -1;//p 加的值是-1
buf.sem_flg = SEM_UNDO;//信号量p操作,然后要执行V操作,如果程序因为某种原因崩溃了,V操作还没来得及执行,其他进程也阻塞的,SEM_UNDO会让
//操作系统记住你这个P操作,如果你因为崩溃为了V操作,操作系统会帮你执行V操作,把资源释放回去,其他进程就可以使用了
if ( semop(semid,&buf,1) == -1 )//1是操作的个数
{
perror("semop p error");
}
}
void sem_v()//v 加一
{
struct sembuf buf;
buf.sem_num = 0;//成员下标
buf.sem_op = 1;//v 加的值是1
buf.sem_flg = SEM_UNDO;
if ( semop(semid,&buf,1) == -1 )
{
perror("semop v error");
}
}
void sem_destroy()//销毁信号量
{
if ( semctl(semid,0,IPC_RMID) == -1 )//IPC_RMID代表是移除信号量,移除的是整个信号量集合哦,所以0只是占位的作用
{
perror("semctl del error");
}
}
信号量只能销毁1次哦
vi a.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include "sem.h"
int main()
{
sem_init();//初始化1
int i = 0;
for( ;i < 10; i++ )
{
sem_p();//如果检测到1,通过,执行减1操作,往下执行
printf("a");
fflush(stdout);
int n = rand() % 3;
sleep(n);
printf("a");
fflush(stdout);
sem_v();//+1操作
n = rand() % 3;
sleep(n);
}
}
vi b.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
int main()
{
sem_init();
int i = 0;
for( ;i < 10; i++ )
{
sem_p();
printf("b");
fflush(stdout);
int n = rand() % 3;
sleep(n);
printf("b");
fflush(stdout);
sem_v();
n = rand() % 3;
sleep(n);
}
}
编译运行
这个信号量是我们通知内核创建的,内核来维护
我们结束后,内核还在维护
而且信号量可能还有其他人在使用
只能删一次
ipcs可以查看信号量,共享内存,消息队列
ipcs -s查看信号量
ipcs -m查看共享内存
ipcs -q查看消息队列
使用完,我们必须移除,不然不知道是1还是0(上一次的)
用命令删除
拓展提升
三个进程 a、b、c 分别输入“A”、“B”、“C”,要求输出结果必须是“ABCABCABC…”
三个信号量 第一个初始化为1,后两个初始化为0
ps1第一次通过,ps2 ps3不通过
vi a.c
vi b.c
vi c.c
运行