【Linux】进程间通信--信号量

一. 什么是信号量
为了防止出现因多个程序同时访问一个共享资源而引发的问题,我们需要在任一时刻只能有一个执行线程访问代码的临界区域。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,信号量它本质上是一个计数器,是用来统计临界区中临界资源数目的计数器。

二.信号量的工作原理
(1)信号量在进程间传递的是个整数值。
(2)信号量是个二元信号量
(3)信号量就是为了保护临界资源,实现同步与互斥机制
(4)信号量只能进行两种操作:P操作和V操作。
临界资源之所以要保护,是由于不同进程都可以看到这份临界资源,都可以同时对其访问,这样一来就会出问题,所以引入信号量来保护临界资源,但信号量也可以被不同进程所看到,所以信号量本身也是临界资源。所以这个信号量也是要被保护起来的。这就要求对信号量的操作必须是原子性的。即对信号量的加减操作必须要是原子性的,但我们知道加减操作本身并不具备原子性,所以就有了P/V操作,它们都是原子性操作。

原理如下:
P操作:P操作申请临界资源。如果临界资源的值大于0,就给它减1;如果临界资源的值为0,就挂起该进程,该进程处于等待状态。

V操作:V操作释放临界资源。如果有其他进程因等待临界资源而被挂起,就唤醒该进程,让该进程获得临界资源恢复运行;如果没有进程因等待临界资源而被挂起,就给临界资源的值加1.

三.信号量的使用

1、创建信号量

semget函数创建一个信号量集或访问一个已存在的信号量集。

#include <sys/sem.h>
int semget (key_t key, int nsems, int semflg) ;

-返回值是一个称为信号量集标识符的整数。成功返回信号量集的键值;失败返回-1.semop和semctl函数将使用它。

-参数key表示由ftok()函数生成的信号量集的标识符。

-参数nsems设置信号量集中的信号量数。(若用于访问一个已存在的集合,那就可以把该参数指定为0)

-参数semflg可以是IPC_CREAT或IPC_EXCL
①IPC_CREAT和IPC_EXCL同时使用,创建信号量集。如果信号量集key存在,会报错;如果信号量集key不存在,则创建。
②单独使用IPC_EXCL,创建信号量集。如果信号量集key存在,则返回该信号量集;如果信号量集key不存在,则创建。

2、信号量P/V操作

使用semget打开一个信号量集后,对其中一个或多个信号量的操作就使用semop函数来执行。

#include <sys/sem.h>
int semop (int semid, struct sembuf * sops, unisigned nsops) ;

-参数semid是信号量标识符

-参数sops是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:

struct sembuf
{

    short sem_num; // 除非使用一组信号量,否则它为0 

    short sem_op; // 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作 

    short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量 

};

-参数nsops规定sops数组中元素个数。

sem_op值:

(1)若sem_op为正,这对应于进程释放占用的资源数。sem_op值加到信号量的值上。(V操作)

(2)若sem_op为负,这表示要获取该信号量控制的资源数。信号量值减去sem_op的绝对值。(P操作)

(3)若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, /*可选参数*/ ) ;

-第四个参数是可选的,取决于第三个参数cmd。

-参数semnum指定操作信号集中的哪个信号

-参数cmd指定以下10种命令中的一种,在semid指定的信号量集合内的某个信号上执行此命令。

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

4、信号量值的初始化

semget并不初始化各个信号量的值,这个初始化必须通过以SETVAL命令(设置集合中的一个值)或SETALL命令(设置集合中的所有值) 调用semctl来完成。semctl参数中的semunion参数是可选的;semunion是union semun联合体的实例。该联合体如下:

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

初始化用例如下:

int initval  = 10;
union semun sem_args;
sem_srgs.val = initval;
semtal(semid,0,SETVAL,sem_args);
内核为每个信号量集合设置了一个semid_ds结构
struct semid_ds {

struct ipc_permsem_perm ;

 structsem* sem_base ; //信号数组指针

ushort sem_nsem ; //此集中信号个数

time_t sem_otime ; //最后一次semop时间

time_t sem_ctime ; //最后一次创建时间

} ;

