在介绍信号量&同步与互斥之前,我们先来了解一下生产者与消费者模型。

互斥量和信号量和消息队列的使用场景_信号量

生产者将数据放入缓冲区,消费者将数据从缓冲区取走消费。

生产者消费者的模型提出了三种关系,两种角色,一个场所

三种关系:
- 生产者之间的互斥关系
- 消费者之间的竞互斥关系
- 生产者和消费者之间互斥和同步关系(同一时刻只能有一个,要么在生产,要么在消费,这就是互斥关系,只能在生产者生产完了之后才能消费,这就是同步关系)

两个角色:一般是用进程或线程来承担生产者或消费者

一个场所:有效的内存区域。(如单链表,数组)
我们就可以把这个想象成生活中的超市供货商,超市,顾客的关系,超市供货商供货,超市是摆放货物的场所,然后用户就是消费的。


信号量主要用于同步和互斥,那么什么是同步与互斥呢?
进程互斥:

  • 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系称为互斥。
  • 系统中的某些资源一次只允许一个进程使用,这样的资源称为临界资源或者互斥资源。
  • 进程中涉及到互斥资源的程序段叫临界区。

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

司机 p1                               售票员 p2
while(1)                            while(1)
{                                   {
    启动车辆;                            关门;
    正常运行;                            售票;
    到站停车;                            开门;
}                                   }
信号量和P、V原语
  • 信号量和p、v原语由大佬Dijkstra(迪杰斯特拉)提出
  • 信号量

互斥:P、V在同一进程中
同步:P、V在不同进程中

  • 信号量值的含义

s > 0: s表示可用资源的个数
s = 0: 表示无可用资源,无等待进程
s < 0: |s|表示等待队列中进程个数

信号量结构体的伪代码
信号量本质上是一个计数器
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中等待的一个进程
        改变其状态为就绪态
        将其插入就绪队列    
    }
}

简单的介绍之后,就让我们来用代码测试一下同步与互斥吧!

这里,我们按照二元信号量来测试。
comm.h

#pragma once

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<unistd.h>
#include<sys/wait.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 initSem(int semid, int nums, int initVal);
int getSemSet(int nums);
int P(int semid,int who);
int V(int semid,int who);
int destroySemSet(int semid);

comm.c

#include"comm.h"

static int commSemSet(int nums,int flags){
    key_t _key = ftok(PATHNAME,PROJ_ID);
    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 initSem(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 destroySemSet(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);
    initSem(semid, 0, 1);
    pid_t id = fork();
    if(id == 0){
        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{
        while(1){
            P(semid, 0);
            printf("B");
            fflush(stdout);
            usleep(223456);
            printf("B");
            fflush(stdout);
            usleep(12346);
            V(semid, 0);
        }
        wait(NULL);
    }
    destroySemSet(semid);
    return 0;
}

最后奉上Makefile

test_sem:comm.c test_sem.c
    gcc -o $@ $^

.PHONY:clean
clean:
    rm -f test_sem

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

执行效果如下:

互斥量和信号量和消息队列的使用场景_系统编程_02

所有AB都是成对出现,不会出现交叉的情况,那如果去掉PV呢?

互斥量和信号量和消息队列的使用场景_同步与互斥_03


出现打印的内容相互影响

在ctrl + c之后使用ipcs -s 查看信号量,及ipcsrm -s + semid删除信号量
关于ipcs&ipcrm的使用,参照我得博客:ipcs和ipcrm的使用
注意,删除信号量不是必须通过手动删除,这里只是为了演示相关指令,删除IPC资源是进程应该做的事。