在讲解信号量之前有必要先介绍几个重要的概念。
进程的互斥:由于各进程要求共享资源,而有些资源需要互斥使用,因此各进程间竞争使用这些资源,这种关系叫做进程的互斥
临界资源:系统中的某些资源能被多个进程看见,但是一次只允许一个进程使用,而这样的资源被称为临界资源或互斥资源。
临界区:进程中访问临界资源的一段需要互斥执行的代码。
临界区的访问规则:
空闲则入:没有进程在临界区是任何进程均可进入。
忙则等待:有进程在临界区时,其他进程均不能进入临界区。
有限等待:等待进程临界区的进程不能无限期的等待下去。
让权等待(可选):不能进入临界区的进程,应释放CPU(如转换到阻塞状态)
信号量是什么呢?是操作系统提供的一种协调共享资源访问的方法。这么说感觉很抽象,它到底是一种什么样的方法呢。信号量本质上就是一个计数器,用于描述临界资源多少的计数器。其结构为
struct semaphore
{
int value;
pointer_PCB queue;
}
当我们使用一个计数器来保证一个临界资源的互斥访问时,将资源的数目记录下来,等待资源数目为0时,就不让其他进程再访问了,所以,所有想要访问某种临界资源的时候,是可以看到它的信号量的,即信号量被多个进程都看到了,那么,它本身不也就是一种临界资源了么。意思就是,我们此时是希望利用一种本身就是临界资源的信号量去保证其他临界资源的正确访问,那么,我们首先就应该保证信号量本身的正确性。或许有人不太理解就一个计数器而已,有多少就初始化为多少,然后用多少就减多少,还回来多少再对应加回去多少不就行了?
那如果是这样一个场景呢?(讲的比较啰嗦,懂得为什么计数器是可能会出错的就不用看了),value = 5,Process used 1,value -- ;虽然 value-- 在我们看来这就是一行很简单的代码,但是在计算机内部却是要分3步来执行。第一步,将value的值写入到寄存器中;第二步,将寄存器中的value的值减1;第三步,将结果4写回到value中。而我们又知道现在的计算机都是多道处理系统,那么,就很有可能上述步骤执行了一部分该进程就被切走了,比如说还有最后一步将4写回到value还未执行就被切走了,然后,刚好有另一个进程B也刚好要访问该临界资源,然后,它一看value还有5,于是用了2个,将value的由5减到了3,这时候进程A切回来了,继续执行之前的动作,将4写到value中,于是value又变成了4,而实际上只剩下2个了。很显然,这个时候计数器就出现问题了。
那么怎么确保信号量自身的正确性呢?保证信号量的跟新操作为原子操作,即,确保信号量正在更新的时候不会被切走,跟新的操作要么被完整地做完,要么停留在正要跟新时的位置下次切回来的时候再完整的执行数据跟新的操作。讲到这里,想必大家也已经明白了,其实信号量就是一种抽象的数据类型,由一个整型value(用于计数)和两个原子操作(P,V操作)组成。
P() ---prolaag (荷兰语,尝试减少的意思) sem减1
if(sem < 0)进入等待
else 继续
V() ---verhoog(荷兰语,增加的意思) sem加1
if(sem <= 0)唤醒一个等待进程
信号量一次只能表示单一的一种临界资源,且使用P、V操作申请和归还都是一个一个的,而通常情况下,一个进程在执行过程中可能会同时需要多种临界资源,且每种临界资源可能不止需要一个。为了更加符合实际情况的使用,信号量集的概念就被提出来来,事实上,我们一般使用的就是信号量集。信号量集的结构为:
struct semid_ds {
struct ipc_perm sem_perm; //关系及权限值
time_t sem_otime; // 最后一次访问的时间
time_t sem_ctime; // 最后一次改变的时间
unsigned short sem_nsems; //信号量集中信号的数量
};
信号量集,一堆信号量的集合,可以把它理解为一个数组。而数组里边的每个元素都是一个结构体,用于表示一种信号量。而这个结构体里边除了有信号量的id 之外,还有一些方便我们使用的其他信息。
sembuf 结构体
struct sembuf{
short sem_num;
short sem_op;
short sem_flg;
};
说明:
sem_num 是信号量的编号
sem_op 是信号量一次PV操作时加减的数值,一般只会用的两个值,
-1 即进行P操作;+1 即进行V操作
sem_flg 有两个取值:IPC_NOWAIT、SEM_UNDO;
接下来介绍几个重要的函数
semget函数 ---创建
功能:⽤来创建和访问⼀个信号量集
原型:int semget(key_t key, int nsems, int semflg);
参数:key: 信号量集的名字
nsems:信号量集中信号量的个数
semflg: 由九个权限标志构成,它们的⽤法和创建⽂件时使⽤mode模式标志是⼀样的
返回值:成功返回⼀个⾮负整数,即该信号量集的标识码;失败返回-1
semctl函数 --- 控制
功能:⽤于控制信号量集原型:int semctl(int semid, int semnum, int cmd, ...);
参数:semid:由semget返回的信号集标识码
semnum:信号量集中信号量的序号
cmd:将要采取的动作(比如给信号量赋初值,删除信号量等)根据命令不同⽽不同
返回值:成功返回0;失败返回-1
参数cmd常见的值有:
SETVAL 设置信号量集中的信号量的计数值;
GETVAL 获取信号量中的信号量的计数值;
IPC_STAT 把semid_ds结构中的数据设置为信号量的当前关联值;
IPC_RMID 删除信号量;
需要特别说明的是,该函数的参数列表后边为...,表明参数的长度是可变的。当第三个参数cmd为SETVAL,用于给信号量赋初值,调用semctl函数时一定要给出第四个参数,且给成是一个包含了初始化值的联合体
union semun
{
int val;
struct semid_ds* buf;
unsigned short* array;
struct seminfo* _buf;
};
semop函数 ---操作
功能:⽤来操作⼀个信号量集
原型:int semop(int semid, struct sembuf *sops, unsigned nsops);
参数:semid:是该信号量的标识码,也就是semget函数的返回值
sops:是个指向⼀个结构数值的指针
nsops:信号量的个数
返回值:成功返回0;失败返回-1
代码实例:
用于包含头文件、宏定义、函数声明等的comm.h部分
#ifndef __COMM_H__
#define __COMM_H__
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
union semun
{
int val;
struct semid_ds* buf;
unsigned short* array;
struct seminfo* _buf;
};
#define ERR_EXIT(n) \
do\
{\
perror("n");\
exit(EXIT_FAILURE);\
}while(0)
int creatSemSet(int nums);
int initSem(int semid,int nums,int initval);
int getSemSet(int nums);
int P(int semid,int num);
int V(int semid,int num);
int destroySetSet(int semid);
#endif
具体函数实现的comm.c部分
#include "comm.h"
static int CreatSem(int nums,int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
ERR_EXIT("ftok");
int semid = semget(key,nums,flags);
if(semid < 0)
ERR_EXIT("semget");
return semid;
}
int creatSemSet(int nums)
{
return CreatSem(nums,IPC_CREAT|IPC_EXCL|0666);
}
int initSem(int semid,int num,int initval)
{
union semun un;
un.val = initval;
if(semctl(semid,num,SETVAL,un) < 0)
ERR_EXIT("semctl");
return 0;
}
int getSemSet(int nums)
{
return CreatSem(nums,IPC_CREAT);
}
static int PV(int semid,int num,int op)
{
struct sembuf _sf;
_sf.sem_num = num;
_sf.sem_op = op;
_sf.sem_flg = 0;//NO_WAIT
if(semop(semid,&_sf,1) < 0)
{
ERR_EXIT("semop");
return -1;
}
return 0;
}
int P(int semid,int num)
{
return PV(semid,num,-1);
}
int V(int semid,int num)
{
return PV(semid,num,1);
}
int destroySetSet(int semid)
{
if(semctl(semid, 0, IPC_RMID)<0)
{
ERR_EXIT("destroy");
return -1;
}
return 0;
}
信号量的实现 mysem.c 部分
#include "comm.h"
int main()
{
int semid = creatSemSet(1);
if(initSem(semid,0,1) < 0)
ERR_EXIT("init");
pid_t id = fork();
if(id == 0)//child
{
int _semid = getSemSet(0);
while(1)
{
P(_semid,0);
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(123456);
V(_semid,0);
}
}
else//father
{
while(1)
{
P(semid,0);
printf("B");
fflush(stdout);
usleep(123456);
printf("B ");
fflush(stdout);
sleep(1);
V(semid,0);
}
wait(NULL);
}
destroySemSet(semid);
return 0;
}
程序执行结果: