进程间通信-信号量

时间:2022-12-20 15:12:25

前言

信号量的本质是⼀一种数据操作锁,它本⾝身不具有数据交换的功能,⽽而是通过控制其他的通信资源(⽂文件,外部设备)来实现进程间通信,它本⾝身只是一种外部资源的标识。信量在此过程中负责数据操作的互斥、同步等功能。当请求⼀一个使⽤信号量来表⽰示的资源时,进程需要先读取信号量的值来判断资源是否可用。大于0,资源可以请求,等于0,⽆无资源可用,进程会进⼊入睡眠状态直⾄至资源可⽤用。
当进程不再使用⼀一个信号量控制的共享资源时,信号量的值+1,对信号量的值进⾏行的增减操作均为原子操作,这是由于信号量主要的作⽤是维护资源的互斥或多进程的同步访问。而在信号量的创建及初始化上,不能保证操作均为原⼦子性。

信号量的特点:
1.信号量本质是一个计数器,描述临界资源数量
2.任何一个进程访问临界资源,都需要申请信号量。(申请P V 操作都是原子的)
3.对于信号量而言,能保证互斥,同步
4.信号量本身并不以传送数据为目的

一、什么是信号

用过Windows的我们都知道,当我们无法正常结束一个程序时,可以用任务管理器强制结束这个进程,但这其实是怎么实现的呢?同样的功能在Linux上是通过生成信号和捕获信号来实现的,运行中的进程捕获到这个信号然后作出一定的操作并最终被终止。

信号是UNIX和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中。

信号量的意图在于进程间同步,互斥锁和条件变量的意图则在于线程间同步。但是信号量也可⽤用于线程间,互斥锁和条件变量也可⽤用于进程间。我们应该使⽤用适合具体应⽤用的那组原语

描述

信号量是一个计数器,常用于处理进程和线程的同步问题,特别是对临界资源访问的同步。
获取一次信号量的操作就是对信号量减一,而释放一次信号量的操作就是对信号量加一。
Linux内核为每个信号集提供了一个semid_ds数据结构.该结构定义如下(linux/sem.h):

/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct semid_ds {
    struct ipc_perm    sem_perm;        /* 对信号操作的许可权 */
    __kernel_time_t    sem_otime;        /*对信号操作的最后时间 */
    __kernel_time_t    sem_ctime;        /*对信号进行修改的最后时间 */
    struct sem    *sem_base;        /*指向第一个信号 */
    struct sem_queue *sem_pending;        /* 等待处理的挂起操作 */
    struct sem_queue **sem_pending_last;    /* 最后一个正在挂起的操作 */
    struct sem_undo    *undo;            /* 撤销的请求 */
    unsigned short    sem_nsems;        /* 数组中的信号数 */
};

(1)Linux下使用系统函数创建和打开信号集.这个函数定义在头文件sys/sem.h中,函数原型如下:

int semget(key_t key, int nsems, int semflg);

该函数执行成功则返回一个信号集的标识符,失败返回-1。

函数第一个参数由ftok()得到键值。
第二个参数nsems指明要创建的信号集包含的信号个数,如果只是打开信号集,把nsems设置为0即可;
第三个参数semflg为操作标志,可以取如下值。
IPC_CREAT:调用semget时,它会将此值与其他信号集的key进行比较,如果存在相同的Key,说明信号集已经存在,此时返回给信号集的标识符,否则新建一个信号集并返回其标识符。
IPC_EXCL:该宏须和IPC_CREAT一起使用。使用IPC_CREAT|IPC_EXCL时,表示如果发现信号集已经存在,则返回错误,错误码是EEXIST。

(2)信号量的操作,信号量的值和相应资源的使用情况有关,当它的值大于0时,表示当前可用资源的数量,当它的值小于0时,其绝对值表示等待使用该资源的进程个数.信号量的值仅能由PV操作来改变。在Linux下,PV操作通过调用函数semop实现。该函数定义在文件/sys/sem.h,原型如下:

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

参数semid为信号集的标识符;参数sops指向进行操作的结构体数组首地址;参数nsops指出将要进行操作的信号的个数。semop函数调用成功返回0,否则返回-1.

struct  sembuf {
        ushort  sem_num; //信号在信号集的索引
        short   sem_op;  //操作类型
        short   sem_flg; //操作标志
};

sem_num表示进行操作的信号集合中的信号量下标,sem_op表示的是操作方式,sen_flg与SEM_UNDO,与防止死锁相关。

表 1 sem_op的取值和意义

取值 意义
大于0 信号加上sem_op.表示进程释放控制的资源
等于0 如果没有设置IPC_NOWAIT,则进程进入睡眠,直到信号值为0;否则进程不会睡眠,直接返回EAGAIN
小于0 信号加上sem_op的值,若没有设置IPC_NOWAIT,则进程进入睡眠,直到资源可用。否则直接返回EAGAIN

