之前我们学习了管道,消息队列,共享内存,今天我们再来学一种进程间通信的方式-----信号量
信号量
信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
在了解信号量之前,我们先来看几个概念
临界资源:两个进程看到的同一个公共的资源,但是同时只能被一个进程所使用的的资源叫做临界资源(互斥资源)
临界区:在晋城中涉及到互斥资源的程序段叫临界区
信号量主要用于同步和互斥,下面我们来看看什么是同步和互斥。
互斥:各个进程都要访问共享资源,但共享资源是互斥的,同时只能有一个进程使用。因此,各个进程之间竞争使用这些资源,将这种关系称为互斥。
同步:多个进程需要相互配合共同完成一项任务。
信号量的工作机制
简单说一下信号量的工作机制,可以直接理解成计数器,信号量会有初值(>0),每当有进程申请使用信号量,通过一个P操作来对信号量进行-1操作,当计数器减到0的时候就说明没有资源了,其他进程要想访问就必须等待,当该进程执行完这段工作(我们称之为临界区)之后,就会执行V操作来对信号量进行+1操作。
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模式标志是⼀样的
IPC_CREAT|IPC_EXCL:不存在创建,存在出错返回
IPC_CREAT:不存在创建,存在返回
设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
返回值:成功返回⼀个⾮负整数,即该信号集的标识码;失败返回-1
shmctl函数
功能:⽤用于控制信号量集
原型
int semctl(int semid, int semnum, int cmd, ...);
参数
semid:由semget返回的信号集标识码
semnum:信号集中信号量的序号
cmd:将要采取的动作(有三个可取值)最后一个参数根据命令不同⽽不同
返回值:成功返回0;失败返回-1
命令 | 说明 |
SETVAL | 设置信号量集中的信号量的计数值 |
CETVAL | 获取信号量集中的信号量的计数值 |
IPC_STAT | 把semid_ds结构中的数据设置为信号集的当前关联值 |
IPC_SET | 在进程由足够权限的前提下,把信号集的当前关联值设置为semid_ds数据结构中给出的值 |
IPC_RMID | 删除信号集 |
功能:⽤来创建和访问⼀个信号量集
原型int semop(int semid, struct sembuf *sops, unsigned nsops);
参数
semid:是该信号量的标识码,也就是semget函数的返回值
sops:是个指向⼀个结构数值的指针
nsops:信号量的个数
返回值:成功返回0;失败返回-1
sembuf定义如下:
struct sembuf{
short sem_num; //除非使用一组信号量,否则它为0
short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数
//一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
信号量操作步骤:
一、创建或获取一个信号量,调用semget()函数。
二、初始化信号量,调用semctl()。
三、进行信号量的PV操作,调用semop()函数。
四、信号退出时,从系统中删除该信号,调用semctl(),IPC_RMID操作。
接下来我们用信号量来实现两个进程间的通信:
代码如下:
comm.h
#ifndef __COMM_H__
#define __COMM_H__
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <error.h>
#define PATHNAME "."
#define PROJ_ID 0
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
struct seminfo *__buf;
};
//信号量是创建还是获取在于semget函数参数flag的设置
static int CommSemid(int nums, int flags);
//创建信号量
int CreatSemid(int nums);
//获取已经创建的信号量
int GetSemid(int nums);
//初始化信号量
int InitSem(int semid, int which, int _val);
//PV操作在于它_op的值
static int SemPV(int semid, int which, int _op);
//P操作
int P(int semid, int which, int _op);
//V操作
int V(int semid, int which, int _op);
//由于(System V通信方式)信号量生命周期随内核,所以要销毁信号量
int Destory(int semid);
#endif
comm.c
#include "comm.h"
static int CommSemid(int nums, int flags)
{
key_t _key = ftok(PATHNAME, PROJ_ID);
if (_key>0)
{
return semget(_key, nums, flags);
}
else
{
perror("CommSemid");
return -1;
}
}
int CreatSemid(int nums)
{
return CommSemid(nums, IPC_CREAT | IPC_EXCL | 0666);
}
int GetSemid(int nums)
{
return CommSemid(nums, IPC_CREAT);
}
int Destory(int semid)
{
if (semctl(semid, 0, IPC_RMID)>0)
{
return 0;
}
else
{
perror("Destory");
return -1;
}
}
int InitSem(int semid, int which, int _val)
{
union semun _semun;
_semun.val = _val;
if (semctl(semid, which, SETVAL, _semun)<0)
{
perror("InitSem");
return -1;
}
return 0;
}
static int SemPV(int semid, int which, int _op)
{
struct sembuf _sf;
_sf.sem_num = which;
_sf.sem_op = _op;
_sf.sem_flg = 0;
return semop(semid, &_sf, 1);
}
int P(int semid, int which, int _op)
{
if (SemPV(semid, which, _op)<0)
{
perror("P");
return -1;
}
return 0;
}
int V(int semid, int which, int _op)
{
if (SemPV(semid, which, _op)<0)
{
perror("V");
return -1;
}
return 0;
}
test_sem.c
#include "comm.h"
int main()
{
int semid = CreatSemid(1);
printf("%d\n", semid);
InitSem(semid, 0, 1);
pid_t id = fork();
if (id == 0)
{//child
int semid = GetSemid(0);
while (1)
{
//P(semid, 0, -1);
printf("A");
fflush(stdout);
usleep(100000);
printf("A ");
fflush(stdout);
usleep(200000);
//V(semid, 0, 1);
}
}
else
{//father
while (1)
{
//P(semid, 0, -1);
usleep(300000);
printf("B");
fflush(stdout);
usleep(400000);
printf("B ");
fflush(stdout);
usleep(20000);
// V(semid, 0, 1);
}
if (waitpid(id, NULL, 0) < 0)
{
perror("waitpid");
return -1;
}
}
Destory(semid);
return 0;
}
Makefile
test_sem:comm.c test_sem.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f test_sem
此时显示器只有一个,两个进程同时打印,此时显示器成为临界资源,使用二元信号量(互斥锁)进行保护。
我们在运行结果前,可以使用 ipcs -s 命令查看信号进行进程间通信的信息。
可以看出,此时并没有信息。
我们现在来运行代码,结果如下:
我们可以看到所有的AB都是成对出现的,不会出现交叉的情况。这是因为P、V操作实现过程中具有原子性,能够实现对临界区
的管理,它的执行是不会受其他进程的影响。
但是如果我们不加P、V操作,那么会出现什么样的情况呢??我们来看一下:
代码修改后,运行结果如下:
我们可以发现,屏幕上输出的AB出现交叉,并没有成对打印。