前言
对于信号量来说,是IPC机构当中的一个,与消息队列等不同,它是一个计数器,用于为多个进程提供对共享数据对象的访问,接下来,我们对信号量进行一些探索。
在这我们需要提出一些概念:
临界区:指的是访问临界资源的程序代码片段。临界区只能允许一个进程进入。
临界资源:临界资源说的是一次只能提供一个进程使用的资源。
互斥:互斥是指某一个资源同时只允许一个访问者对其进行访问。
原子性:一个事务包含多个操作,这些操作要么全部执行,要么全都不执行。
加锁:这里的加锁就是给临界区加锁,也叫做互斥锁,用来保护临界区。因为两个进程同时访问临界区会可能出现问题,所以需要互斥锁来保护。
1.使用信号量的原因
经常我们会遇到这种情况,就是可能会出现多个程序同时去访问一个共享资源,这个时候会出现问题,为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。信号量具有原子性,最简单的信号量只能取0和1,也叫做二元信号量,可控制单个资源。
程序对信号量的操作是原子性的,只会进行P(等待)操作或者是V(发送)操作,当两个进程在进行对信号量的访问的时候,一个进程运行,一个进程这个时候为休眠状态。
临界区加了互斥锁,所以这个时候申请锁资源,如果申请到了,那么就进入临界区,如果没申请到,那么就挂起。
一个进程获得共享资源,需要执行的操作为:
(1)测试控制该资源的信号量。
(2)若此信号量为正,则进程可以使用该资源。这个时候信号量值减1,表示进程在使用一个资源。
(3)当型号了为0,这个时候进入休眠状态,直到信号量大于0,进程唤醒。
2.信号量的特点
(1)和消息队列一样。信号量也是在内核中实现的,并且信号量是随内核的,也就是当进程结束的时候,信号量是没有被销毁的。
(2)信号量并非单个非负值,而必须定义为含有一个或多个信号量值的集合。当创建信号量的时候,需要指定集合当中信号量值的数量。
(3)信号量的创建和初始化不是原子的,所以这样就会导致出一个问题,由于不互斥,就是可能会出现创建后未初始化就被另外一个进程获取。
3.信号量的操作
信号量只能进行两种操作等待和发送信号,即P和V:
P(sv):如果sv的值大于0,就给它减1,如果它的值为0,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
4.信号量相关函数
Linux提供一些关于信号量操作的函数,这些函数在sys/sem.h。
函数名 | 作用 |
---|---|
ftok | IPC通信 |
semget | 创建一个信号量集或访问一个已经存在的信号量集 |
semctl | 对一个信号量执行各种控制操作 |
semop | 操作信号量集合中的一个或多个信号量 |
key_t ftok(const char *path,int id)
用来产生提供产生一个键,每一个IPC对象都与一个键相关联,这个键作为该对象的外部名。
int semget(key_t key,int nsems,int flag);
将key变换成一个标识符,并且讨论时创建一个新集合还是引用一个现有集合。nsems是该集合当中的信号量数。如果是创建新集合,则必须指定nsems,引用现有的,则设定nsems为0
int semctl(int semqid,int semnum,int cmd,...);
第四个参数是可选的,是公用体semun。semun必须自己来定义。
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) */
};
semnum是指定该信号量集合中的一个成员,cmd参数是选择命令,根据你的操作进行选择。在这说下几种比较重要的:
命令 | 作用 |
---|---|
IPC_RMID | 删除信号量集合 |
SETVAL | 设置成员semnum的semval值 |
semctl函数成功返回0.失败返回-1。
int semop(int semid,struct sembuf semoparray[],size_t nops);
首先需要知道一个结构体struct sembuf
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
这个结构体和信号量的操作有关,sem_num表示进行操作的信号集合中的信号量下标,sem_op表示的是操作方式,sen_flg与SEM_UNDO,与防止死锁相关。
npos表示信号量集合当中信号量的个数。
5.信号量示例:
下面的例子是利用信号量,实现了父子进程的互斥,实现能够打印子进程AA然后打印完以后再打印父进程的BB
comm.h
#ifndef __COMM_H__
#define __COMM_H__
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define PATHNAME "."
#define PROJID 0x6666
int Init_Sem(int semid);
int Get_Sem();
int Creat_Sem();
int Destory_Sem(int semid);
int P_Sem(int semid,int which);
int V_Sem(int semid,int which);
#endif //!__COMM_H__
comm.c
#include"comm.h"
int Init_Sem(int semid)
{
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_*/
};
union semun semun;
semun.val=1;
int ret=semctl(semid,0,SETVAL,semun);
if(ret<0)
{
perror("semctl");
return -1;
}
return ret;
}
static int Comm_Sem(int flags,int nsems)
{
key_t semkey=ftok(PATHNAME,PROJID);
if(semkey<0)
{
perror("ftok");
return -1;
}
int semid=semget(semkey,nsems,flags);
if(semid<0)
{
perror("semget");
return -2;
}
return semid;
}
int Creat_Sem()
{
return Comm_Sem(IPC_CREAT|IPC_EXCL|0666,1);
}
int Get_Sem()
{
return Comm_Sem(IPC_CREAT,0);
}
int Destory_Sem(int semid)
{
int ret=semctl(semid,0,IPC_RMID);
}
static int Comm_Op(int semid,int which,int op)
{
struct sembuf sembuf;
sembuf.sem_num=which;
sembuf.sem_op=op;
sembuf.sem_flg=0;
int ret=semop(semid,&sembuf,1);
if(ret<0)
{
perror("semop");
return -1;
}
return ret;
}
int P_Sem(int semid,int which)
{
return Comm_Op(semid,which,-1);
}
int V_Sem(int semid,int which)
{
return Comm_Op(semid,which,1);
}
sem.c
#include<unistd.h>
#include"comm.h"
int main()
{
int semid=Creat_Sem();
int ret=Init_Sem(semid);
if(ret<0)
{
return -1;
}
printf("semid:%d\n",semid);
pid_t id=fork();
if(id==0)
{
//child
while(1)
{
P_Sem(semid,0);
usleep(10002);
printf("A");
fflush(stdout);
usleep(1080);
printf("A");
fflush(stdout);
V_Sem(semid,0);
}
}
else
{
while(1)
{
P_Sem(semid,0);
printf("B");
usleep(10080);
fflush(stdout);
printf("B");
usleep(12000);
fflush(stdout);
V_Sem(semid,0);
}
wait(NULL);
}
Destory_Sem(semid);
return 0;
}
6.关于探究sem_flg
sem_flg是用来说明semop的行为。可取值:
IPC_NOWAIT:对信号的操作不满足时,semop不会阻塞,并且立即返回。
SEM_UNDO:程序结束的时候,保证信号值被重设为semop之前的值。避免
一般来说二元信号量,为了防止进程以外退出,避免程序在异常情况下结束时未将锁定的资源解锁。造成资源永远锁定的问题,需要设置SEM_UNDI标志。SEM_UNDO //程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值,简单说就是撤销对信号量的操作,这样就回到了信号量没操作的时候。真正的操作其实也就是IPC信号量计数器的回滚。