一 概述
信号量与先前介绍的IPC(管道,FIFO,以及消息队列)不同,它是一个计数器,主要用来解决进程或线程间共享资源引发的同步问题。使得资源在一个时刻只有一个进程(线程)所拥有。
信号量只能进行两种,等待(使用信号)和发送(释放)信号:
- 使用信号(P):如果信号量的值大于零,就给它减1,表示它使用了一个资源单位。如果它的值为零,就挂起该进程的执行,表示资源被占用。
- 释放信号(V):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
二 信号量函数使用
在说信号量使用函数时,先说下信号量结构体
struct semid_ds {
struct ipc_perm sem_perm ;
struct sem* sem_base ; //信号数组指针
ushort sem_nsem ; //此集中信号个数
time_t sem_otime ; //最后一次semop时间
time_t sem_ctime ; //最后一次创建时间
} ;
struct sem {
ushort_t semval ; //信号量的值
short sempid ; //最后一个调用semop的进程ID
ushort semncnt ; //等待该信号量值大于当前值的进程数(一有进程释放资源 就被唤醒)
ushort semzcnt ; //等待该信号量值等于0的进程数
} ;
1.semget函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int oflag) ;
返回:若成功则返回信号量ID,若出错则为-1
创建或者打开一个IPC对象。
参数一:key_t key 可通过ftok获取 ,也可指定IPC_PRIVATE,这会保证创建一个新的,唯一的IPC对象。
参数二:int nsems 集合中信号量数目,如果不创建新的信号集,只是访问一个已存在的信号集,nsems 可指定为0 。 一旦创建完一个信号集,就不能再改变nsems的值了。
参数三:oflag 权限标志,它的作用与open函数的mode参数一样,
- oflag 设置为IPC_CREAT但不设置它的IPC_EXCL,如果IPC对象不存在,则创建一个新的IPC对象,否则会返回该已经存在的IPC对象
- 同时设置shmflg为IPC_CREAT|IPC_EXCL,如果IPC对象不存在,则创建一个新的IPC对象,否则返回EEXIST错误,因为该对象已经存在
注意点:
- 该函数若是创建新的信号集时,会初始化上面提到的信号量结构体struct semid_ds ,sem_nsem 被置为nsems 参数的值,sem_otime=0,sem_ctime = 当前时间。还有就是初始化struct ipc_perm sem_perm 内的值。
- 但是与每个信号量关联的各个sem结构不会初始化(struct sem* sem_base ),这些结构要在semctl函数,以SET_VAL或SET_ALL调用设置。
**致命的缺陷:**创建信号集(shmget)并初始化信号量(semctl)需调用两次函数。这存在 多个进程时会导致第一个进程只是创建了信号集,还没有初始化信号量 。第二个进程使用该信号量时,信号量还没有初始化的问题。
2.semop 函数
#include <sys/sem.h>
int semop (int semid, struct sembuf * opsptr, size_t nops) ;
返回 成功 0; 失败出错 -1;
参数一: IPC对象的标识符,即shmget 返回值
参数二:struct sembuf * opsptr
struct sembuf{
short sem_num; // (范围 0 ~ nsems-1) 指定你要操作的那个信号集
short sem_op; // 信号量在一次操作中需要改变的数据
short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量
};
◆参数nops规定opsptr数组中元素个数。
sem_op值:
- 若sem_op为正,这对应于进程释放占用的资源数。sem_op值加到信号量的值上,信号量的值就是struct sem 结构体中的semval 的值。(V操作)
- 若sem_op为负, 这表示要获取该信号量控制的资源数。信号量值减去sem_op的绝对值。(P操作)
- 若sem_op为0, 这表示调用进程希望等待到该信号量值变成0。
◆如果信号量值小于sem_op的绝对值(资源不能满足要求),则:
(1)若指定了IPC_NOWAIT,则semop()出错返回EAGAIN。
(2)若未指定IPC_NOWAIT,则信号量的semncnt值加1(因为调用进程将进 入休眠状态),然后调用进程被挂起直至:①此信号量变成大于或等于sem_op的绝对值;②从系统中删除了此信号量,返回EIDRM;③进程捕捉到一个信 号,并从信号处理程序返回,返回EINTR。(与消息队列的阻塞处理方式 很相似)
参数三:表示要调用的操作数
3 semctl 函数
#include <sys/sem.h>
int semctl (int semid, int semnum, int cmd, .../*union semum arg*/ ) ;
返回值 成功 > 0 ; 失败 -1
参数二:semnum 标识该信号量集内的某个成员 (0~ nsems -1)
参数三:cmd
IPC_STAT 读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET 设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID 将信号量集从内存中删除。
GETALL 用于读取信号量集中的所有信号量的值。
GETNCNT 返回正在等待资源的进程数目。
GETPID 返回最后一个执行semop操作的进程的PID。
GETVAL 返回信号量集中的一个单个的信号量的值。
GETZCNT 返回这在等待完全空闲的资源的进程数目。
SETALL 设置信号量集中的所有的信号量的值。
SETVAL 设置信号量集中的一个单独的信号量的值。
参数四:可选的,取决与参数三cmd的取值
union semun {
int val ; /* for SETVAL */
struct semid_ds * buf ; /* for IPC_STAT and IPC_SET */
ushort * array; /* for GETALL and SETALL */
} ;
三 信号量的使用
我们知道当两个进程同时操作一块资源时,就会引起很多奇怪的问题,而信号量主要是解决资源同步的问题,使得资源在一个时刻只有一个进程(线程)所拥有。
下面是购票的一个常见问题,用了共享内存 shm 实现进程间的通讯。如果对shm不太清楚的可查看楼主 进程间通讯(IPC) 四 System V共享存储
首先是主程序: ticket_main
ticket_main.c
/*
* ticket_main.c
*
* Created on: 2019-5-7
* Author: root
*/
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
typedef struct _channel_info_t {
int ticket_id[3];
int current_idx;
int rest_ticket; //剩下多少张票
} ticket_info_t;
#define SHM_KEY 1357
int main(void)
{
int shm_fd = shmget(SHM_KEY, sizeof(ticket_info_t), 0666 | IPC_CREAT);
if(shm_fd <0){
return shm_fd;
}
void *shm_addr = shmat(shm_fd, NULL, 0);
if (shm_addr == (void *) -1) {
return -1;
}
//初始化了两张票
ticket_info_t * ticket_info = shm_addr;
memset(ticket_info,0,sizeof(ticket_info_t));
ticket_info->rest_ticket = 2;
printf("init ticket success has total %d tickets\n",ticket_info->rest_ticket);
//同时主程序自己购买了一张票
ticket_info->ticket_id[ticket_info->current_idx++] = getpid();
ticket_info->rest_ticket --;
printf("main buy ticket one ticket only %d ticket left\n",ticket_info->rest_ticket);
shmdt(shm_addr);
return 1;
}
ticket_main 主进程 主要是创建了一块共享内存,里面初始化了两张票,然后主程序自己买了一张票。
子进程
buy_ticket.c
/*
* buy_ticket说.c
*
* Created on: 2019-5-7
* Author: root
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
typedef struct _channel_info_t {
int ticket_id[3];
int current_idx;
int rest_ticket; //剩下多少张票
} ticket_info_t;
#define SHM_KEY 1357
int main(void)
{
int shm_fd = shmget(SHM_KEY, sizeof(ticket_info_t), 0666 | IPC_CREAT);
if(shm_fd <0){
return shm_fd;
}
void *shm_addr = shmat(shm_fd, NULL, 0);
if (shm_addr == (void *) -1) {
return -1;
}
ticket_info_t * ticket_info = shm_addr;
printf("has %d ticket you can buy\n",ticket_info->rest_ticket);
if(ticket_info->rest_ticket >0){
sleep(1);
ticket_info->ticket_id[ticket_info->current_idx] = getpid();
ticket_info->rest_ticket --;
printf("you success buy ticket num is:%d \n only %d ticket left\n",ticket_info->ticket_id[ticket_info->current_idx],ticket_info->rest_ticket);
ticket_info->current_idx++;
}
shmdt(shm_addr);
return 0;
}
而子进程拿到票的资源信息,当还有剩余票的时候,购买了一张票。
运行:先启动主进程,结果如下
运行子进程:
同时运行了两个子进程,子进程获取到票发现还剩一张票,然后都去买了这张票,结果发现显示还有-1张剩余票了。这显然是不正确的。
---------------------------------------------------分割线-----------------------------------------------
于是需要在子进程要操作资源前,加上信号量
子进程程序如下
/*
* shmread.c
*
* Created on: 2019-5-7
* Author: root
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
#include "vg_sem.h"
typedef struct _channel_info_t {
int ticket_id[3];
int current_idx;
int rest_ticket; //剩下多少张票
} ticket_info_t;
#define SHM_KEY 1357
int main(void)
{
int shm_fd = shmget(SHM_KEY, sizeof(ticket_info_t), 0666 | IPC_CREAT);
if(shm_fd <0){
return shm_fd;
}
void *shm_addr = shmat(shm_fd, NULL, 0);
if (shm_addr == (void *) -1) {
return -1;
}
ticket_info_t * ticket_info = shm_addr;
vg_sem_lock(NULL);
printf("has %d ticket you can buy\n",ticket_info->rest_ticket);
if(ticket_info->rest_ticket >0){
sleep(1);
ticket_info->ticket_id[ticket_info->current_idx] = getpid();
ticket_info->rest_ticket --;
printf("you success buy ticket num is:%d \n only %d ticket left\n",ticket_info->ticket_id[ticket_info->current_idx],ticket_info->rest_ticket);
ticket_info->current_idx++;
}
vg_sem_unlock(NULL);
shmdt(shm_addr);
return 0;
}
上面代码加入了vg_sem_lock vg_sem_unlock 这是自己对信号量实现的封装,后面会给出代码,这里先看加入信号量的结果
主进程还是先启动,初始化票数,然后子进程运行
结果发现正确
这里列出vg_sem_lock vg_sem_unlock 的代码
//获取或等待资源
int vg_sem_p(int semid){
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if(semop(semid, &sem_b, 1) == -1)
{
vg_errno = VG_ERROR_FAILURE;
return -1;
}
return 1;
}
//释放资源 使信号量变为可用
int vg_sem_v(int semid){
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;//S()
sem_b.sem_flg = SEM_UNDO;
if(semop(semid, &sem_b, 1) == -1)
{
vg_errno = VG_ERROR_FAILURE;
return -1;
}
return 1;
}
int vg_sem_lock(char * file_path){
int key_val , semid ,i;
if(file_path ==NULL){
key_val = KEY_VAL;
}else{
key_val = ftok(file_path,1);
}
union semun arg;
struct semid_ds seminfo;
if((semid = semget(key_val, 1, 0666|IPC_CREAT|IPC_EXCL)) > 0){
arg.val =1;
semctl(semid,0,SETVAL,arg);
}else if(errno == EEXIST){
semid = semget(key_val, 1, 0666);
arg.buf = &seminfo;
for(i = 0; i<10; i++){
semctl(semid,0,IPC_STAT,arg);
if(arg.buf->sem_otime != 0){
vg_sem_p(semid);
return semid;
}
sleep(1);
}
vg_errno = VG_ERROR_FAILURE;
return -1;
}else{
vg_errno = VG_ERROR_FAILURE;
return -1;
}
vg_sem_p(semid);
return semid;
}
int vg_sem_unlock(char * file_path){
int key_val , semid ;
if(file_path ==NULL){
key_val = KEY_VAL;
}else{
key_val = ftok(file_path,1);
}
semid = semget(key_val, 1, 0666);
if(semid <0){
vg_errno = VG_ERROR_FAILURE;
return -1;
}
vg_sem_v(semid);
return semid;
}
这里主要说下vg_sem_lock 函数:前面提到信号量创建的时候有个致命错误:(*创建信号集(semget)并初始化信号量(semctl)需调用两次函数。这存在 多个进程时会导致第一个进程只是创建了信号集,还没有初始化信号量 。而第二个进程就使用未初始化的信号量)
因此vg_sem_lock 给出的是不完备的解决方案是:
大概意思就是第一次初始化成功IPC后,紧跟着调用semop 函数,使得初始化 的struct semid_ds结构体 有个sem_otime 函数就不在为0 ,(sem_otime 该值表示最后一次调用semop 函数的时间),从而下次使用信号量IPC 对象时,可以先取得该值,判断是否为0,从而到达判断信号量是否初始化成功了。
当然给出的vg_sem_lock 函数还是不太完善的,你会发现每次加锁都会先去判断信号量是否创建过,然后获取信号量IPC对象 。显然理想的状态是第一次没创建,创建初始化。而后面就直接使用创建的IPC 对象加锁即可。 这里可以通过两个全局变量实现,一个是返回的shm_id ,一个是 init_flag ,分别表示创建的IPC对象,和该IPC是否是第一次创建。具体实现不再深入。
四 总结
- 一般我们使用信号量,使用的比较多的就是二值信号量(表示信号量的值不是0,就是1,同时只有一个信号量)
- 但是其实除了二值信号量,还有计数的信号量,和计数的信号量集。计数信号量就是信号量的值时自己设定的代表一个资源数,而信号量集,从初始化的信号量的结构体你就可以看出,System V信号量 是可以有多个计数信号量的。这里没有深入讨论。
- 最后在初始化创建IPC 信号量时,提到的那个致命错误,是要开发者自己多注意的。