Linux — IPC进程通信之信号量

时间:2022-09-20 15:16:48

IPC进程通信之信号量详解







信号量与已经介绍过的IPC机构不同,它是一个计数器,用于为多个进程提供对共享数据对象的访问. 为了获得共享资源,进程需要执行下列操作:

(1)测试控制该资源的信号量

(2)若此信号量的值为正,则进程可以使用该资源. 在这种情况下,进程会将信号量值减去1,表示它使用了一个资源单位.

(3)否则,若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0. 如果有进程正在休眠等待此信号量,则唤醒他们.

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作. 为此,信号量通常是在内核中实现的.

常用的信号量形式被称为二元信号量. 它控制单个资源,其初始值为1. 但是,一般而言,信号量的初值可以是任意一个正值,该值表明有多少个共

资源单位可供共享应用.  信号量有几个需要注意的地方:

1.信号量并非是单个非负值,而必需定义为含有一个或多个信号量值的集合. 当创建信号量时,要指定集合中信号量值的数量

2.信号量的创建时独立于它的初始化的,知识一个致命的缺点,因为不能原子地创建一个信号量集合,并且对该集合中的各个信号量值赋初值.

3.即使没有进程正在使用各种形式的IPC,他们依然是存在的. 有点程序在终止时并没有释放已经分配给它的信号量,所以我们不得不为这种程序担

心. 后面将要说明undo功能就是处理这种情况的:

为了防止多个因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生产并使用令牌来授权,在 任一时刻只能有

一个 执行线程访问代码的临界区域. 临界区域是指执行数据更新的代码需要独占试的执行.而信号量就可以提供这样 的一种访问机制,让一个临界区

同一时 间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的.其*享内 存的使用就要用到信号量.

内核为每一个信号量集合维护这一个semid_ds结构:

struct semid_ds
{
struct ipc_perm sem_perm;
unsigned short sem_nsems;
time_t sem_otime;
time_t sem_ctime;
};

每个信号量由一个无名结构表示,它至少包含下列成员:
struct ...
{
unsigned short semval;
pid_t sempid;
unsigned semncnt;
unsigned semzcnt;
.
.
.
}




二.信号量的工作原理



由于信号量只能进行两种操作等待和发送信号,既P V,他们的行为是这样的: (这里的SV就是共享信号量)

P(sv): 如果sv的值大于零,就给它减一; 如果它的值为零,否则就挂起该进程

V(sv): 如果有其他进程因等待sv而被挂起,就让他恢复运行,如果没有进程因等待sv而挂起,就给它+1.


这个P V操作其实非常好理解,举个例子我们这里的两个进程共享信号量sv是一个公共厕所(我给你讲不要针对这个名字,其实例子 很恰 当的!),当

进程 一来了执行P(sv)操作,进入厕所并且让sv标记为有人(给sv-1),当进程二来了执行P(sv),发现sv为零,然后 它蹲在门口等着 (被挂起等待),

当第一 个进程离开临界区域并执行V(sv)操作释放信号量(sv+1),这时第二个进程就可以恢 复执行 信号量都是以信号量集的形式创建的,再然后信号

量的生命周 期是随内核的.


三.相关的Linux命令


查看信号量的命令: ipcs -s

Linux — IPC进程通信之信号量

删除一条信号量的命令: ipcrm -q semid(信号量的semid号).

Linux — IPC进程通信之信号量


四.信号量的相关函数


1.semget函数

创建一个新的信号量或取得一个已有的信号量

int semget(key_t key, int num_sems, int sem_flags);  


参数意义:

key:参数key是一个整数值(不为0),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信

号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),

只有semget函数才能直接使用信号量键,所有其他的信号量使用由semget函数返回的信号量标识符.如果多个程序使用相同的key

值,key将负责协调工作.


num_sems:指定需要的信号量数目,它的值几乎总为1.


sem_flags:他是一个标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作.设置IPC_CREAT标识后

,即使给出的键是一个已有的信号量键,也不会产生错误.而IPC_CREAT|IPC_EXCL则可以创建一个新的唯一的的信号量,如果信号量

存在,返回一个错误.

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




2.semop函数


它的作用是改变信号量的值:

int semop(int sem_id, struct sembuf *sem_opa,size_t num_sem_ops);


参数意义:

sem_id:是由semget函数返回的信号量标识符.

struct sembuf: struct sembuf 的结构如下定义:

struct sembuf{  
short sem_num;//除非使用一组信号量,否则它为0
short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};


nsops:操作结构的数量,恒大于或等于1.