SystemV版本的信号量的设计中,创建一个信号量集并将它初始化需两次函数调用是一个致命的缺陷。一个不完备的解决方案是:在调用semget时指定IPC_CREAT | IPC_EXCL标志,这样只有一个进程(首先调用semget的那个进程)创建所需信号量,该进程随后初始化该信号量

四、信号量应用实例
我们接下来编写一个信号量代码,实现二元信号量对显示器进行保护
并实现父子进程输出成对AA或BB

代码如下:

comm.h
#ifndef _COMM_H
#define _COMM_H

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

#define PATHNAME "."
#define PROJ_ID 0x6666

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



int CreateSemSet(int nums);
int GetSemSet(int nums);
int InitSemSet(int semid, int nums, int initval);
int P(int semid, int who);
int V(int semid, int who);
int DestroySemSet(int semid);

#endif

comm.c
#include "comm.h"

static int commSemSet(int nsems,int flag)
{
    key_t _key = ftok(PATHNAME,PROJ_ID);
    if(_key < 0)
    {
        perror("ftok");
        return -1;
    }

    int semid = semget(_key,nsems,flag);
    if(semid < 0)
    {
        perror("semget");
        return -2;
    }

    return semid;
}

int CreateSemSet(int nums)
{
    return commSemSet(nums,IPC_CREAT | IPC_EXCL | 0666);
}

int GetSemSet(int nums)
{
    return commSemSet(nums,IPC_CREAT);
}


int InitSemSet(int semid,int nums,int initval)
{
    union semun _un;
    _un.val = initval;
    if(semctl(semid,nums,SETVAL,_un) < 0)
    {
        perror("semctl");
        return -3;
    }
    return 0;
}

static commPV(int semid, int who, int op)
{
    struct sembuf _sf;
    _sf.sem_num = who;
    _sf.sem_op = op;
    if(semop(semid,&_sf,1) < 0)
    {
        perror("semop");
        return -4;
    }
    return 0;
}

int P(int semid, int who)
{
    return commPV(semid, who, -1);
}

int V(int semid, int who)
{
    return commPV(semid, who, 1);
}


int DestroySemSet(int semid)
{
    if(semctl(semid,0,IPC_RMID) < 0)
    {
        perror("semctl");
        return -5;
    }
    return 0;
}

test.c
#include "comm.h"

int main()
{
    int semid = CreateSemSet(1);
    InitSemSet(semid,0,1);
    pid_t id = fork();
    if(id == 0)
    {//child
        int _semid = GetSemSet(0);
        while(1)
        {
            P(semid,0);
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A");
            fflush(stdout);
            usleep(321456);
            V(semid,0);

        }
    }
     else
     {//father
       while(1)
      {
          P(semid,0);
          printf("B");
          fflush(stdout);
          usleep(223456);
          printf("B");
          fflush(stdout);
          usleep(121456);
          V(semid,0);
      }
     }
     wait(NULL);

    DestroySemSet(semid);
    return 0;
}

这里写图片描述

这里写图片描述

五、SEM_UNDO标志代表什么含义,用途是什么,如何做到回滚
为什么要设置SEM_UNDO标志位
当一个进程刚一执行就因某种原因退出了,但此进程是抱着信号量退出的,即只执行了P操作,还未执行V操作就死去了。此时信号量的值一直是0,则对于其他进程来说就造成了死锁的状态。所以为了保证一个进程突然异常退出时把后续的V操作执行完,我们用sem_flg设置SEM_UNDO标志,来对进程进行回滚操作。换句话说,对于异常退出的进程,进行回滚操作后信号量的值又回到了1,那么它的意外退出就不会造成其他进程的死锁了。

当操作信号量(semop)时,结构体sembuf中的sem_flg可以设置SEM_UNDO标识;SEM_UNDO用于将修改的信号量值在进程正常退出(调用exit退出或main执行完)或异常退出(如段异常、除0异常、收到KILL信号等)时归还给信号量。

回滚操作的实现:
如果一个信号量的初始值是1,对它进行P操作是信号量变成0(表示当前资源不可用),在进程异常退出之前它将之前信号量的值恢复成初始值,也就是将它变成之前的初始值1,这样另一个进程就不会因为资源没有被解锁而长期得不到资源,处于饥饿状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值