Linux进程间通信之信号量篇

  信号量不以传输数据为目的,其本质是计数器+等待队列,本身不具有数据交换的功能,而是通过控制其他的通信资源(例如文件、外设等)来实现进程间通信,只是一种外部资源的标志。信号量在此过程中负责数据操作的互斥、同步等功能。其建立和初始化的过程不能保证均是基于原子层面的操作。为了防⽌出现因多个程序同时访问⼀个共享资源⽽引发的⼀系列问题,我们采用信号量的同步、互斥机制来解决此类问题。

  二元信号量:是最简单的一种锁,适合只能被唯一一个线程独占访问的资源;对于允许多个线程并发访问的资源,多元信号量简称信号量。

进程互斥

  • 多进程环境下,若一个进程已进入临界区访问临界资源时,其他进程不得进入临界区。该进程独自享有临界区全部临界资源,对于其他进程具有强烈的排斥性。这种只允许一个执行流访问临界资源的情形称为互斥。
  • 临界资源:不同进程访问的同一资源称为临界资源。
  • 临界区:两个或多个进程访问临界资源的代码称为临界区。

如:ATM自助取款机,每次只允许一人访问

进程同步

  • 在多执行流访问临界资源时,对某一执行流频繁访问临界资源而长时间享有独占性时,对于较之不频繁访问临界资源的执行流来说要等待的时间过久,所以互斥虽能保证各进程进入临界区访问临界资源时的数据正确性,但却并非最高效的方法。所以在互斥基础上,访问临界区的临界资源时各进程产生一定的访问顺序,这种顺序性称为同步。
  • 进程同步指的是多个进程需要相互配合共同完成⼀项任务。

如:蓝球运动,需要各个队员相互协作

信号量和P、V原语

信号量和P、V原语由Dijkstra(迪杰斯特拉)提出
PV的含义:

  • P(Proberen)表示申请资源,信号量的数值减一(剩余的可用资源数目减少一)
  • V(Verhogen)表示释放资源,信号量的数值加一(剩余的可用资源数目增加一)

记忆技巧:资源要先申请,才能释放,因此通常说PV操作。P在前,表示申请资源,V在后,表示释放资源

信号量的含义:

  • S>0:S表⽰可⽤资源的个数
  • S=0:表⽰⽆可⽤资源,⽆等待进程
  • S<0:|S|表⽰等待队列中进程个数

信号量既可以用于同步,也可以用于互斥

  • 互斥:P、V在同⼀个进程中
  • 同步:P、V在不同进程中

信号量结构体伪代码

信号量本质上是⼀个计数器+等待队列

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中等待的⼀个进程改变其状态为就绪态
        并将其插⼊就绪队列
    }
}

信号量集函数

1、semget函数

功能:⽤来创建和访问⼀个信号量集
原型:int semget(key_t key, int nsems, int semflg);
参数:
  key:信号集的名字
  nsems:信号集中信号量的个数
  semflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
返回值:成功返回⼀个⾮负整数,即该信号集的标识码;失败返回-1

2、semctl函数

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

描述

3、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

信号量实现进程间通信

common.c

#include "common.h"
#include <stdio.h>

int SemCommon(int nums, int flags){
    key_t key = ftok(PATHNAME, PROJ_ID);
    if(key < 0){
        perror("ftok error");
        return -1;
    }
    int semid = semget(key, nums, flags);
    if(semid < 0){
        perror("semget error");
        return -1;
    }
    return semid;
}

int SemCreate(int nums){
    return SemCommon(nums, IPC_CREAT | IPC_EXCL | 0666);
}

int SemOpen(){
    return SemCommon(0, IPC_CREAT);
}

int SemDestroy(int semid){
    int ret = semctl(semid, 0, IPC_RMID);
    if(ret < 0){
        perror("semctl error");
        return -1;
    }
    return 0;
}

void SemSetValue(int semid, int index, int value){
    union semun _semun;
    _semun.val = value;
    int ret = semctl(semid, index, SETVAL, _semun);
    if(ret < 0){
        perror("semctl SETVAL");
        return;
    }
}

void SemGetValue(int semid, int index, int* value){
    int ret = semctl(semid, index, GETVAL);
    if(ret < 0){
        perror("semctl GETVAL");
        return;
    }
    *value = ret;
}

int CommonPV(int semid, int index, int op){
    struct sembuf _sembuf;
    _sembuf.sem_num = index;
    _sembuf.sem_op = op;
    _sembuf.sem_flg = 0;
    int ret = semop(semid, &_sembuf, 1);
    if(ret < 0){
        perror("semop error");
        return -1;
    }
    return 0;
}

void P(int semid, int index){
    CommonPV(semid, index, -1);
}

void V(int semid, int index){
    CommonPV(semid, index, 1);
}
//////////////////////////////////////////////////////////////////
//以下是测试函数
///////////////////////////////////////////////////////////////////
#if 0
void TestCreate(){
    int semid = SemCreate(1);
    printf("semid = %d\n", semid);
}

void TestOpen(){
    int semid = SemOpen();
    printf("semid = %d\n", semid);
}

void TestDestroy(){
    int semid = SemOpen();
    int ret = SemDestroy(semid);
    printf("ret expect 0, actual %d\n", ret);
}

void TestSetGet(){
    int semid = SemCreate(1);
    SemSetValue(semid, 0, 1);
    int value = 0;
    SemGetValue(semid, 0, &value);
    printf("value expect 1, actual %d\n", value);
    SemDestroy(semid);
}

void TestPV(){
    int semid = SemCreate(1);
    SemSetValue(semid, 0, 1);
    P(semid, 0);
    int value = -1;
    SemGetValue(semid, 0, &value);
    printf("value expect 0, actual %d\n", value);
    V(semid, 0);
    SemGetValue(semid, 0, &value);
    printf("value expect 1, actual %d\n", value);
    SemDestroy(semid);
}

int main(){
    //TestCreate();
    //TestOpen();
    //TestDestroy();
    //TestSetGet();
    TestPV();
    return 0;
}
#endif

创建之后:
描述

销毁之后:
描述

PV操作:
描述

sem.c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include "common.h"

int main(){
    int semid = SemCreate(1);
    SemSetValue(semid, 0, 1);
    int ret = fork();
    if(ret == 0){
        //child
        while(1){
            P(semid, 0);      //P操作
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A ");
            fflush(stdout);
            usleep(323456);
            V(semid, 0);      //V操作
        }
    }
    else if(ret > 0){
        //father
        while(1){
            P(semid, 0);         //P操作
            printf("B");
            fflush(stdout);
            usleep(223456);
            printf("B ");
            fflush(stdout);
            usleep(121456);
            V(semid, 0);         //V操作
        }
        wait(NULL);
    }
    SemDestroy(semid);
    return 0;
}

内部实现原理:
描述

若没有PV操作,效果如下:
描述

若有PV操作,效果如下:
描述

可以看出没有PV操作时,AB并不是成对出现的;而加上PV操作后AB成对出现,符合预期。

详细代码请参考Git

猜你喜欢

转载自blog.csdn.net/adorable_/article/details/80473551