对集合中每个成员的操作由相应的sem_op值规定,此值可以是负值,0,正值. 论将提到信号量的"undo"标志. 此标志对应于相应的sem_flg成员的

SEM_UNDO位 undo标志:通俗一点来讲就是让进程对一个信号量p操作之后呢,无论是异常退出还是正常退出 都会自动的释放该信号量资源.

(1)最易于处理的情况是sem_op为正值,这对英语进程释放占用的资源数. sem_op值会加到信号量的值上,如果指定了undo标志,则也要从该进程

的此信号量调整值中减去sem_op.

(2)若se_op为负值,则表示要获取由该信号量控制的资源.

如果该信号量的值大于或等于sem_op的绝对值(具有所需的资源),则从信号量值中减去sem_op的绝对值. 这能保证信号量的结果值大于等于0,如果

指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上.

如果信号量的值小于sem_op的绝对值,则适用于下面的条件:

a.若指定了IPC_NOWAIT,则semop出错返回EAGAIN.

b.若未指定IPC_NOWAIT,则该信号量的semncnt+1(因为调用进程将进入休眠模式),然后调用进程被挂起直至下列事情之一发生:

i.此信号量值变成大于等于sem_op的绝对值(既某个进程已经释放了某些资源). 此信号量的semncnt的值减一,并且从信号量值中减去sem_op的绝对

值.如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值当中.

ii.从系统中删除了此信号量,自这种情况下,函数出错返回EIDRM.

iii. 进程捕捉到一个信号,并从信号处理程序返回,在这种情况下,此信号量的semncnt的值减去1,并且函数出错返回EINTR。

(3)若sem_op为0,这表示调用进程希望等待到该信号量的值变为0.

如果信号量的当前为0,则此函数立即返回.   如果信号量非0,则适用下面的条件:

a.若指定了IPC_VOWAIT,则出错返回EAGAIN.

b.若未指定IPC_NOWAIT,则信号量的semzcnt的值加1,然后调用进程被挂起,直至下列的一个事件发生.

i.此信号量的值变成0,此信号量的semzcnt减去一(因为调用进程已结束等待)

ii.从系统中删除此信号量,在这种情况下,函数出错返回EIDRM.

iii.进程捕捉到一个信号,并且从信号处理程序返回. 在这种情况下,此信号量的semzcnt值减去1,并且函数出错返回EIINTR.

semop函数具有原子性,他执行数组中的所有操作,又或者一个也不做.



3.semctl函数


该函数用来直接控制信号量信息.也就是直接删除信号量或者初始化信号量.

int semctl(int sem_id, int sem_num, int command, ...);  


参数意义:

sem_id:是由semget函数返回的信号量标识符.

sem_num: 操作信号在信号集中的编号,从0开始.

command : 命令,标识要进行的操作->

参数command中可以使用的命令如下:

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设置信号量集中的一个单独的信号量的值。


如果有第四个参数,他通常是一个union semum结构,定义如下:

union semun{  
int val;
struct semid_ds *buf;
unsigned short *arry;
};




五:运用信号量的实例


我这里在linux的平台下,创建一个子进程让子进程的不停的输出A,然后父进程不停地输出B.大致上程序就是这样:

int main()
{
if (fork() == 0)
{
//child
while (1)
{
printf("A");
fflush(stdout);
usleep(100000);
printf("A ");
fflush(stdout);
usleep(100000);
}
}
else{//father
while (1){
printf("B");
fflush(stdout);
usleep(100000);
printf("B ");
fflush(stdout);
usleep(100000);
}
return 0;
}
}

输出的结果杂乱无章->

Linux — IPC进程通信之信号量

但是我现在呢,我希望输出的是AA BB这样输出,再详细一点就是子进程在打印时父进程就在一旁等待,父进程在打印的时候子进程 在一旁等待,这

里就 是信号量大展身手的时候了,我们想想我们是不是可以使用信号量的P V操作对进程的运行进行限制. 我们再来看看修改后的代码->

/*************************************************************************
> File Name: mestext.c
> Author: ma6174
> Mail: ma6174@163.com
> Created Time: Thu 22 Jun 2017 11:03:42 PM PDT
************************************************************************/

#include"comm.h"

int main()
{
int semid = createSemSet(10);
initSemSet(semid,0,1);

if(fork() == 0)
{
//child
int _semid = getSemSet(1);
while(1)
{
p(_semid , 0);
printf("A");
fflush(stdout);
usleep(100000);
printf("A ");
fflush(stdout);
usleep(100000);
v(_semid , 0);
}
}else{//father
while(1){
p(semid,0);
printf("B");
fflush(stdout);
usleep(100000);
printf("B ");
fflush(stdout);
usleep(100000);
v(semid,0);
}
destorySemSet(semid);
return 0;
}
}

