在介绍信号量&同步与互斥之前,我们先来了解一下生产者与消费者模型。
生产者将数据放入缓冲区,消费者将数据从缓冲区取走消费。
生产者消费者的模型提出了三种关系,两种角色,一个场所
三种关系:
- 生产者之间的互斥关系
- 消费者之间的竞互斥关系
- 生产者和消费者之间互斥和同步关系(同一时刻只能有一个,要么在生产,要么在消费,这就是互斥关系,只能在生产者生产完了之后才能消费,这就是同步关系)
两个角色:一般是用进程或线程来承担生产者或消费者
一个场所:有效的内存区域。(如单链表,数组)
我们就可以把这个想象成生活中的超市供货商,超市,顾客的关系,超市供货商供货,超市是摆放货物的场所,然后用户就是消费的。
信号量主要用于同步和互斥,那么什么是同步与互斥呢?
进程互斥:
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系称为互斥。
- 系统中的某些资源一次只允许一个进程使用,这样的资源称为临界资源或者互斥资源。
- 进程中涉及到互斥资源的程序段叫临界区。
进程同步
进程同步指的是多个进程需要相互配合共同完成一项任务。
司机 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
此时,显示器只有一个,两个进程同时打印,显示器称为临界资源,使用二元信号量(互斥锁)进行保护
执行效果如下:
所有AB都是成对出现,不会出现交叉的情况,那如果去掉PV呢?
出现打印的内容相互影响
在ctrl + c之后使用ipcs -s 查看信号量,及ipcsrm -s + semid删除信号量
关于ipcs&ipcrm的使用,参照我得博客:ipcs和ipcrm的使用
注意,删除信号量不是必须通过手动删除,这里只是为了演示相关指令,删除IPC资源是进程应该做的事。