进程间通信-信号量

时间:2022-09-16 15:13:11


信号量实质上是一个用来描述临界资源数目的计数器。本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。

当进程不再使用一个信号量控制的共享资源时,信号量的值+1,对信号量的值进的增减操作均为原子是操作,这是由于信号量主要的作是维护资源的互斥或多进程的同步访问。在信号量的创建及初始化上,不能保证操作均为原性。

为什么要用到信号量呢?

当一个临界资源同时被多个进程访问时容易出现错误,那么就必须有一种机制来让代码的临界区域只能有一个线程来访问。临界区域是指执行数据更新的代码需要独占式地执行。也就是说信号量实际上是来协调临界资源被进程访问的。其中进程间通行的另一种方式共享内存就要使用采用信号量。

信号量常见的操作有P()操作和V()操作。

P(sv):如果sv的值等于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1. 


举例说明:一个教室一次只能有一个人学习。当几个人同时想进教室学习,但是最多只能进一个,这时管理教室的人就会让最先到达的人进教室学习,然后教室的信息(容量)由1变为0;后面的人依次排队,等第一个人出来之后信息由0变为1;这里的进教室就相当于P()操作,出教室相当于V()操作,而等待的人相当于进程挂起!


下来看看如何创建一个信号量,这里用到的函数是semget();先来看看他的参数

进程间通信-信号量

key值和前面的消息队列key一样,是一个唯一的标识,第二个参数是第多少个信号量,第三个参数和管道的格式是一个IPC_CREAT和IPC_EXCL。


信号量的销毁函数为semctl();各个参数如下

进程间通信-信号量

其中semun是一个联合体对象,调用的时候在头文件里声明一下。


调用PV所用到的函数是semop函数,先来看看semop参数

进程间通信-信号量

第一个参数就是要操作的信息量ID,第二个参数是一个结构体的地址,第三个第四个分别是结构体里的元素。

这里如果要进行P操作,实参传的就是-1,如果是V操作参数传的就是1;

具体的在代码里看(采用接口封装方式)

comm.h

#ifndef _COMM_H_
#define _COMM_H_

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>



#define PATHNAME "."
#define PROJ_ID 0x6666

union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};

int initSemset(int semid, int num, int val);
int creatSemset(int nums);
int getSemset(int nums);
int destroySemset(int semid);
int P(int semid, int nums);
int V(int semid, int nums);

#endif

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 initSemset(int semid, int num, int val){
union semun _un;
_un.val = val;
if(semctl(semid, num, SETVAL, _un) < 0){
perror("semstl");
return -1;
}
return 0;
}



int creatSemset(int nums){
return commSemset(nums, IPC_CREAT|IPC_EXCL);
}


int getSemset(int nums){
return commSemset(nums, IPC_CREAT);
}

int destroySemset(int semid){
if(semctl(semid, 0, IPC_RMID) < 0){
perror("semctl");
return -1;
}
return 0;
}

static int commPV(int semid ,int nums, int op){
struct sembuf _sf;
_sf.sem_num = nums;
_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 nums){
return commPV(semid, nums, -1);
}

int V(int semid, int nums){
return commPV(semid, nums, 1);
}


如果要是不用信号量直接想通过父子进程直接往标准输出里输出成对的A和成对的B,那么会出现什么现象呢?

进程间通信-信号量
可以看到打印出来的A和B是随机输出的,那是因为两个进程防伪同一块临界区域时,先后次序是内核随机读取的,所以会随机的输出。当采用信号量来加以约束时,会将临界区域保护起来,等一个进程访问完后,另一个进程再进行访问!将会出现A和B成对的出现!即信号量满足原子性!


test_sem.c

#include "comm.h"

int main()
{
int semid = creatSemset(12);
initSemset(semid, 0, 1);

if(fork() == 0){
int _semid = getSemset(0);
while(1){
P(_semid, 0);
printf("A");
fflush(stdout);

usleep(124556);
printf("A ");
fflush(stdout);
usleep(545);
V(_semid, 0);
}
}
else{
while(1){
P(semid, 0);
printf("B");
fflush(stdout);
usleep(124556);
printf("B ");
fflush(stdout);
usleep(345);
V(semid, 0);
}
}
return 0;
}

进程间通信-信号量


当采用了信号量的P操作后,相当于已经有一个进程正在访问它了,但是如果正在访问临界区域的这个进程意外死掉之后,信号量只进行了-1操作,没有+1,。那么这块临界区域相当于被锁死了,打开临界区域的钥匙已经随进程的死亡而消失了,这时候就会造成临界区域被永久锁死的现象,以后这块区域不会再被访问到了!

 

为了防止这种问题的出现,semop函数会指定一个标志sem_undo,每个内核对IPC信号量资源所执行的可撤销操作,都存放在一个叫sem_undo的数据结构中,比如上个例子中semop()做的+1,-1这两个操作,在进程结束时,内核就做了一个sem_undo的数据结构中记录的数值的反向操作,来达到IPC信号量计数器回滚的目的。


IPC_UNDO标志保证进程终止后,它对信号量的修改都撤销,好像它从来没有操作过信号量一样。