首先这里面的用到的每一个函数,都是我针对信号量功能的一个封装,比如说P V操作,每个函数里面都是灵活运用到了上面讲解到

的函数.现在首先来看comm.h

/*************************************************************************
> File Name: comm.h
> Author: ma6174
> Mail: ma6174@163.com
> Created Time: Thu 22 Jun 2017 08:31:41 PM PDT
************************************************************************/

#ifndef _COMM_H__
#define _COMM_H__
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define PATHNAME "."
#define PROJ_ID 0x6666

//semctl函数需要用到的结构体.
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};

//对某个信号量进行初始化
int initSemSet(int semid, int which, int val);
//创建一个信号量
int createSemSet(int nums);
//销毁一个信号量
int destorySemSet(int semid);
//信号量的P操作
int p(int semid, int num);
//信号量的V操作
int V(int semid, int num);
//获取一个信号量
int getSemSet(int semid);



#endif //_COMM_H__



接下来看每个函数的具体实现,我这里希望的就是大家带着问题去在函数中探索,不是单纯的看懂了这个函数就算了,没有去思考 函数为什么这么

实现 或者还有没有更优的解决方法.还有Linux中最重要的其实还是对库函数参数的了解,因为这里参数太多还有种 类繁杂所以需要我们时常的复习.


/*************************************************************************
> File Name: comm.c
> Author: ma6174
> Mail: ma6174@163.com
> Created Time: Thu 22 Jun 2017 08:48:37 PM PDT
************************************************************************/

#include"comm.h"


//这是由于createSemSet和getSemSet代码复用性及其的高,其实只是一个参数的不同
//int semget(key_t key, int num_sems, int sem_flags); 注意sem_flags参数不同时函数不同的功能.
//所以对他们的公共部位进行一个函数的封装.
static int comSemSet(int nums, int flags)
{
key_t _k = ftok(PATHNAME, PROJ_ID);
if (_k < 0)
{
perror("ftok");
return -1;
}
int semid = semget(_k, nums, flags);
if (semid < 0)
{
perror("semget");
return -2;
}
return semid;
}

//创建一个信号量
int createSemSet(int nums)
{
return comSemSet(nums, IPC_CREAT | IPC_EXCL | 0666);
}

//获取一个信号量
int getSemSet(int nums)
{
return comSemSet(nums, IPC_CREAT);
}


//因为这里P操作和V操作代码服用性太高,无非就是对_sf.sem_op的值修改为1和-1
//所以在这里封装一个公共的函数来进行代码服用.
static int commPV(int semid, int nums, int op)
{
struct sembuf _sf;
_sf.sem_num = nums;
_sf.sem_op = op;
_sf.sem_flg = 0;

if (semop(semid, &_sf, 1) != 0){
perror("semop");
return -1;
}
return 0;
}

//P操作的封装
int p(int semid, int num)
{
return commPV(semid, num, -1);
}
//V操作的封装
int v(int semid, int num)
{
return commPV(semid, num, 1);
}

//对信号量集里面单个信号量进行初始化.
int initSemSet(int semid, int which, int val)
{
union semun _un;
_un.val = val;
if (semctl(semid, which, SETVAL, _un) != 0)
{
perror("semctl");
return -1;
}
return 0;
}

//销毁一个信号量.
int destorySemSet(int semid)
{
if (semctl(semid, 0, IPC_RMID) < 0){
perror("semctl");
return -1;
}
return 0;
}


这些封装函数的具体实现对照着上面提到的库函数一定可以看明白实现原理,接下来我们继续来看mestext.c,在main函数里,我们

已经给父子进程加上了信号量的限制,当子进程运行时,因为父进程的第一句就是同一信号量的P操作所以挂起等待,当父进程运行

时则反之.最后我再贴出我的Makefile然后我们来看运行的结果。

Linux — IPC进程通信之信号量



运行结果:

Linux — IPC进程通信之信号量



最后的最后我再提醒一句,信号量使用完毕后一定要删除,要不然对于你的程序下一次使用是有影响的,因为如果你的semget函数 sem_flags参数

果为IPC_CREAT|IPC_EXCL,没有删除掉信号量下次使用该程序时就会直接报错. 我在这里是有血的教训的..... 调BUG调了半天.



六.总结


信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操 作。我们通常通

过信 号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也 可以说它是协调进程间的对同一

资源的访 问权,也就是用于同步进程的。