进程间通信之信号量

时间:2021-01-19 15:12:54

何为信号量

信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。

对信号量的操作

当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用:大于0,资源可以请求,将信号量的值-1(P操作);

等于0,无资源可用,进程会进入睡眠状态直至资源可用;

当进程不再使用一个信号量控制的共享资源时,信号量的值+1(V操作)。

PV操作均为原子操作。这是由于信号量主要的作用是维护资源的互斥或多进程的同步访问。而在信号量的创建及初始化上,不能保证操作均为原子性(SystemV版本信号量的缺陷),因为这个版本的信号量的创建与初始化是分开的。。

补充两个概念:

临界资源:一次只允许一个进程(一个线程)使用的资源叫做临界资源。

临界区:访问临界资源的代码称为临界区。

为什么要使用信号量

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

函数原型

像对于消息队列来说,信号量也有一套类似的接口,从它们的名字上就可以看出来。

信号量的创建

          进程间通信之信号量

其中的key依旧是可以通过ftok函数来获得,与消息队列中的key一模一样。关于第二个参数,值得一提的是这个版本的信号量的创建是以信号量集为单位的。也就是说可以一次性创建多个信号量。所以这个nsems表示的就是创建的信号量集中信号量的个数。最后一个参数又是和消息队列的flg参数一模一样:IPC_CREAT:存在则打开,否则创建;IPC_CREAT | IPC_EXCL存在则出错返回,否则创建,这样保证了打开的是一个全新的信号量集。还是要注意,这个IPC_EXCL单独使用没有任何意义。

信号量的初始化

          进程间通信之信号量

第一个参数为信号量集的id,semnum表示给信号量集中的哪一个信号量进行初始化(从0开始,分别表示第一个信号量,... ...)。cmd就是初始化命令了:SETVAL。最后用可变参数的方式传递信号量的初始值,这个参数的类型是union:

          进程间通信之信号量

其中val就是信号量的初始值,表示初始时临界资源的个数。其它字段暂时不关心。

信号量的销毁

首先说是如何在终端童通过命令查看信号量:ipcs -s ; 删除是 ipcrm -s 对应sem_id。命令都是类似的。下来是函数:

          进程间通信之信号量

对,依旧是这个函数,这个ctl能干的事情多着呢。删除就是一次性删除整个信号量集,所以第二个参数缺省0即可。第三个参数就是删除命令:IPC_RMID.

最重要的操作:

P操作和V操作

通过semop函数来完成:

          进程间通信之信号量

其中sembuf包含如下字段:

          进程间通信之信号量

sem_num表示要对信号量集中的哪一个信号量操作,sem_op为1,表示V操作,表示加1,sem_op为-1,表示P操作,表示减1。

sem_flg:信号操作标志,可能的选择有两种
IPC_NOWAIT //对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
SEM_UNDO //程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。

举个栗子

此程序由于父子进程同时向显示器打印语句,导致父子进程打印结果交叉出现:

          进程间通信之信号量

如图AB交叉出现,通过引进信号量后,父子进程对显示器这个临界资源实行互斥访问,保证了父子进程打印的消息成对出现:

          进程间通信之信号量

comm.h

#pragma once

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>

#define _PATH_NAME_ "/tmp"
#define _PROJ_ID_ 0x666

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


int create_sem_set(int nums);
int get_sem_set(int nums);
int init_sem_set(int sem_id, int which, int val);
int P(int sem_id);
int V(int sem_id);
int destroy(int sem_id);

comm.c

#include "comm.h"


static int comm_sem_set(int nums, int flag)
{
	key_t key = ftok(_PATH_NAME_, _PROJ_ID_);
	if (key < 0)
	{
		perror("ftok");
		return -2;
	}

	return semget(key, nums, flag);	
}

int create_sem_set(int nums)
{
	int flag = IPC_CREAT | IPC_EXCL | 0644;
	return comm_sem_set(nums, flag);
}

int get_sem_set(int nums)
{
	int flag = IPC_CREAT;
	return comm_sem_set(nums, flag);
}

int init_sem_set(int sem_id, int which, int val)
{
	union semun un;
	un.val = val;
	
	return semctl(sem_id, which, SETVAL, un);
}

static int pv(int sem_id, int op)
{
	struct sembuf buf;
	buf.sem_num = 0;
	buf.sem_op = op;
	buf.sem_flg = 0;

	return semop(sem_id, &buf, 1);
}

int P(int sem_id)
{
	return pv(sem_id, -1);
}

int V(int sem_id)
{
	return pv(sem_id, 1);
}

int destroy(int sem_id)
{
	return semctl(sem_id, 0, IPC_RMID);	
}

test_sem.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "comm.h"
 
int main()
{
	int sem_id = create_sem_set(1);
	init_sem_set(sem_id, 0, 1);

	pid_t id = fork();
	if (id < 0)
	{
		perror("fork");
		return 1;
	}
	else if (id == 0) // child
	{
		while (1)
		{
			P(sem_id);

			printf("A");
			fflush(stdout);
			usleep(rand()%12345);
			usleep(200000);
			printf("A");
			fflush(stdout);
			usleep(rand()%12345);

			V(sem_id);
		}
	}
	else // father
	{
		while (1)
		{
			P(sem_id);

			printf("B");
			fflush(stdout);
			usleep(rand()%12345);
			usleep(200000);
			printf("B");
			fflush(stdout);
			usleep(rand()%12345);

			V(sem_id);
		}
	}

	
	destroy(sem_id);
	return 0;
}