文章目录
前言:进程同步篇
前一节学习了管道完成进程间通信(IPC),其主要机制就是使用虚拟文件系统(VFS)的管道文件的半双工的通信。
为什么需要进行进程同步?进程异步不是效率更高吗?
因为牵扯到两个概念
两个概念:
- 临界资源:同一时刻,只允许一个进程或者线程访问的资源(打印机等)
- 临界区:访问临界资源的代码段(p和v操作之间的代码,尽量减少代码量,代码量过多会导致p操作阻塞,影响程序执行效率)
- 原子操作:不能被终止的,一旦开始操作,必须等待操作完成(申请临界资源的操作)
举个栗子(一定要看!!!)
一个试衣间,是不是在某个时刻只能被一个人使用呢?答案是肯定的,此时的一个试衣间就算是临界资源;那你在试衣间干什么?那废话,肯定试衣服了,此时的试衣服的过程就算是临界区,那为什么在试衣间换衣服的过程就是临界区呢?折磨说吧,你(进程a)进入试衣间正在换衣服,秃然之间,有个猥琐大汉(进程b)闯进了你的试衣间,那是不是完蛋了。所以你在使用试衣间(临界资源)之前,肯定要反锁(P操作),当你换衣完成,出来的时候要开锁(V操作),在你换衣服的整个过程中就是访问临界资源的操作(代码操作临界资源)
(一)信号量
信号量是一个特殊的变量,一般取正数值。代表着允许访问资源的数目。
- 获取资源时,需要对信号量原子减一,称为p操作(荷兰语passeren)。
- 释放资源时,需要对信号量原子加一,称为v操作(荷兰语Verhoog)
信号量的内核对象是一个信号量集(信号量的数组)
(二)信号量的作用
作用:主要用于同步进程(进程同步工作)。
- 二进制信号量:信号量的值如果只取0,1;
- 计数信号量:信号量的值大于1;
注意:
当进程获取资源时,信号量为0时,进行p操作进程会阻塞;
(三)信号量的能进程通信的原理
(四)信号量的操作的函数
所需头文件:
sys/types.h
sys/ipc.h
sys/sem.h
(1)创建或者获取已存在的信号量集
int semget(key_t key, int nsems, int semflg)
key
:两个进程使用相同的key值,就可以使用同一个信号量nsems
:内核维护的是一个信号量集,创建信号量集时,会指定信号量集的信号量个数(数组的大小),获取无所谓semflg
:可选IPC_CREAT没有key对应信号量就创建,存在则返回对应的信号量集的ID 、 IPC_EXCL(不管有没有该信号量都返回-1);IPC_CREAT | IP_EXCL没有则创建,有则返回-1返回值
:成功返回信号量集的ID,失败返回-1
注意:创建的信号量不手动销毁,系统内核就会一直帮我们维护下去,除非重启系统;
(2)操作信号量(初始化信号量的值、销毁信号量集)
int semctl(int semid, int semnum, int cmd, .../* union semun val*/ )
- semid:信号量ID
- semnum:信号量集的下标(操作第几个信号量)
- cmd:SETVAL(创建) 、 IPC_RMID(销毁)
- 返回值:成功0,失败-1
union semun联合体的结构体(需要自己声明)(都是4个字节):
union semun { int val; //设置的信号量的值 :临界资源的个数(一个打印机等) struct semid_ds *buf; //IPC_STAT, IPC_SET的缓冲区 unsigned short *array; //GETALL,SETALL的数组 struct seminfo *__buff; //IPC_INFO的缓冲区 };
(3) P V操作函数
int semop(int semid, struct sembuf *sops, size_t nsops);
-semid:信号量集ID
- struct sembuff *sops:指定p/v操作的buff
- size_t nsops :操作信号量的个数
//假设我们定义了一个大小为10的信号量集,现在要对其中的下标5和下标8信号量进行访问,进行PV操作,则需要修改struct sembuf中的元素; - 返回值:
第二个参数类型:(该类型已经存在sys/sem.h头文件中)
struct sembuf
{
unsigned short sem_num; /* semaphore number */ 5 / 8
short sem_op; /* semaphore operation */ -1是P操作 +1是V操作
short sem_flg; /* operation flags */IPC_NOWAIT非阻塞、SEM_UNDO阻塞
}
(五)信号量的具体操作
(1)void sem_init();
函数功能:创建/获取信号量集,初始化信号量的值
(2)void sem_p();
函数功能:实现对临界资源的P(-1)操作
(3)void sem_v();
函数功能:实现对临界资源的V(+1)操作
(4)void sem_destroy();
函数功能:销毁创建的信号量集(只销毁一次)
(六)一个进程同步的栗子
(1)未同步之前
jiege的代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
for(int i = 0; i < 3; i++)
{
//用j表示jiege在换衣服
write(1, "j", 1);
int num = rand() % 3;
sleep(num);
write(1, "j", 1);
num = rand() % 3;
sleep(num);
}
return 0;
}
awei的代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
for(int i = 0; i < 3; i++)
{
//用a表示阿伟在换衣服
write(1, "a", 1);
int num = rand() % 3;
sleep(num);
write(1, "a", 1);
num = rand() % 3;
sleep(num);
}
return 0;
}
结果:可以看到杰哥和阿伟有时候共用一个试衣间。。。显然需要进程同步。
(2)同步之后
显然,同步后的结果应该是jj 或者aa成对出现,也就是当杰哥或者阿伟用完试衣间后,另一个人再进去换衣服;
- sem.h文件
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/sem.h>
//union semun共用体
union semun
{
int val; //信号量的值
};
//创建(获取)信号量集并初始化信号集中的信号量的值
void sem_init();
//p操作
void sem_p();
//v操作
void sem_v();
//销毁信号量集
void sem_destroy();
- sem.c文件
#include "sem.h"
//信号量集的ID
static int semid = -1;
//创建(获取)信号量集并初始化信号集中的信号量的值
void sem_init()
{
//创建信号集
semid = semget((key_t)8888, 1, IPC_CREAT | IPC_EXCL | 0600);
//若创建失败则说明存在该信号量集
if(semid == -1)
{
//此时第二、三个参数起到占位作用
//该信号量集存在,获取即可
semid = semget((key_t)8888, 1, IPC_CREAT);
if(semid == -1)
{
perror("semget create err");
return;
}
}
else
{
//初始化信号量的val值
//定义union semun的变量保存信号量的值(一种临界资源的个数)
union semun tmp;
tmp.val = 1;
//第二参数是信号量集数组下标
if(semctl(semid, 0, SETVAL, tmp) == -1)
{
perror("semctl初始化失败");
return;
}
}
}
//p操作
void sem_p()
{
struct sembuf buf;
buf.sem_num = 0; //下标
buf.sem_op = -1; //P操作
buf.sem_flg = SEM_UNDO; //阻塞
//第三个参数信号量的个数
if(semop(semid, &buf, 1) == -1)
{
perror("semop p err");
return;
}
}
//v操作
void sem_v()
{
struct sembuf buf;
buf.sem_num = 0; //下标
buf.sem_op = 1; //v操作
buf.sem_flg = SEM_UNDO; //阻塞
if(semop(semid, &buf, 1) == -1)
{
perror("semop v err");
return;
}
}
//销毁信号量集
void sem_destroy()
{
//IPC_RMID是删除信号量集的ID
//第二个参数起到占位作用
if(semctl(semid, 0, IPC_RMID) == -1)
{
perror("destroy err");
}
}
- jiege.c文件
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "sem.h"
int main()
{
//新建信号量集
sem_init();
for(int i = 0; i < 3; i++)
{
//p操作
sem_p();
//用j表示jiege在换衣服
write(1, "j", 1);
int num = rand() % 3;
sleep(num);
write(1, "j", 1);
//v操作
sem_v();
num = rand() % 3;
sleep(num);
}
//销毁信号量集
sleep(2); //确保jiege后退出销毁一次即可
sem_destroy();
return 0;
}
- awei.c文件
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "sem.h"
int main()
{
sem_init();
for(int i = 0; i < 3; i++)
{
sem_p();
//用a表示阿伟在换衣服
write(1, "a", 1);
int num = rand() % 3;
sleep(num);
write(1, "a", 1);
sem_v();
num = rand() % 3;
sleep(num);
}
return 0;
}
使用信号量进行同步的结果:正确输出是jj或aa, 解决了jaja交叉出现的结果
- 结果: