一.什么是信号量
信号量的本质是一个计数器外带一个等待队列,它主要是用于同步与互斥的。它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。
关于信号量结构体的伪代码如下
struct semaphore
{
int value;
pointer_PCB queue;
}
当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用。大于0,资源可以请求,等于0,无资源可用,进程会进入等待状态直至资源可用。
当进程不再使用一个信号量控制的共享资源时,信号量的值+1,对信号量的值进行的增减操作均为原子操作,这是由于信号量主要的作用是维护资源的互斥或多进程的同步访问。 而在信号量的创建及初始化上,不能保证操作均为原子性。
信号量不以传送数据为目的,而以保护数据为目的。
二.使用信号量的目的
为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法, 它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。 临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是用来调协进程对共享资源的访问的。其中共享内存的使用就要用到信号量。
在信号量中只有两种操作等待和发送信号,分别是P(sv)操作和V(sv)操作。
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
查找系统中的信号量:ipcs -s
删除系统中的某个信号量:ipcrm -s semid
关于信号量在/usr/include/linux/sem.h中的原型如下
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
三.信号量集函数
先来看一下几个需要用到的重要的函数:
1.semget函数
该函数成功返回一个非负整数,即该信号量集的标识码;失败返回-1。
2.semctl函数,用于控制信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
semctl() 在 semid 标识的信号量集上,或者该集合的第 semnum 个信号量上执行 cmd 指定的 控制命令。(信号量集合索引起始于零。)根据 cmd 不同,这个函数有三个或四个参数。当有 四个参数时,第四个参数的类型是 union 。调用程序必须按照下面方式定义这个联合体:
union semun
{
int val; // 使⽤用的值
struct semid_ds *buf //IPC_STAT、IPC_SET使⽤用缓存区
unsigned short *array; // GETALL,、SETALL 使⽤用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使⽤用缓存区
};
该联合体需用户自己声明。
关于cmd参数的命令,主要有如下几个命令
- SETVAL : 设置信号量集中的信号量的计数值
- GETVAL:获取信号量集中的信号量的计数值
- IPC_STAT:把semid_ds结构中的数据设置为信号量集的当前关联值
- IPC_SET:在进程有足够权限的前提下,把信号集的当前关联值设置为semid_ds结构中给出的值
- IPC_RMID:删除信号量集
3.semop函数,用来创建和访问一个信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
semid:该信号量的标识码
sops:是个指向一个结构体sembuf的指针
nsops:信号量的个数
关于sembuf结构体包含以下内容
struct sembuf{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
- sem_num是信号量的编号。
- sem_op是信号量一次PV操作时加减的数值,一般会用到两个值,一个是“-1”也就是P操作,一个是“+1”也就是V操作。
- sem_flg的两个取值分别是IPC_NOWAIT(非阻塞式等待)或SEM_UNDO(为了防止死锁的产生)
四.测试用例
common.h
#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#define PATHNAME "."
#define PROJ_ID 0x666
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int CreateSemSet(int nums);
int GetSemSet(int nums);
int InitSemSet(int semid,int nums,int initVal);
int P(int semid,int who);
int V(int semid,int who);
int DestroySemSet(int semid);
common.c
#include "common.h"
static int CommonSemSet(int nums,int flags)
{
key_t key=ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int semid=semget(key,nums,flags);
if(semid < 0)
{
perror("semget");
return -2;
}
return semid;
}
int CreateSemSet(int nums)
{
return CommonSemSet(nums,IPC_CREAT|IPC_EXCL|0666);
}
int GetSemSet(int nums)
{
return CommonSemSet(nums,IPC_CREAT);
}
int InitSemSet(int semid,int nums,int initVal)
{
union semun _un;
_un.val=initVal;
if(semctl(semid,nums,SETVAL,_un)<0)
{
perror("semctl");
return -1;
}
return 0;
}
static int CommonPV(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)
{
perror("semop");
return -1;
}
return 0;
}
int P(int semid,int who)
{
return CommonPV(semid,who,-1);
}
int V(int semid,int who)
{
return CommonPV(semid,who,+1);
}
int DestroySemSet(int semid)
{
if(semctl(semid,0,IPC_RMID)<0)
{
perror("semctl");
return -1;
}
return 0;
}
test.c
#include "common.h"
int main()
{
int semid=CreateSemSet(1);
InitSemSet(semid,0,1);
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(321654);
V(_semid,0);
}
}
else//father
{
while(1)
{
P(semid,0);
printf("B");
fflush(stdout);
usleep(23456);
printf("B ");
fflush(stdout);
usleep(213213);
V(semid,0);
}
}
DestroySemSet(semid);
return 0;
}
测试结果如下
如上AA 和 BB都是成对出现的,那么我们不用PV操作会如何呢?
可以看到父子进程输出AB的方式并不是成对输出的,也就是说,不是原子的。
下面我们对信号量做一个总结
- 对于信号量,它的本质是一个计数器加一个等待队列
- 信号量的目的就是实现同步与互斥
- 信号量使用于任何进程
- 生命周期随内核
- 面向数据块
- 双向通信