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

信号量的相关概念:

信号量

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

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

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

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

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

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

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

信号量的工作机制

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

由于信号量只能进行两种操作等待和发送信号,即 P 和 V ,他们的行为是这样的:(进程共享信号量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模式标志是⼀一样的
返回值:成功返回⼀一个⾮非负整数,即该信号集的标识码;失败返回-1

shmctl函数:

功能:⽤用于控制信号量集
原型
int semctl(int semid, int semnum, int cmd, ...);
参数
semid:由semget返回的信号集标识码
semnum:信号集中信号量的序号
cmd:将要采取的动作(有三个可取值)
最后⼀一个参数根据命令不同⽽而不同
返回值:成功返回0;失败返回-1

semop函数:

功能:⽤用来创建和访问⼀一个信号量集
原型
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数
semid:是该信号量的标识码,也就是semget函数的返回值
sops:是个指向⼀一个结构数值的指针
nsops:信号量的个数
返回值:成功返回0;失败返回-1

说明:

sembuf结构体:
struct sembuf {
short sem_num;
short sem_op;
short sem_flg;
};
sem_num是信号量的编号。
sem_op是信号量⼀一次PV操作时加减的数值,⼀一般只会⽤用到两个值:
⼀一个是“-1”,也就是P操作,等待信号量变得可⽤用;
另⼀一个是“+1”,也就是我们的V操作,发出信号量已经变得可⽤用
sem_flag的两个取值是IPC_NOWAIT或SEM_UNDO

同消息队列类似,这里也可以使用ipcs -s查看IPC资源,使用ipcrm -s删除IPC资源:


信号量实例:

应用以上函数,编写一段代码:

创建Makefile:

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

comm.h

#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
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*/
};
int createSemSet(int nums);
int initSem(int semid, int nums, int initval);
int getSemSet(int nums);
int P(int semid, int who);
int V(int semid, int who);
int destorySemSet(int semid);
#endif

comm.c

#include "comm.h"
static int commSemSet(int nums, int flags)
{
    key_t key = ftok("/tmp", 0x6666);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    int semid = semget(key, nums, flags);
    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 -1;
    }
    return 0;
}
static int commPV(int semid, int who, int op)
{
    struct sembuf _sf;
    _sf.sem_num = who;
    _sf.sem_op = op;
    _sf.sem_flg = 0;
    if(semop(semid, &_sf, 1) < 0)
    {
        perror("semop");
        return -1;
    }
    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 destorySemSet(int semid)
{
    if(semctl(semid, 0, IPC_RMID) < 0)
    {
        perror("semctl");
        return -1;
    }
    return 0;
}

test_sem.c

#include "comm.h"
int main()
{
    int semid = createSemSet(1);
    initSemSet(semid, 0, 1);
    pid_t pid = fork();
    if(pid == 0)
    {
        //child
        int _semid = getSemSet(0);
        while(1)
        {
            P(_semid, 0);
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A ");
            fflush(stdout);
            usleep(123456);
            V(_semid, 0);
        }
    }
    else
    {
        //parent
        while(1)
        {
            P(semid, 0);
            printf("B");
            fflush(stdout);
            usleep(123456);
            printf("B ");
            fflush(stdout);
            usleep(123456);
            V(semid, 0);
        }
        wait(NULL);
    }
    destorySemSet(semid);
    return 0;
}

未使用PV操作时结果:


加入PV操作时的结果:


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


猜你喜欢

转载自blog.csdn.net/yaotengjian/article/details/80243543