(3)信号量的控制,使用信号量时,往往需要对信号集进行一些控制操作,比如删除信号集、对内核维护的信号集的数据结构semid_ds进行设置,获取信号集中信号值等。通过semctl可以操作:(sys/sem.h)

int semctl(int semid, int semnum, int cmd,...);

函数中,参数semid为信号集的标识符,参数semnum标识一个特定的信号;cmd指明控制操作的类型。最后的“…”说明函数的参数是可选的,它依赖于第3个参数cmd,它通过共用体变量semun选择要操作的参数.semun定义在Linux/sem.h:

/* arg for semctl system calls. */
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 */
    void *__pad;
};

以上各个字段含义如下:
1)val:仅用于SETVAL操作类型,设置某个信号的值等于val
2)buf:用于IPC_STAT和IPC_SET,存取semid_ds结构
3)array:用于SETALL和GETALL操作
4)_buf:为控制IPC_INFO提供的缓存

cmd的宏含义如下:
IPC_SET:对信号量的属性进行设置
IPC_RNID:删除semid指定的信号集
GETPID:返回最后一个执行semop操作的进程ID
GETVAL:返回信号集semnum指定信号的值。
GETALL:返回信号集中所用信号的值.
GETNCNT:返回正在等待资源的进程的数量.
GETZCNT:返回正在等待完全空闲资源的进程的数量.
SETVAL:设置信号集中semnum指定的信号的值
SETALL:设置信号集中所用信号的值.

semctl() 在 semid 标识的信号量集上,或者该集合的第 semnum 个信号量上执⾏行 cmd 指定的控制命令。(信号量集合索引起始于零。)根据 cmd 不同,这个函数有三个或四个参数。当有四个参数时,第四个参数的类型是 union。调用程序必须按照下面方式定义这个联合体:

union semun {
int val; // 使用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使用缓存区
unsigned short *array; // GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用缓存区
};

注意:该联合体没有定义在任何系统头⽂文件中,因此得⽤用户⾃己声明。

#ifndef __COMM_H__
#define __COMM_H__

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

#define PATHNAME "."
#define PROJID 0x6666

int Init_Sem(int semid);
int Get_Sem();
int Create_Sem();
int Destroy_Sem(int semid);

int P_Sem(int semid,int which);
int V_Sem(int semid,int which);

#endif

comm.c

#include"comm.h"


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

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

}
static int Comm_Sem(int flags,int nsems)
{

    key_t semkey=ftok(PATHNAME,PROJID);

    if(semkey<0)
    {
        perror("ftok");
        return -1;
    }
    int semid=semget(semkey,nsems,flags);

    if(semid<0)
    {
        perror("semget");
        return -2;
    }
    return semid;

}
int Get_Sem()
{
    return Comm_Sem(IPC_CREAT,0);
}
int Create_Sem()
{
    return Comm_Sem(IPC_CREAT|IPC_EXCL|0666,1);
}
int Destroy_Sem(int semid)
{
    int ret=semctl(semid,0,IPC_RMID);
    return ret;
}
static int Comm_Op(int semid,int which,int op)
{
    struct sembuf sembuf;
    sembuf.sem_num=which;
    sembuf.sem_op=op;
    sembuf.sem_flg=0;

    int ret=semop(semid,&sembuf,1);

    if(ret<0)
    {
        perror("semop");
        return -1;
    }
    return ret;

}
int P_Sem(int semid,int which)
{
    return Comm_Op(semid,which,-1);
}
int V_Sem(int semid,int which)
{

    return Comm_Op(semid,which,1);
}

sem.c

#include<unistd.h>
#include"comm.h"

int main()
{
    int semid=Create_Sem();
    int ret=Init_Sem(semid);

    if(ret<0)
        return -1;

    printf("semid : %d\n",semid);
    pid_t id=fork();

    if(id==0)
    {
        while(1)
        {
            P_Sem(semid,0);
            usleep(1001000);
            printf("A");
            fflush(stdout);
            usleep(1000);
            printf("A");
            fflush(stdout);
            V_Sem(semid,0);
        }
    }
    else
    {
        while(1)
        {
            P_Sem(semid,0);
            printf("B");
            fflush(stdout);
            usleep(2001000);
            printf("B");
            fflush(stdout);
            usleep(100000);
            V_Sem(semid,0);

        }
        wait(NULL);
    }
    Destroy_Sem(semid);

    return 0;
}

Makefile

sem:sem.c comm.c
    gcc -o $@ $^
.PHONY: clean
clean:
    rm -f sem

进程间通信-信号量