UNIX 环境编程 之 进程间通讯(IPC) 五 System V信号量

本文详细介绍了信号量的概念,包括其作为计数器的作用及如何解决进程或线程间共享资源引发的同步问题。深入探讨了信号量的使用,如semget、semop和semctl函数,以及信号量在购票系统中的实际应用案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 概述

信号量与先前介绍的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 信号量时,提到的那个致命错误,是要开发者自己多注意的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值