进程间通信(4) 信号量

之前我们学习了管道,消息队列,共享内存,今天我们再来学一种进程间通信的方式-----信号量

信号量

    信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。

在了解信号量之前,我们先来看几个概念

临界资源:两个进程看到的同一个公共的资源,但是同时只能被一个进程所使用的的资源叫做临界资源(互斥资源)

临界区:在晋城中涉及到互斥资源的程序段叫临界区

信号量主要用于同步和互斥,下面我们来看看什么是同步和互斥。

互斥:各个进程都要访问共享资源,但共享资源是互斥的,同时只能有一个进程使用。因此,各个进程之间竞争使用这些资源,将这种关系称为互斥。

同步:多个进程需要相互配合共同完成一项任务。

信号量的工作机制

扫描二维码关注公众号,回复: 1911036 查看本文章

    简单说一下信号量的工作机制,可以直接理解成计数器,信号量会有初值(>0),每当有进程申请使用信号量,通过一个P操作来对信号量进行-1操作,当计数器减到0的时候就说明没有资源了,其他进程要想访问就必须等待,当该进程执行完这段工作(我们称之为临界区)之后,就会执行V操作来对信号量进行+1操作。

由于信号量只能进行两种操作等待和发送信号,即 PV ,他们的行为是这样的:(进程共享信号量sv)
P:如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

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

在信号量进行PV操作时都为原子操作(因为它需要保护临界资源)

信号量结构体

struct semaphore
{
    int value;
    pointer_PCB queue;
}

P原语

P(s)
{
    s.value = s.value--;
    if (s.value < 0)
    {
        该进程状态置为等待状态
        将该进程的PCB插入相应的等待队列s.queue末尾
    }
}

V原语

V(s)
{
    s.value = s.value++;
    if (s.value < =0)
    {
        唤醒相应等待队列s.queue中等待的一个进程
        改变其状态为就绪态
        并将其插入就绪队列
    }
}

信号量集结构

struct semid_ds {
    struct ipc_perm sem_perm; /* Ownership and permissions */
    time_t sem_otime; /* Last semop time */
    time_t sem_ctime; /* Last change time */
    unsigned short sem_nsems; /* No. of semaphores in set */
};

信号量集函数

semget函数

功能:⽤用来创建和访问⼀一个信号量集

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

参数

    key: 信号集的名字

    nsems:信号集中信号量的个数

    semflg: 由九个权限标志构成,它们的⽤法和创建⽂件时使用的mode模式标志是⼀样的

        IPC_CREAT|IPC_EXCL:不存在创建,存在出错返回

        IPC_CREAT:不存在创建,存在返回

        设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

返回值:成功返回⼀个⾮负整数,即该信号集的标识码;失败返回-1

shmctl函数

功能:⽤用于控制信号量集

原型

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

    semid:由semget返回的信号集标识码

    semnum:信号集中信号量的序号

    cmd:将要采取的动作(有三个可取值)最后一个参数根据命令不同⽽不同

返回值:成功返回0;失败返回-1

命令 说明
SETVAL 设置信号量集中的信号量的计数值
CETVAL 获取信号量集中的信号量的计数值
IPC_STAT 把semid_ds结构中的数据设置为信号集的当前关联值
IPC_SET 在进程由足够权限的前提下,把信号集的当前关联值设置为semid_ds数据结构中给出的值
IPC_RMID 删除信号集
semop函数

功能:⽤来创建和访问⼀个信号量集

原型
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数
    semid:是该信号量的标识码,也就是semget函数的返回值
    sops:是个指向⼀个结构数值的指针
    nsops:信号量的个数

返回值:成功返回0;失败返回-1

sembuf定义如下:

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

信号量操作步骤:

一、创建或获取一个信号量,调用semget()函数。

二、初始化信号量,调用semctl()。

三、进行信号量的PV操作,调用semop()函数。

四、信号退出时,从系统中删除该信号,调用semctl(),IPC_RMID操作。

接下来我们用信号量来实现两个进程间的通信:


代码如下:

comm.h

