Linux - 进程间通信之信号量
一、信号量及使用场景
1、信号量
- 信号量:是一种特殊的变量。只能对信号量执行P操作和V操作。
- P操作:如果信号量的值大于0,则把该信号量减1。如果信号量的值等于0,则挂起该进程。
- V操作:如果有进程因该信号量而被挂起,则恢复该进程运行。如果没有进程因该信号量而挂起,则把该信号量减1。
- 注意:P操作和V操作都是原子操作,即在执行时不会被中断。
- 此处的"信号量"指的是 System V IPC的信号量,与线程所使用的信号量不同。该信号量,用于进程间通信。
2、应用场景
在程序中,有时存在一种特殊代码,这段代码只允许一个进程执行,这部分区域,称为"临界区"。然而在多进程并发执行时,当一个进程进入临界区,因某种原因被挂起时,其他进程就有可能也进入该区域。此类问题可用信号量来解决(与红绿灯类似)。
二、信号量的使用
1、信号量的获取,使用函数semget
/***************************************************************************
* 函数:int semget(key_t key, int nsems, int semflg);
* 功能:获取一个已存在的或创建一个新的信号量。
* 参数:
* key - 键值,该键值对应一个唯一的信号量。类似于共享内存的键值。不同的进程可通过该键值和semget获取唯一的信号量。特殊键值:IPC_PRIVAT该信号量只允许创建者本身, 可用于父子进程间通信。
* nsems - 需要的信号量数目,一般取1
* semflg - 消息队列的存取权限和建立标志(传值示例:0666 | IPC_CREAT)。
可取如下值:
IPC_CREAT(如果信号量不存在,则创建,否则进行打开操作)、
IPC_EXCL(和IPC_CREAT一起使用(使用 | 连接),如果消息对象不存在则创建,否则报错)
* 返回:
* 成功 - 返回值将是信号量集标识符(非负整数)
* 失败 - 返回-1,其中errno表示错误。
****************************************************************************/
2、信号量的操作,使用函数semop
/***************************************************************************
* 函数:int semop(int semid, struct sembuf *sops, size_t nsops);
* 功能:改变信号量的值,即对信号量执行P操作、或V操作。
* 参数:
* semid - 信号量标识符, 即semget的返回值
* sops - 是一个数组,元素类型为struct sembuf
* nsops - 表示第二个参数sops所表示的数组的大小,即表示有几个struct sembuf
* 返回:
* 成功 - 返回0
* 失败 - 返回-1
****************************************************************************/
struct sembuf {
short sem_num; // 信号量组中的编号(即指定对哪个信号量操作)。semget实际是获取一组信号量,如果只获取到了1个信号量,则该成员取0
short sem_op; // 信号量运算。-1表示P操作。1表示V操作
short sem_flg; // 操作标志。可传IPC_NOWAIT和SEM_UNDO。如果操作指定SEM_UNDO,则该操作将在进程结束时自动撤销
};
3、信号量的控制,使用函数semctl
/***************************************************************************
* 函数:int semctl(int semid, int semnum, int cmd, ...);
* 功能:对信号量进行控制
* 参数:
* semid - 信号量标识符, 即semget的返回值
* semnum - 信号量组中的编号,如果只有一个信号量,则取0
* cmd - 对信号量的操作
* 一般取值:
* SETVAL - 把信号量初始化为指定的值,具体由第四个参数确定
* IPC_RMID - 删除信号量
* 返回:
* 成功 - 根据cmd的不同,返回不同的值。IPC_STAT、IPC_SETVAL(或SETVAL)、IPC_RMID返回0;IPC_GETVAL返回信号量的当前值
* 失败 - 返回-1
****************************************************************************/
// 参数4的类型为:
union semun {
int val; // SETVAL命令要设置的值
struct semid_ds *buf;
unsigned short *array;
};
// 注意:unison semun类型要求自定义(有些Linux发行版在 sys/sem.h中定义,有些发行版没有定义)。可自定义如下:
#if defined(__GNU_LIBARAY__) && !define(_SEM_SEMUN_UNDEFINED)
#else
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
}
#endif
三、示例代码
1、不使用信号量和使用信号量的临界区对比
- 不使用信号量
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
pid_t pid = fork();
if(pid < 0) {
fprintf(stderr, "fork() - failed!\n");
exit(1);
}
for(int i = 0; i < 5; ++i) {
// 模拟临界区 begin
printf("[%d] - In!\n", getpid());
sleep(1);
printf("[%d] - Out!\n", getpid());
// 模拟临界区 end
sleep(1);
}
return 0;
}
执行结果如下:
2. 使用信号量
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
//#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINEED)
//#else
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
//#endif
// 初始化
static int sem_initial(int semid) {
union semun sem;
sem.val = 1;
int ret = semctl(semid, 0, SETVAL, sem);
if(ret == -1) {
fprintf(stderr, "semctl() - failed!\n");
}
return ret;
}
// P操作
static int sem_p(int semid) {
struct sembuf buf;
buf.sem_op = -1;
buf.sem_num = 0;
buf.sem_flg = SEM_UNDO;
return semop(semid, &buf, 1);
}
// V操作
static int sem_v(int semid) {
struct sembuf buf;
buf.sem_op = 1;
buf.sem_num = 0;
buf.sem_flg = SEM_UNDO;
return semop(semid, &buf, 1);
}
int main(int argc, char *argv[]) {
// 获取信号量
int semid = semget((key_t)2021, 1, 0666 | IPC_CREAT);
if(semid == -1) {
printf("semget() - failed!\n");
exit(1);
}
// 初始化信号量
int ret = sem_initial(semid);
if(ret == -1) {
exit(2);
}
int pid = fork();
if(pid < 0) {
fprintf(stderr, "fork() - failed!\n");
exit(3);
}
for(int i = 0; i < 5; ++i) {
if(sem_p(semid) == -1) {
fprintf(stderr, "sem_p() - failed!\n");
exit(3);
}
// 模拟临界区 begin
printf("[%d] - In!\n", getpid());
sleep(1);
printf("[%d] - Out!\n", getpid());
// 模拟临界区 end
if(sem_v(semid) == -1) {
fprintf(stderr, "sem_v() - failed!\n");
exit(4);
}
sleep(1);
}
// 删除信号量
if( semctl(semid, 0, IPC_RMID) == -1) {
fprintf(stderr, "semctl() - failed!\n");
exit(5);
}
return 0;
}
执行结果如下:
2、使用信号量实现对文件的互斥访问
示例代码 - 使用信号量实现对文件的互斥访问:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
union semun {
int val;
struct semid_de *buf;
unsigned short int *array;
struct seminfo *__info;
};
int sem_init(int semid);
int sem_p(int semid);
int sem_v(int semid);
int main(int argc, char *argv[]) {
int semid = semget((key_t)2021, 1, 0666 | IPC_CREAT);
if(semid < 0) {
fprintf(stderr, "semget() - failed!\n");
exit(1);
}
// 初始化信号量
if(sem_init(semid) < 0) {
fprintf(stderr, "sem_init() - failed!\n");
exit(2);
}
int pid = fork();
char info[64] = { 0 };
if(pid < 0) {
fprintf(stderr, "fork() - failed!\n");
exit(3);
} else if(pid == 0) {
strcpy(info, "student");
} else {
strcpy(info, "teacher");
}
for(int i = 0; i < 10; ++i) {
if(sem_p(semid) < 0) {
fprintf(stderr, "No.%d sem_p() - failed!\n", i);
exit(3);
}
FILE *file = fopen("./info.txt", "a+");
if(!file) {
fprintf(stderr, "No.%d fopen() - failed!\n", i);
exit(4);
}
char buffer[1024];
sprintf(buffer, "No. %d, Write %s information!\n", i, info);
fwrite(buffer, sizeof(char), strlen(buffer), file);
fclose(file);
if(sem_v(semid) < 0) {
fprintf(stderr, "No.%d sem_v() - failed!\n", i);
exit(5);
}
}
return 0;
}
int sem_init(int semid) {
union semun sem;
sem.val = 1;
return semctl(semid, 0, SETVAL, sem);
}
int sem_p(int semid) {
struct sembuf buf;
buf.sem_op = -1;
buf.sem_num = 0;
buf.sem_flg = SEM_UNDO;
return semop(semid, &buf, 1);
}
int sem_v(int semid) {
struct sembuf buf;
buf.sem_op = 1;
buf.sem_num = 0;
buf.sem_flg = SEM_UNDO;
return semop(semid, &buf, 1);
}
执行结果: