Linux下IPC数据结构之信号量机制模拟实现

时间:2022-09-02 16:15:44
  这篇文章将讲述别一种进程间通信的机制——信号量。注意请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物。有关信号的更多内容,可以阅读我的另一篇文章:Linux进程间通信——使用信号。下面就进入信号量的讲解。

一、什么是信号量
  为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

  信号量是一个特殊的变量,它实际是一个计数器,对临界资源进行计数。程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二元信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二元信号量。

二、信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

三、Linux的信号量机制
Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

1、semget函数
它的作用是创建一个新信号量或取得一个已有信号量,原型为:
[cpp]  view plain  copy  print ?
  1. int semget(key_t key, int num_sems, int sem_flags);  
第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。

第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。

第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

semget函数成功返回一个相应信号标识符(非零),失败返回-1.

2、semop函数
它的作用是改变信号量的值,原型为:
[cpp]  view plain  copy  print ?
  1. int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);  
sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:
[cpp]  view plain  copy  print ?
  1. struct sembuf{  
  2.     short sem_num;//除非使用一组信号量,否则它为0  
  3.     short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,  
  4.                     //一个是+1,即V(发送信号)操作。  
  5.     short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,  
  6.                     //并在进程没有释放该信号量而终止时,操作系统释放信号量  
  7. };  
3、semctl函数
该函数用来直接控制信号量信息,它的原型为:
[cpp]  view plain  copy  print ?
  1. int semctl(int sem_id, int sem_num, int command, ...);  
如果有第四个参数,它通常是一个union semum结构,定义如下:
[cpp]  view plain  copy  print ?
  1. union semun{  
  2.     int val;  
  3.     struct semid_ds *buf;  
  4.     unsigned short *arry;  
  5. };  
 前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个:
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它      进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

四、进程使用信号量通信
   下面使用一个例子来说明进程间如何使用信号量来进行通信,代码实现如下:
comm.h文件:
#ifndef _COMM_
#define _COMM_

#define PATHNAME "."
#define PROJID 0x6666

union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *array;
	struct seminfo *_buf;
};

int creatSemSet(int nums);
int initSemSet(int semid, int which);
int getSemSet();
int P(int semid, int which);
int V(int semid, int which);
int destorySemSet(int semid);

#endif
comm.c文件
#include "comm.h"
#include <stdio.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdlib.h>

static int commonSemSet(int nums, int flags)
{
	key_t _k = ftok(PATHNAME, PROJID);
	int semid = semget(_k, nums, flags);
	if(semid < 0)
	{
		perror("semget");
		return -1;
	}
	return semid;
}

static int semOp(int semid, int which, int op)
{
	struct sembuf s;
	s.sem_num = which;
	s.sem_op = op;
	s.sem_flg = 0;
	int ret = semop(semid, &s, 1);
	if(ret < 0)
	{
		perror("semop");
		return -1;
	}
	return 0;
}

int creatSemSet(int nums)
{
	int flags = (IPC_CREAT | IPC_EXCL | 0666);
	return commonSemSet(nums, flags);
}

int initSemSet(int semid, int which)
{
	union semun un;
	un.val = 1;
	int ret = semctl(semid, which, SETVAL, un);
	if(ret < 0)
	{
		perror("setctl");
		return -1;
	}
	return 0;
}

int getSemSet()
{
	return commonSemSet(0,0);
}

int P(int semid, int which)
{
	return semOp(semid, which, -1);
}

int V(int semid, int which)
{
	return semOp(semid, which, 1);
}

int destorySemSet(int semid)
{
	int ret = semctl(semid, 0, IPC_RMID);
	if(ret < 0)
	{
		perror("semctl");
		return -1;
	}
	return 0;
}

test_sem.c文件
/*************************************************************************
	> File Name: test_sem.c
	> Author:lmh
	> Mail:暂无
	> Created Time: Thu 16 Feb 2017 02:35:01 AM PST
 ************************************************************************/
#include "comm.h"
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	int semid = creatSemSet(10);
	//printf("semid: %d\n", semid);

	initSemSet(semid, 0);
	pid_t id = fork();
	if(id == 0)
	{
		//child
		printf("child is running,pid:%d,ppid:%d\n", getpid(), getppid());
		while(1)
		{
			P(semid, 0);
			printf("A");
			usleep(10050);
			fflush(stdout);
			printf("A");
			usleep(11250);
			fflush(stdout);
			V(semid, 0);
		}
	}
	else
	{
		//father
		printf("father is tunning,pid:%d,ppid:%d\n",getpid(),getppid());
		while(1)
		{
			P(semid, 0);
			printf("B");
			usleep(621);
			fflush(stdout);
			printf("B");
			usleep(13100);
			fflush(stdout);
			V(semid, 0);
		}
	}

	wait(NULL);

	destorySemSet(semid);

	return 0;
}
Makefile文件
test_sem:comm.c test_sem.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm -f test_sem
    当然,我们要注意的是,应该先写好Makefile文件,调通之后再测试其他的文件是否正确,否则后面如果出现问题的话我们会忽略Makefile的存在而苦恼很久的。
测试结果我就不给大家展示了,大家可以在自己的linux下尝试一下。
五.sem_flg所包含的选项以及SEM_UNDO的功能以及实现

  semop函数:对信号量进行P,V操作(本文的重点): 

P操作负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。操作为:申请一个空闲资源(把信号量减1),若成功,则退出;若失败,则该进程被阻塞;

V操作负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。

semop函数原型如下:

int semop(int semid, struct sembuf  *sops, unsigned nsops);

semop操作中:sembuf结构的sem_flg成员可以为0IPC_NOWAITSEM_UNDO 。为SEM_UNDO时,它将使操作系统跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的。

sembuf结构的sem_flg成员为SEM_UNDO时,它将使操作系统跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量,从而使另外一个进程可以继续工作,防止其他进程因为得不到信号量而发生【死锁现象】。为此一般建议使用SEM_UNDO。