信号量是什么?
在开始知道信号量的这个名词的时候,根本不能从字面上来理解信号量到底是什么?它跟信号有什么关系呢?
其实信号量的实质就是计数器!!!
为什么是计数器呢?接着往下看。
信号量原理
我们知道,进程在通信的时候会对临界资源进行访问操作等,但这样说来但凡能够访问到这块临界资源的进程都可以对其进行操作。那么会不会出现一种问题,就是当进程A刚刚在临界资源内存放一个数据以备后面使用,此时进程B访问临界资源拿走了A刚刚存放的数据,那么A在后面用什么?这样说来,临界资源也是需要保护的,只要合理的保护临界资源,才更避免一些问题的产生,而信号量就是来保护临界资源的。
举个例子:一间教室内有五十个空座位,然后门口有一个人是这个教室的管理老师,这时候一个同学来到了这个教室门口,向这位老师申请进入教室自习,此时有五十个空着的座位,那么老师允许这位同学进去,此时剩下49个座位。接着源源不断的同学过来请求进入教室自习,老师都一个个的同意了。这时候教室里面坐满了,没有座位了。接着再来这个教室的同学向老师申请的时候,老师会告诉他等等,如果有同学出来了,那么这位同学就可以进去。这个时候教室就相当于是临界资源,同学就是一个个进程,而老师则就是信号量。老师那里记录着教室内剩余的座位。
这样说来就很清楚,信号量实质上是一个计数器,可以起到对临界资源的保护,而我们每个进程去访问临界资源的时候,进行对信号量的请求,从而达到访问临界资源。
信号量的P、V操作
我们进程在向信号量申请资源,信号量减一的动作称为P操作,而进程访问完资源,离开并释放资源,信号量加一的动作称为V操作。
这里的PV操作都是原子性的。
信号量值的含义
信号量值用S表示:
- S>0:S表示可用的资源个数
- S=0:表示没有可用的资源了,并且此时没有其他进程在这里申请并等待资源
- S<0:此时|S|表示等待队列中进程的个数,也就是意思没有资源后,又有进程向信号量发出请求,那么信号量S的值就会减一
信号量结构体伪代码
struct semaphore
{
int value;
pointer_PCB queue;
};
//P操作
P(s)
{
s.value = s.value--;
if(s.value < 0)
{
//该进程状态设置为等待状态
//将该进程的PCB插入相对应的等待队列s.queue末尾
}
}
//V操作
V(s)
{
s.value = s.value++;
if(s.value <= 0)
{
//唤醒等待队列s.queue中的一个进程
//改变这个进程的状态为就绪状态
//并且将这个进程的PCB加入到就绪队列
}
}
信号量集函数
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_SET:在进程有足够权限的前提下,把信号集的当前关联值设置为semid_ds数据结构中的给定值
IPC_RMID:删除信号集
semop函数
功能:用来创建和访问一个信号量集
原型:int semop(int semid, struct sembuf* sops; unsigned nsops);
参数:
semid:是该信号量的标识码
sops:是个指向结构数值的指针
nsops:信号量的个数
返回值:成功返回0,失败返回1
//sembuf结构体
struct sembuf {
short sem_num;//信号量的编号
short sem_op;//信号量一次PV操作时加减的数值,一般是“-1”与“+1”
short sem_flg;//两个取值是IPC_NOWAIT或SEM_UNDO
};
信号量的简单实现
我们在前面大概能够对信号量有一个初步的了解。接下来就是利用信号量的一些操作函数来实现一个信号量,并且对其效果进行展示。
首先我们先要确立一个测试的函数,在没有信号量的保护下,如果两个进程同时抢占一个临界资源那么会出现什么问题?
//测试用例
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
{
printf("fork error!\n");
return -1;
}
if(pid == 0)
{
while(1)
{
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(123456);
}
}
else
{
while(1)
{
printf("B");
fflush(stdout);
usleep(123456);
printf("B ");
fflush(stdout);
usleep(123456);
}
wait(NULL);
}
return 0;
}
我们发现在没有信号量保护显示器这个临界资源的时候,发现此时AB两个打印完全是乱序,根本不是我们想要的。我们想要的是父进程再屏幕上输入两个AA然后子进程再继续在屏幕上输出BB,这时候就需要我们的信号量来进行保护。
//comm.h
#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
#define PATHNAME "/home/kaka"
#define PROJ_ID 0x6666
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
int createSemSet(int nums);
int initSem(int semid, int nums, int initVal);
int getSemSet(int nums);
int P(int semid, int who);
int V(int semid, int who);
int destroySemSet(int semid);
//comm.c
#include "comm.h"
int commSemSet(int nums, int flags)
{
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0)
{
printf("ftok error!\n");
return -1;
}
int semid = semget(key, nums, flags);
if(semid < 0)
{
printf("semget error!\n");
return -2;
}
return semid;
}
int createSemSet(int nums)
{
return commSemSet(nums, IPC_CREAT|IPC_EXCL|0666);
}
int getSemSet(int nums)
{
return commSemSet(nums, IPC_EXCL);
}
int initSem(int semid, int nums, int initVal)
{
union semun _un;
_un.val = initVal;
if(semctl(semid, nums, SETVAL, _un) < 0)
{
printf("semctl error!\n");
return -1;
}
return 0;
}
int commPV(int semid, int who, int op)
{
struct sembuf _sf;
_sf.sem_num = who;
_sf.sem_op = op;
_sf.sem_flg = 0;
if(semop(semid, &_sf, 1) < 0)
{
printf("semop error!\n");
return -1;
}
return 0;
}
int P(int semid, int who)
{
return commPV(semid,who,-1);
}
int V(int semid, int who)
{
return commPV(semid,who,1);
}
int destroySemSet(int semid)
{
if(semctl(semid, 0, IPC_RMID) < 0)
{
printf("semctl error!\n");
return -1;
}
return 0;
}
//sem.c
#include "comm.h"
int main()
{
int _semid = createSemSet(1);
initSem(_semid, 0,1);
pid_t pid = fork();
if(pid < 0)
{
printf("fork error!\n");
return -1;
}
if(pid == 0)
{
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
{
while(1)
{
P(_semid, 0);
printf("B");
fflush(stdout);
usleep(123456);
printf("B ");
fflush(stdout);
usleep(123456);
V(_semid, 0);
}
wait(NULL);
}
destroySemSet(_semid);
return 0;
}
这里我们利用comm所创建好的信号量来保护显示器这一临界资源,这时候我们只需要进行对要执行的代码进行PV操作,这样就可以让我每次的操作都具有原子性,即每次只有一个进程对临界资源进行访问,那么我们就可以保证任何进程在临界资源的访问上都是安全的。
我们发现,在增加了PV操作以后,我们的父子进程在显示器上打印的AB都是成对出现的,这也验证了我们所说的进程访问的原子性。
总结进程间通信
我们在这几个博客中,着重介绍了进程间通信systemV版本中的管道、消息队列、共享内存以及信号量这几种方式。其中消息量是对这些临界资源的保护,而消息量其实也是一种临界资源。再者,无论我们是创建消息队列还是共享内存还是信号量,它们在创建了之后,如果不手动的删除是永远存在的。也就是说systemV版本中的这些临界资源的生命周期随内核。
ipcs与ipcrm指令
ipcs + - q/m/s这个是可以对内核中消息队列、共享内存、信号量的内容进行查询
ipcrm + - q/m/s + id是对内核中消息队列、共享内存、信号量的删除即销毁
欢迎大家共同讨论,如有错误及时联系作者指出,并改正。谢谢大家!