Linux/Unix系统IPC是各种进程间通信方式的统称,但是其中极少能在所有Linux/Unix系统实现中进行移植。随着POSIX和Open Group(X/Open)标准化的推进呵护影响的扩大,情况虽已得到改善,但差别仍然存在。一般来说,Linux/Unix常见的进程间通信方式有:管道、消息队列、信号、信号量、共享内存、套接字等。博主将在《进程间通信方式总结》系列博文中和大家一起探讨学习进程间通信的方式,并对其进行总结,让我们共同度过这段学习的美好时光。这里我们就以其中一种方式信号量展开探讨,为了防止出现因多个程序同时访问同一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。下面让我们一起来看看信号量的操作函数吧!
函数原型:int semget(key_t key, int num_sems, int sem_flags);
key:当key值为0(IPC_PRIVATE),会建立新信号量集对象;当值为大于0的32位整数,视参数flags来确定操作。不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
num_sems:指定需要的信号量数目,它的值几乎总是1。如果num_sems大于0,则表示一个信号集合(数组)。
sem_flags:是一组标志,当想要在信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。sem_flags的设置和open系统调用的设置类似,也可以用八进制表示法。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT| IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
函数原型:int semctl(int sem_id, int sem_num, int command, .../*unionsemun arg*/);
sem_id:信号标识符,semget函数的返回值。
sem_num:信号数组下标,其值几乎总是0。
command:控制命令
命令
解 释
IPC_STAT
从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中
IPC_SET
设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值
IPC_RMID
从内核中删除信号量集合
GETALL
从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体成员的一个array数组中
GETNCNT
返回当前等待资源的进程个数
GETPID
返回最后一个执行系统调用semop()进程的PID
GETVAL
返回信号量集合内单个信号量的值
GETZCNT
返回当前等待100%资源利用的进程个数
SETALL
与GETALL正好相反
SETVAL
用联合体中val成员的值设置信号量集合中单个信号量的值
arg:semun结构体
union semun{
int val; //单个信号量的值(在SETVAL和GETVAL时使用)
struct semid_ds *buf;
unsigned short *arry; //信号量值数组
};
函数原型:int semop(int semid, struct sembuf *sops, unsigned nsops)
semid:信号标识符,semget函数的返回值。
sops:sembuf结构体数组
struct sembuf {
shortsemnum;
short val;
short flag;
};
semnum:信号量集合中的信号量编号,0代表第1个信号量(即信号数组下标)
val:若val>0进行V操作信号量值加val,表示进程释放控制的资源。若val<0进行P操作信号量值减val,若(semval-val)<0(semval为该信号量值),则调用进程阻塞,直到资源可用;若设置IPC_NOWAIT不会睡眠,进程直接返回EAGAIN错误。若val==0时阻塞等待信号量为0,调用进程进入睡眠状态,直到信号值为0;若设置IPC_NOWAIT,进程不会睡眠,直接返回EAGAIN错误。
flag:0设置信号量的默认操作。IPC_NOWAIT设置信号量操作不等待。SEM_UNDO选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值。若flag包含SEM_UNDO,则当进程退出的时候会还原该进程的信号量操作,这个标志在某些情况下是很有用的,比如某进程做了P操作得到资源,但还没来得及做V操作时就异常退出了,此时,其他进程就只能都阻塞在P操作上,于是造成了死锁。若采取SEM_UNDO标志,就可以避免因为进程异常退出而造成的死锁。
nsops:进行操作的信号量个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作。
看了一些函数的介绍,没有使用过信号量的小伙伴可能一头雾水,下面就我们一起来拨开迷雾,领悟信号量如何实现对多个进程访问临界资源的同步作用。哈哈,是不是很拗口啊,下面我们来看一个博主写的栗子吧,博主将共享内存作为临界资源,通过信号量的P、V操作来控制多个进程对临界资源的同步访问。
#ifndef SEMPV_H_
#define SEMPV_H_
#include
#include
#include
//semun联合体,在设置信号量值是使用
typedef union semun
{
int val;//设置或获取单个信号量的值
struct semid_ds *buf;//IPC_STAT(读取信号量的semid_ds结构体数据存于buf中)和IPC_SET(根据buf设置信号量的semid_ds结构体)缓存
unsigned short *array;//设置或获取信号量集合(数组)所有信号量值
struct seminfo *__buf;//IPC_INFO缓存
}semun_t;
//C++编译器执行
#ifdef __cplusplus
extern "C"
{
#endif
//P操作
int P(int semid, int semnum);
//V操作
int V(int semid, int semnum);
//C++编译器执行
#ifdef __cplusplus
}
#endif
#endif
#include "sempv.h"
/*
*
*实现信号量的P、V操作
*
*
*/
//P操作
int P(int semid, int semnum)
{
//sembuf结构体
//成员1:信号量数组下标
//成员2:操作(P:使信号量值减小)
//参数3:flag标识(SEM_UNDO:当程序异常终止时,避免产生死锁)
struct sembuf sops = {semnum, -1, SEM_UNDO};
//semop完成对信号量的P、V操作
//参数1:信号量标识,semget返回值
//参数2:sembuf结构体指针
//参数3:操作的信号量数量
return semop(semid, &sops, 1);
}
//V操作
int V(int semid, int semnum)
{
//sembuf结构体
//成员1:信号量数组下标
//成员2:操作(V使信号量值增加)
//参数3:flag标识(SEM_UNDO:当程序异常终止时,避免产生死锁)
struct sembuf sops = {semnum, 1, SEM_UNDO};
//semop完成对信号量的P、V操作
//参数1:信号量标识,semget返回值
//参数2:sembuf结构体指针
//参数3:操作的信号量数量
return semop(semid, &sops, 1);
}
/*
*
*通过信号量实现多个进程对共享内存(临界资源)的同步访问
*
*
*/
#include "sempv.h"
#include
#include
#include
//key的子序号
#define IPC_KEY 0x12
//定义共享内存数据结构体
typedef struct share
{
int n_ID;
char szName[64];
}share_t;
//定义信号量标识
int semid = -1;
//定义共享内存标识
int shmid = -1;
//共享内存连接到进程中返回的共享内存地址
share_t *shmaddr = NULL;
//SIGINT信号处理函数
void sigint(int sig)
{
//卸载共享内存
shmdt(shmaddr);
//删除信号量
semctl(semid, 0, IPC_RMID);
//删除共享内存
shmctl(shmid, IPC_RMID, NULL);
exit(EXIT_SUCCESS);
}
//主函数
int main(int argc, char **argv)
{
//检查命令行参数
if (argc < 2 || ('R' != argv[1][0] && 'W' != argv[1][0]))
{
fprintf(stdout, "Usage %s R|W\n", argv[0]);
exit(EXIT_FAILURE);
}
//注册中断信号(当进程接收到SIGINT信号(如:【strl+c】),将调用信号处理函数sigint)
//signal返回旧的信号处理函数
__sighandler_t ret = signal(SIGINT, sigint);
if (SIG_ERR == ret) perror("signal"), exit(EXIT_FAILURE);
//根据目录创建key_t
char path[256] = {0};
sprintf(path, "%s", getenv("HOME"));//getenv读取环境变量的值
//根据文件inode号和子序号生成key_t
//如果path的inode号16进制表示:0x450620,那么key为0x12450620(IPC_KEY:0x12)
key_t key = ftok(path, IPC_KEY);
/***************创建并初始化信号量*****************/
//创建信号量
//参数1:key_t,用于semget区分不同信号量
//参数2:信号量数量
//参数3:flag(此处为根据key创建信号量,权限为0644)
//内存创建者的进程对该信号量的权限为6;
//内存创建者所在用户组里的其他进程对该信号量的权限为4;
//其他进程对该信号量的权限为4
semid = semget(key, 1, IPC_CREAT | 0644);
//注意此处为逗号表达式,不要将","误写成";"
if (-1 == semid) perror("semget"), exit(EXIT_FAILURE);
//设置semun结构体
semun_t arg;
arg.val = 1;
//由写共享内存进程初始化信号量的值
if ('W' == argv[1][0])
{
//初始化信号量
//参数1:信号量标识
//参数2:信号量数组下标
//参数3:控制命令(此处设置信号量的值)
//参数4:semun结构体(存有待设置的信号量的值)
if (-1 == semctl(semid, 0, SETVAL, arg)) perror("semctl"),exit(EXIT_FAILURE);
}
/****************创建并连接共享内存*********************/
//参数1:key_t,用于shmget区分不同共享内存
//参数2:共享内存所占字节数
//参数3:flag(此处根据key创建共享内存,权限为0644)
//内存创建者的进程对该内存的权限为6;
//内存创建者所在用户组里的其他进程对该内存的权限为4;
//其他进程对该内存的权限为4
shmid = shmget(key, sizeof(share_t), IPC_CREAT | 0644);
//将共享内存连接到当前进程
shmaddr = (share_t*)shmat(shmid, 0, 0);
//写共享内存
if ('W' == argv[1][0])
{
while (1)
{
//执行P操作
P(semid, 0);
fprintf(stdout, "*********写共享内存*********\n");
fprintf(stdout, "请输入用户ID: ");
scanf("%d", &shmaddr->n_ID);
scanf("%*c");
fprintf(stdout, "请输入用户名: ");
scanf("%[^\n]%*c", shmaddr->szName);
//执行V操作
V(semid, 0);
sleep(3);//进程休眠3S
//pause();//将当前进程挂起
}
}
else if ('R' == argv[1][0])//读共享内存
{
while (1)
{
//执行P操作
P(semid, 0);
fprintf(stdout, "*********读共享内存*********\n");
fprintf(stdout, "用户ID: %d\n", shmaddr->n_ID);
fprintf(stdout, "请输入用户名: %s\n", shmaddr->szName);
//执行V操作
V(semid, 0);
sleep(3);
//pause();
}
}
return 0;
}
程序运行结果:
在程序中,通过命令行参数区分读写逻辑,写进程初始化信号量(信号量的值初始化为1),同一个键对应的共享内存和信号量只在第一次调用shmget和semget时创建。不论是读进程还是写进程,在进入各自的代码逻辑后,通过P、V操作使读写进程对临界资源的操作是原子操作,同一时间只有一个进程操作共享内存。在代码逻辑中,博主让进程休眠3S,避免CPU使用率过高。在程序中,读写进程谁先抢占到信号量谁就可以使用临界资源,即获得共享内存的使用权。本篇博文主要是总结信号量的使用方法,共享内存只是简单的应用一下,后期博主会对其进行单独总结。可能有的小伙伴会问:“信号量主要是实现进程对临界资源的同步访问,和进程间通信有什么关系啊?”。不同进程在通过唯一的semid访问信号量,并对信号量的值进行设置,着就是在通信,不同的进程在访问一个共享资源信号量,并修改其值。
现在是不是对信号量有一定的了解啦,博主更希望你已经理解并掌握了信号量的使用方法,可能你第一次接触P、V操作是在操作系统课程,并使用它写了一些伪代码,那么现在你不用再写伪代码了,可以实际的写一个程序去体验一番了。感兴趣的小伙伴可以对博主的栗子进行改进:当有进程写共享内存时,进程独享该临界资源,避免交叉写入和读取脏数据;当只有进程在读共享内存时,其他读进程也可以访问该临界资源,而写进程阻塞。
关于信号量的学习我们就到此结束了,相信大家都有所收获,希望小伙伴们都已经理解并掌握了信号量的常用方法。如果你觉得对进程间通信的方式不胜了解,还有些许疑惑,请关注博主《进程间通信方式总结》系列博文,相信你在那里能找到答案。