Linux应用进程间通信五(信号量)
一、概述
1.1、信号量的基本概念
信号量是一种用于解决进程间同步与互斥问题的通信机制。在多任务操作系统环境下,多个进程可能会为了完成同一个任务而相互协作,形成进程间的同步关系。同时,不同进程间也可能会为了争夺有限的系统资源而进入竞争状态,形成进程间的互斥关系。信号量通过维护一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及提供对信号量进行的两个原子操作(P操作和V操作),来实现进程间的同步与互斥。
1.2、信号量的工作原理
- 信号量的表示:信号量对应于某一种资源,取一个非负的整型值。这个值表示当前可用的该资源的数量。若信号量值等于0,则意味着目前没有可用的资源。
- P操作:当进程需要占用一个资源时,会执行P操作。如果信号量大于0,表示有可用的资源,进程将占用一个资源(信号量值减1),并继续执行。如果信号量等于0,表示没有可用的资源,进程将被阻塞,直到系统将资源分配给该进程。
- V操作:当进程释放一个资源时,会执行V操作。如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程。如果没有进程等待它,则释放一个资源(信号量值加1)。
1.3、信号量的类型
在Linux系统中,信号量主要分为以下几种类型:
- POSIX有名信号量:有名信号量的值保存在文件中(具体是保存在/dev/shm/目录下),使用时需要链接库。它既可以用于进程间通信(包括不相关进程),也可以用于线程间通信。
- POSIX无名信号量:无名信号量的值保存在内存中,Linux只支持线程间的同步。
- SYSTEM V信号量:这是另一种在Linux系统中使用的信号量类型。
1.4、信号量的编程接口
在Linux系统中,使用信号量通常涉及以下几个函数:
- semget()函数:用于创建信号量或获得系统中已存在的信号量。
- semctl()函数:用于对信号量进行各种控制操作,如初始化信号量、获取信号量的当前值、从系统中删除信号量等。
- semop()函数:用于对信号量进行P操作和V操作。
二、实现过程
2.1、信号量
信号:是在软件层次上对中断机制的一种模拟,是一种异步通信方式。可以直接进行用户空间进程和内核进程之间的交互。它可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它。
信号量:它是一个计数器,用于为多个进程提供对共享数据对象的访问。
为了获取共享资源,进程需要执行下列操作
1.测试控制该资源的信号量。
2.若信号量的值大于0,则进程可以使用该资源。在这种情况下,进程会将信号量的值减1,表示它使用了一个资源。
3.若信号量的值为0,则进程进入休眠,直至信号量的值大于0。进程被唤醒后,它返回至步骤1。
内核为每个信号量集合维护着一个semid_ds结构
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short sem_nsems;
time_t sem_otime;
time_t sem_ctime;
...
}
2.2、semget
#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
第一个参数key:key是一个整数值,不相关的进程可以通过它访问同一个信号量。
第二个参数num_sems:指定需要的信号量数目。
第三个参数sem_flags:访问权限。
返回值: 成功:正数(非零)值; 失败:-1
作用:创建一个新的信号量或取一个已有信号量的键。当创建一个新的信号量时,要对semid_ds结构的下列成员赋初值。
初始化ipc_perm结构,该结构中的mode成员被设置为flag中的相应权限位。
sem_otime设置为0.
sem_ctime设置为当前时间。
sem_nsems设置为num_sems。
注意:如果是创建新的信号量(一般在服务器进程中),则必须指定num_sems。如果是引用现有信号量,则将num_sems指定为0。
2.3、semop
#include <sys/sem.h>
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
第一个参数sem_id:信号量标识符。
第二个参数sem_ops是指向一个结构数组的指针,每个数组元素至少包含以下几个成员:
struct sembuf{
short sem_num; //信号量编号,除非使用一组信号量,否则它的取值为0
short sem_op; //信号量在一次操作中需要改变的数值。通常用到两个值,-1,也就是p操作,它等待信号量变为可用;+1,也就是V操作,它发送信号表示信号量现在已可用。
short sem_flg; //通过被设置为SEM_UNDO
};第三个参数num_sem_ops规定该数组中操作得数量。
2.4、semctl
#include <sys/shm.h>
int semctl(int sem_id, int sem_num, int command,...);
第一个参数sem_id:信号量标识符
第二个参数sem_num:信号量编号,当用到成组的信号量,就要用到这个参数。它一般取值为0,表示这是第一个也是唯一的一个信号量
第三个参数command:要采取的动作
有很多不同的值,有两个常用的SETVAL:用来把信号量初始化成一个已知的值,这个值通过union semun中的val成员设置。
IPC_RMID:用于删除一个已经无需使用的信号量标识符。
第四个参数:可选。是否使用取决于所请求的命令。如果使用该参数,则其类型是semun。
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
2.5、sembuf中sem_flg的设置问题
通常设置为SEM_UNDO,使操作系统跟踪信号量, 并在进程没有释放该信号量而终止时,操作系统释放信号量 ,例如在二元信号量中,你不释放该信号量 而异常退出,就会导致别的进程一直申请不到信号量,而一直处于挂起状态。
是否设置sem_flg为SEM_UNDO的区别:
三、实例应用
以下是一个简单的编程实例,演示了如何在Linux中使用信号量进行进程间通信:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <errno.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main() {
int semid;
struct sembuf sop;
// 创建信号量集合,其中包含一个信号量
semid = semget((key_t)1234, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
exit(1);
}
// 初始化信号量的值为1
union semun sem_init_val;
sem_init_val.val = 1;
if (semctl(semid, 0, SETVAL, sem_init_val) == -1) {
perror("semctl SETVAL");
exit(1);
}
// 父进程创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
}
if (pid == 0) {
// 子进程进行P操作,申请资源
sop.sem_num = 0;
sop.sem_op = -1; // P操作
sop.sem_flg = SEM_UNDO;
if (semop(semid, &sop, 1) == -1) {
perror("semop P");
exit(1);
}
printf("子进程:成功申请到资源\n");
sleep(2); // 模拟子进程使用资源的时间
// 子进程进行V操作,释放资源
sop.sem_op = 1; // V操作
if (semop(semid, &sop, 1) == -1) {
perror("semop V");
exit(1);
}
printf("子进程:已释放资源\n");
} else {
// 父进程等待一段时间,然后尝试进行P操作申请资源
sleep(1);
sop.sem_num = 0;
sop.sem_op = -1; // P操作
sop.sem_flg = SEM_UNDO;
if (semop(semid, &sop, 1) == -1) {
perror("semop P");
exit(1);
}
printf("父进程:成功申请到资源\n");
sleep(2); // 模拟父进程使用资源的时间
// 父进程进行V操作,释放资源
sop.sem_op = 1; // V操作
if (semop(semid, &sop, 1) == -1) {
perror("semop V");
exit(1);
}
printf("父进程:已释放资源\n");
// 删除信号量集合
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID");
exit(1);
}
}
return 0;
}
- 创建信号量集合:使用
semget()
函数创建一个信号量集合,其中包含一个信号量。key_t
类型的键值为1234,用于唯一标识这个信号量集合。权限设置为0666,表示所有用户都有读写权限。 - 初始化信号量:使用
semctl()
函数的SETVAL
操作将信号量的值初始化为1。这表示系统中有1个可用的资源。 - 创建子进程:使用
fork()
函数创建一个子进程。父进程和子进程将竞争访问同一个资源(即信号量表示的资源)。 - P操作和V操作:父进程和子进程分别进行P操作和V操作来申请和释放资源。在P操作中,如果信号量的值为0,则进程将被阻塞;在V操作中,将唤醒等待队列中的一个阻塞进程(如果有的话),并将信号量的值加1。
- 删除信号量集合:在父进程中,使用
semctl()
函数的IPC_RMID
操作删除信号量集合。