#ifndef __COMM_H__
#define __COMM_H__

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

#define  PATHNAME "."
#define  PROJ_ID  0 

union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
    struct seminfo *__buf;
};
//信号量是创建还是获取在于semget函数参数flag的设置
static int CommSemid(int nums, int flags);
//创建信号量
int CreatSemid(int nums);
//获取已经创建的信号量
int GetSemid(int nums);
//初始化信号量
int InitSem(int semid, int which, int _val);
//PV操作在于它_op的值
static int SemPV(int semid, int which, int _op);
//P操作
int P(int semid, int which, int _op);
//V操作
int V(int semid, int which, int _op);
//由于(System V通信方式)信号量生命周期随内核,所以要销毁信号量
int Destory(int semid);

#endif

comm.c

#include "comm.h"

static int CommSemid(int nums, int flags)
{
	key_t _key = ftok(PATHNAME, PROJ_ID);
	if (_key>0)
	{
		return semget(_key, nums, flags);
	}
	else
	{
		perror("CommSemid");
		return -1;
	}
}

int CreatSemid(int nums)
{
	return CommSemid(nums, IPC_CREAT | IPC_EXCL | 0666);
}

int GetSemid(int nums)
{
	return CommSemid(nums, IPC_CREAT);
}
int Destory(int semid)
{
	if (semctl(semid, 0, IPC_RMID)>0)
	{

		return 0;
	}
	else
	{
		perror("Destory");
		return -1;
	}
}

int InitSem(int semid, int which, int _val)
{

	union semun _semun;
	_semun.val = _val;
	if (semctl(semid, which, SETVAL, _semun)<0)
	{
		perror("InitSem");
		return -1;
	}
	return 0;
}
static int SemPV(int semid, int which, int _op)
{
	struct sembuf _sf;
	_sf.sem_num = which;
	_sf.sem_op = _op;
	_sf.sem_flg = 0;
	return semop(semid, &_sf, 1);
}

int P(int semid, int which, int _op)
{
	if (SemPV(semid, which, _op)<0)
	{
		perror("P");
		return -1;
	}
	return 0;
}

int V(int semid, int which, int _op)
{
    if (SemPV(semid, which, _op)<0)
    {
        perror("V");
        return -1;
    }
    return 0;
}

test_sem.c

#include "comm.h"

int main()
{
    int semid = CreatSemid(1);
    printf("%d\n", semid);
    InitSem(semid, 0, 1);
    pid_t id = fork();
    if (id == 0)
    {//child
        int semid = GetSemid(0);
        while (1)
        {
            //P(semid, 0, -1);
            printf("A");
            fflush(stdout);
            usleep(100000);
            printf("A ");
            fflush(stdout);
            usleep(200000);
            //V(semid, 0, 1);
        }
    }
    else
    {//father
        while (1)
        {
            //P(semid, 0, -1);
            usleep(300000);
            printf("B");
            fflush(stdout);
            usleep(400000);
            printf("B ");
            fflush(stdout);
            usleep(20000);
            // V(semid, 0, 1);
        }
        if (waitpid(id, NULL, 0) < 0)
        {
            perror("waitpid");
            return -1;
        }
    }
    Destory(semid);
    return 0;
}

Makefile

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

此时显示器只有一个,两个进程同时打印,此时显示器成为临界资源,使用二元信号量(互斥锁)进行保护。

我们在运行结果前,可以使用 ipcs -s 命令查看信号进行进程间通信的信息。


可以看出,此时并没有信息。

我们现在来运行代码,结果如下:


我们可以看到所有的AB都是成对出现的,不会出现交叉的情况。这是因为P、V操作实现过程中具有原子性,能够实现对临界区

的管理,它的执行是不会受其他进程的影响。

但是如果我们不加P、V操作,那么会出现什么样的情况呢??我们来看一下:

代码修改后,运行结果如下:


我们可以发现,屏幕上输出的AB出现交叉,并没有成对打印。


猜你喜欢

转载自blog.csdn.net/lu_1079776757/article/details/79678585