Linux进程间通信 —— (三) 信号量
一、概述
在多任务操作系统中,多个进程/线程会同时运行。多个任务可能会为了完成某个目标而相互协作,这样就形成了任务之间的同步关系。同样,不同任务之间为了争夺有限的系统资源(硬件/软件资源)会进入竞争状态,这就是任务之间的互斥关系。
任务之间的互斥与同步关系存在的根源在于临界资源。
下面先来看两个概念:临界资源和临界区。
临界资源是指在同一个时刻只允许有限个(通常只有一个)任务访问(读)或修改(写)的资源。
临界资源包括:硬件资源(处理器、内存、存储器、以及其外外围设备等)和软件资源(共享代码段、共享结构和变量等)。
访问临界资源的代码称为临界区。
信号量是用来解决进程/线程之间同步与互斥问题的一种通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(PV操作)。其中,信号量对应于某一种资源,取一个非负的整型值。
信号量值指的是当前可用的该资源的数量,若它等于0则意味着目前没有可用的资源。
上面提到了PV原子操作,具体的定义为:
-
P操作
如果有可用的资源(信号量值大于0),则占用一个资源(信号量值减1,进入临界区代码);如果没有可用资源(信号量值等于0),则被阻塞,直到系统将资源分配给该任务(进入等待队列,一直等到有资源时被唤醒)。 -
V操作
如过在该信号量等待队列中有任务在等待资源,则唤醒一个阻塞任务。如果没有任务等待它则释放一个资源(先后量值加1)。
下面我们以一段伪代码来描述下这个机制:
/* 将R设定为某种资源,S为资源R的信号量 */
INT_VAL(S); // 对信号量S进行初始化
非临界区;
P(S); // 对临界资源R进行P操作
临界区(访问资源R); // 只有有限个(通常只有一个)进程被允许进入该区
V(S); // 对临街资源R进行V操作
非临界区;
最简单的信号量只有0和1,这种信号量通常称为二值信号量。
二值信号量是什么意思呢?即同时只能有一个任务访问临界资源,这个就是互斥的概念了。
下面让我们以最简单的二值信号量,来连接下Linux下的信号量机制。从二值信号量入手也方便后续扩展到多值信号量的情况。
二、信号量编程
Linux系统中,使用信号量通常分为以下几个步骤:
- 创建信号量或获得系统已存在的信号量,此时需要调用
semget()
函数。不同进程通过使用同一个信号量键值来获得同一个信号量。 - 初始化信号量,此时使用
semctl()
函数的SETVAL操作。当使用二维信号量时,通常将信号量初始化为1。 - 进行信号量的PV操作,此时调用
semop()
函数。这一步是实现任务之间的同步和互斥的核心工作部分。 - 如果不需要信号量,则从系统中删除它,此时使用
semctl()
函数的IPC_RMID操作。需要注意的是,在程序中不应该出现对已经被删除的信号量的操作。
semget()函数语法:
所需头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semget(key_t key, int nsems, int semflg);
函数传入值
key
信号量的键值,多个进程可以通过它访问同一个信号量。其中有个特殊的值IPC_PRIVATE, 用于创建当前进程的私有信号量
nsems
需要创建的信号量数目,通常取值为1
semflg
同open()函数的权限位,也可以用八进制表示法。
其中使用IPC_CREAT标志创建新的信号量,即使该信号量已经存在(具有同一个键值的信号量已在系统中存在),也不会出错。
如果同时使用IPC_EXCL标志可以创建一个新的唯一的信号量,此时如果该信号量已经存在, 该函数会返回出错。
函数返回值
成功
信号量表示符,在信号量的其他函数中都会使用该值
出错
-1
semctl()函数语法:
所需头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semctl(int semid, int semnum, int cmd, union semun arg);
函数传入值
semid
semget()函数返回的信号量表示符
semnum
信号量编号,当使用信号量集时才会被用到。通常取值为0,就是使用单个信号量(也是第一个信号量);
cmd
指对信号量的各种操作,当使用单个信号量时(而不是信号量集时),常用到的操作有以下几种:
IPC_STAT:获得该信号量(或者信号量集合)的semid_ds结构,并存放在由第四个参数arg结构变量的buf域指向的semid_ds结构中。
semid_ds是在系统中描述信号量的数据结构
IPC_SETVAL:将信号量值设置为arg的val值
IPC_GETVAL:返回信号量的当前值
IPC_RMID:从系统中删除信号量(或者信号量集)
arg
是union semun结构,可能在某些系统中不给出该结构的定义,此时必须由程序员自己定义。
函数返回值
成功
根据cmd值的不同而返回不同的值:
IPC_STAT、IPC_SETVAL、IPC_RMID:返回0
IPC_GETVAL:返回信号量的当前值
失败
出错:-1
上面提到的union semun结构的定义如下:
union senun
{
int val;
struct semid_ds *buf;
unsigned short *array;
}
semop()函数语法:
所需头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semop(int semid, struct sembuf *sops, size_t nsops);
函数传入值
semid
semget()函数返回的信号量表示符
sops
指向信号量操作数组,具体见下文。
nsops
操作数组sops中的操作个数(元素数目),通常取值为1(一个操作)
函数返回值
成功
根据cmd值的不同而返回不同的值:
IPC_STAT、IPC_SETVAL、IPC_RMID:返回0
IPC_GETVAL:返回信号量的当前值
失败
-1
struct sembuf结构体定义如下:
struct sembuf
{
short sem_num; /* 信号量编号,使用单个信号量时,通常取值为0 */
short sem_op; /* 信号量操作:取值为-1则表示P操作,取值为+1则表示为V操作 */
short sem_flg; /* 通常设置为SEM_UNDO。这样在进程没释放信号量而退出时,系统会自动释放该进程中未释放的信号量 */
}
以上就是信号量的操作的几个常用的函数,可以看到信号量操作相对还是比较复杂的。我们编程使用起来比较麻烦,那么该怎么办呢?对信号量相关的操作进行二次封装。
我们可以将其封装为sem_init()、sem_p()、sem_v()、sem_del()等。分别对应于信号量的初始化、P操作、V操作、信号量删除。
我们再这里对上面用到的几个接口做一个封装,将它们封装在sem_com.c中具体代码为:
/*************************************************************************
> File Name: sem_init.c
> Author:
> Mail:
> Created Time: 2021年12月26日 星期日 18时23分51秒
************************************************************************/
#include "sem_com.h" /* 自己定义union semun结构,否则编译的时候会报错 */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
/* 信号量初始化 */
int sem_init(int sem_id, int init_value)
{
printf("[%s-%d]\n",__func__,__LINE__);
union semun sem_union;
sem_union.val = init_value; /* init_value set*/
if(semctl(sem_id,0,SETVAL,sem_union) == -1)
{
perror("Init semaphore");
return -1;
}
return 0;
}
/* 删除信号量的函数 */
int sem_del(int sem_id)
{
printf("[%s-%d]\n",__func__,__LINE__);
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
{
perror("semaphore delete");
return -1;
}
return 0;
}
/* P 操作函数*/
int sem_p(int sem_id)
{
printf("[%s-%d]\n",__func__,__LINE__);
struct sembuf sem_b;
sem_b.sem_num = 0; /* 单个信号量的编号应该为0 */
sem_b.sem_op = -1; /* 表示P操作 */
sem_b.sem_flg = SEM_UNDO; /*sem_flg设定为此值,则系统会自动释放残留的信号量 */
if(semop(sem_id, &sem_b, 1) == -1)
{
perror("P operation");
return -1;
}
return 0;
}
int sem_v(int sem_id)
{
printf("[%s-%d]\n",__func__,__LINE__);
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = +1;
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
perror("V operation");
return -1;
}
return 0;
}
注意这里的我们自己定义了union semun共用体,因为较新版本的内核将该定义注释掉了,所以这里我们只能自己定义,否则会如下的错误:
这里再贴上sem_com.h
/*************************************************************************
> File Name: sem_com.h
> Function: semaphore operate
> Author: fuyunliuxinag CSDN
************************************************************************/
#ifndef _SEM_COM_H_
#define _SEM_COM_H_
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int sem_init(int sem_id, int init_value);
int sem_del(int sem_id);
int sem_p(int sem_id);
int sem_v(int sem_id);
#endif
接下来,我们写一个测试接口来测试我们封装的信号量相关的接口:
/*************************************************************************
> File Name: main.c
> > Author: fuyunliuxinag CSDN
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "sem_com.h"
#define DELAY_TIME 3
int main(void)
{
pid_t pid;
int sem_id;
sem_id = semget(ftok(".",48),1,0666|IPC_CREAT);
if(sem_id < 0)
{
perror("semget");
return -1;
} else {
printf("semget sem_id = %d\n",sem_id);
}
sem_init(sem_id,0);
pid = fork();
if(pid == -1)
{
perror("Fork");
} else if(pid == 0) {
printf("Child process wait ...\n");
sleep(DELAY_TIME);
printf("fork() returnd value is %d int the Child process(PID = %d)\n",pid,getpid());
sem_v(sem_id);
sem_del(sem_id);
} else {
sem_p(sem_id);
printf("fork() return valuse is %d in the father process(PID = %d)",pid,getpid());
sem_v(sem_id);
}
exit(0);
}
测试例子中用到了ftok()函数,该函数用法如下:
key_t ftok(const char *pathname, int proj_id);
参数:
pathname:路径
proj_id : 0~255的任意值
功能: ftok根据路径名,提取文件信息,再根据这些文件信息及project ID合成key。该路径必须存在。
下面开始测试例子:
运行结果为: