文章目录
前言
本文主要探讨 linux 下进程间的通信方式之信号量,最后通过一个简单的示例实现一个父子进程之间操作临界资源使用信号量处理同步与互斥的功能。
一、信号量概念
信号量(Semaphore)的概念最早由荷兰计算机科学家 Dijkstra(迪杰斯特拉)提出,有时又称“信号灯”。信号量本质上是一个计数器,用于协调多个进程(包括但不限于父子进程)对共享数据对象的读写。与其它进程间通信方式不大相同,它不以传送数据为目的,主要是作为一种进程间的同步机制用来保护共享资源。
1.1 信号跟信号量的区别
信号是一种处理异步事件的机制,用于通知进程某个事件的发生。而信号量是一种用于处理进程或者线程间同步与互斥的机制。
1.2 同步跟互斥的区别
同步与互斥机制是计算机系统中用于控制进程或线程对某些特定资源访问的两种机制。
1.2.1 同步的概念
同步指的是多个进程或线程在相互配合完成某项任务时,需要按照某种特定的顺序或条件来执行。也就是说,一个进程或线程的执行可能依赖于另一个进程或线程的执行结果或状态。
1.2.2 互斥的概念
互斥是指某个资源在某一时刻只允许一个进程或线程访问以防止多个进程或线程同时访问共享资源造成的数据不一致或其他问题。
1.3 原子操作概念
原子操作是指一个操作在执行过程中不会被其他线程或任务打断或分割的一个操作。即这个操作要么完全执行成功,要么完全不执行,不会出现部分执行或中间状态。这种特性使得原子操作在多线程或并发执行的情况下,能够保证对共享资源的操作能够正确、完整地进行。
信号量的原子操作特性是实现进程间同步与互斥的关键。通过P操作和V操作来申请和释放信号量资源,信号量的原子操作特性可以确保在任一时刻只有一个进程能够访问共享资源,从而实现了对临界资源的安全访问。
二、信号量的相关操作函数
2.1 ftok 函数(获取一个key值)
函数原型 | key_t ftok(const char *pathname, int proj_id); |
功能 | 基于文件路径和proj_id子序号生成一个键值 |
参数 | |
pathname: 文件路径 | |
proj_id: 子序号 | |
返回值 | 成功返回一个键值,失败返回-1并设置errno指明错误的原因 |
2.2 semget函数(创建或者获取信号量)
函数原型 | int semget(key_t key, int nsems, int semflg); |
功能 | 创建或者获取一个信号量集标识符 |
参数 | |
key: 键值,唯一标识一个信号量集,可取由ftok创建的key值或指定的一个非负整数值 | |
nsems: 指定信号量集中信号量的个数。 | |
semflg: 这是一组标志,用于控制 semget 函数的行为 | |
返回值 | 成功返回一个信号量集标识符,失败返回-1并设置errno指明错误的原因 |
这里涉及到一个概念,信号量集。什么是信号量集呢?你可以把它理解为一个数组,这个数组里存放一堆信号量,称之为信号量集。
参数 nsems 就是指定信号量集中信号量的个数。我们使用信号量一般的目的就只是用来做进程间的同步,所以我们只需要设置 nsems 为1即可,后续只对这个信号量进行P、V操作。
关于semflg我已经在第一章介绍消息队列的时候就详细讲过了,这里就不再讲了,可以参考一下msgget函数介绍那段。
传送门:Linux进程间的通信方式(一)System V 消息队列
2.3 semctl函数(控制信号量)
函数原型 | int semctl(int semid, int semnum, int cmd, ...); |
功能 | 信号量控制操作,包括创建、删除、获取和设置信号量的初始值以及获取信号量集的信息 |
参数 | |
semid: 信号量集的标识符,通过 semget 函数获取 | |
semnum: 指定信号量的编号,用于指定在信号量集中的特定信号量。简单理解就是下标 | |
cmd: 指定信号量的操作类型。常用的就两个,IPC_RMID 和 SETVAL | |
... : 可选的参数,根据 cmd 的不同,传递不同类型的数据。通常是 union semun | |
返回值 | 成功返回0,失败返回-1并设置errno指明错误的原因 |
我们先来看看man手册的这段描述
semctl() performs the control operation specified by cmd on the System V semaphore set identified by semid, or on the semnum-th semaphore of that set. (The semaphores in a set are numbered starting at 0.)
翻译过来就是 semctl 函数对一个由 semid 标识的System V信号量集或是该信号量集中第 semnum 个信号量执行 cmd 指定的控制操作指令(一个信号量集中的信号量从0开始编号)
在这里要注意-th的用法,意思就是第几个的意思。
简而言之就是说 semctl 函数既可以对整个信号量集做控制操作,也可以对单个信号量做控制操作。对单个信号量做控制操作的时候可以通过下标指定操作的信号量,要注意的是第一个信号量的下标为 0,这个跟数组是一样的。
结合上面我们对 semget 函数的分析,一般 semctl 函数的用法是这样的
/* 创建或者获取一个信号量集 */
int create_sem(const char *path, int proj_id, int nsems)
{
key_t keyval = ftok(path, proj_id);
int semid = semget(keyval, nsems, IPC_CREAT | IPC_EXCL | 0666);
if (semid == -1) {
if (errno == EEXIST) {
semid = semget(keyval, 0, 0);
printf("the semaphore set whose key is %d and semid is %d is already exist!\n", keyval, semid);
return semid;
} else
perror("semget");
}
return semid;
}
/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
union semun su;
su.val = value;
if (semctl(semid, semnum, SETVAL, su) == -1) {
ERR_EXIT("semctl");
}
return 0;
}
int main()
{
int ret, semid;
// 创建或者获取一个信号量集
semid = create_sem(SEM_PATH_NAME, SEM_PROJ_ID, 1);
if (semid < 0)
return -1;
// 初始化信号量值为1
init_sem(semid, 0, 1);
return 0;
}
1. 创建或者获取一个信号量集并获取该信号量集标识符
在这里我们首先创建了一个信号量集并设置信号量的个数为1。
注意:当我们尝试去获取一个已经存在的信号量集的时候,semget 函数的第二第三个参数是被忽略的,因为你可以直接填0。
2. 初始化该信号量集(操作整个信号量集或对指定信号量进行初始值设置)
接着我们设置信号量集中下标为0也就是第一个信号量的初始值为1。
至此就完成了信号量的初始化了,接下来只需要对该信号量进行P、V操作即可。
常用cmd操作指令
指令 | 描述 |
---|---|
IPC_RMID | 删除信号量集 |
SETVAL | 设置指定信号量的值 |
当cmd为IPC_RMID时只需要三个参数
/* 删除信号量集 */
int del_sem(int semid) {
if (semctl(semid, 0, IPC_RMID) == -1) {
ERR_EXIT("semctl");
}
return 0;
}
当cmd为SETVAL时需要四个参数,第四个参数是一个联合体,定义如下:
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
union semun su;
su.val = value;
if (semctl(semid, semnum, SETVAL, su) == -1) {
ERR_EXIT("semctl");
}
return 0;
}
2.4 semop函数(操作信号量)
函数原型 | int semop(int semid, struct sembuf *sops, size_t nsops); |
功能 | 用于对信号量集或信号量的值进行P和V操作 |
参数 | |
semid: 信号量集合的标识符,通过 semget 函数获取 | |
sops: 一个指向sembuf结构体数组的指针 | |
nsops: 该结构体数组的元素个数 | |
返回值 | 成功返回0,失败返回-1并设置errno指明错误的原因 |
关于指针sops可以这样理解。首先它是一个sembuf类型的结构体指针,它指向一个sembuf类型的结构体数组。每个结构体数组元素是一个信号量,一整个结构体数组就是一整个信号量集,这样通过sops可以操作信号量集中的信号量了。
sembuf结构体的定义如下
struct sembuf {
unsigned short sem_num; // 信号量集合中的信号量编号
short sem_op; // 操作类型
short sem_flg; // 操作标志
};
以下是结构体sembuf中各个成员变量的解析:
-
sem_num: 信号量集中某个特定信号量的编号,第一个信号量的编号是0。
-
sem_op: 要执行的操作,一般只会用到两个值-1和1,PV操作均为原子操作。
P(-1)操作: 尝试获取资源。若获取失败则阻塞等待,若获取成功则将信号量的值减1表示信号量被本进程占用,其他进程不可获取。
V(+1)操作: 释放资源。若操作完临界资源后需要释放信号量让其他进程抢夺则进行V操作将信号量的值加1表示信号量被释放,其他进程可以抢夺该信号量。 -
sem_flg: 信号操作标志,一般有如下两种
IPC_NOWAIT: 对信号的操作不能满足时,semop()不会阻塞并立即返回,同时设定错误信息。
SEM_UNDO: 程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁造成该资源永远锁定。一般我们将它设置为SEM_UNDO
具体使用方法:
/* P 操作(-1)阻塞获取一个信号量 */
int sem_p(int semid, int semnum) {
struct sembuf sb = {semnum, -1, SEM_UNDO};
if (semop(semid, &sb, 1) == -1) {
ERR_EXIT("semop");
}
return 0;
}
/* V 操作(+1)释放一个信号量 */
int sem_v(int semid, int semnum) {
struct sembuf sb = {semnum, 1, SEM_UNDO};
if (semop(semid, &sb, 1) == -1) {
ERR_EXIT("semop");
}
return 0;
}
sem_p(semid, 0); // P操作,进入临界区
sem_v(semid, 0); // V操作,离开临界区
三、信号量应用示例
3.1 System V信号量应用示例代码
头文件:sem_com.h
#ifndef SEM_COM_H
#define SEM_COM_H
#ifdef __cplusplus
extern "C" {
#endif
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#define SEM_PATH_NAME "/tmp"
#define SEM_PROJ_ID 0x6666
// 错误处理宏
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int create_sem(const char *path, int proj_id, int nsems);
int init_sem(int semid, int semnum, int value);
int del_sem(int semid);
int sem_p(int semid, int semnum);
int sem_v(int semid, int semnum);
#ifdef __cplusplus
}
#endif
#endif // !SEM_COM_H
实现文件:sem_com.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include "sem_com.h"
/* 创建或者获取一个信号量集 */
int create_sem(const char *path, int proj_id, int nsems)
{
key_t keyval = ftok(path, proj_id);
int semid = semget(keyval, nsems, IPC_CREAT | IPC_EXCL | 0666);
if (semid == -1) {
if (errno == EEXIST) {
semid = semget(keyval, 0, 0);
printf("the semaphore set whose key is %d and semid is %d is already exist!\n", keyval, semid);
return semid;
} else
perror("semget");
}
return semid;
}
/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
union semun su;
su.val = value;
if (semctl(semid, semnum, SETVAL, su) == -1) {
ERR_EXIT("semctl");
}
return 0;
}
/* 删除信号量集 */
int del_sem(int semid) {
if (semctl(semid, 0, IPC_RMID) == -1) {
ERR_EXIT("semctl");
}
return 0;
}
/* P 操作(-1)阻塞获取一个信号量 */
int sem_p(int semid, int semnum) {
struct sembuf sb = {semnum, -1, SEM_UNDO};
if (semop(semid, &sb, 1) == -1) {
ERR_EXIT("semop");
}
return 0;
}
/* V 操作(+1)释放一个信号量 */
int sem_v(int semid, int semnum) {
struct sembuf sb = {semnum, 1, SEM_UNDO};
if (semop(semid, &sb, 1) == -1) {
ERR_EXIT("semop");
}
return 0;
}
主程序文件:example.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include "sem_com.h"
static int g_number = 0;
int main()
{
int ret, semid;
// 创建或者获取一个信号量集
semid = create_sem(SEM_PATH_NAME, SEM_PROJ_ID, 1);
if (semid < 0)
return -1;
// 初始化信号量值为1
init_sem(semid, 0, 1);
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
} else if (pid == 0) { /* child */
printf("I am child,pid:%d,ppid:%d\n", getpid(), getppid());
for (int i = 0; i < 15; i++) {
sem_p(semid, 0); // P操作,进入临界区
g_number += i;
printf("child: g_number: %d\n", g_number);
sem_v(semid, 0); // V操作,离开临界区
sleep(1);
}
exit(0);
} else {
printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
for (int i = 0; i < 10; i++) {
sem_p(semid, 0); // P操作,进入临界区
g_number -= i;
printf("father: g_number: %d\n", g_number);
sem_v(semid, 0); // V操作,离开临界区
sleep(1);
}
/*阻塞等待子进程结束*/
ret = wait(NULL);
if (ret == -1) {
perror("wait");
return -1;
}
del_sem(semid); // 删除信号量
}
return 0;
}
Makefile文件
# Makefile for compiling example program with semaphore implementation
# Compiler
CC = gcc
# Compiler flags
CFLAGS = -Wall -g
# Target executable
TARGET = example
# Source files
SRCS = example.c sem_com.c
# Object files
OBJS = $(SRCS:.c=.o)
# Header files
HEADERS = sem_com.h
# Default target
all: $(TARGET)
# Linking
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
# Compiling source files into object files
%.o: %.c $(HEADERS)
$(CC) $(CFLAGS) -c $< -o $@
# Clean up
clean:
rm -f $(OBJS) $(TARGET)
# Phony targets
.PHONY: all clean
3.2 应用示例代码讲解
编译后输出如下
jeff@jeff:~/jeffPro/practice/IPC/semaphore/sem1$ make
gcc -Wall -g -c example.c -o example.o
gcc -Wall -g -c sem_com.c -o sem_com.o
gcc -Wall -g -o example example.o sem_com.o
jeff@jeff:~/jeffPro/practice/IPC/semaphore/sem1$ ./example
I am father,pid:6338,ppid:2334
father: g_number: 0
I am child,pid:6339,ppid:6338
child: g_number: 0
child: g_number: 1
father: g_number: -1
child: g_number: 3
father: g_number: -3
child: g_number: 6
father: g_number: -6
child: g_number: 10
father: g_number: -10
child: g_number: 15
father: g_number: -15
father: g_number: -21
child: g_number: 21
father: g_number: -28
child: g_number: 28
father: g_number: -36
child: g_number: 36
father: g_number: -45
child: g_number: 45
child: g_number: 55
child: g_number: 66
child: g_number: 78
child: g_number: 91
child: g_number: 105
jeff@jeff:~/jeffPro/practice/IPC/semaphore/sem1$
通过 ipcs -s 指令可以查看到创建的信号量相关信息
jeff@jeff:~$ ipcs -s
------ Semaphore Arrays --------
key semid owner perms nsems
0x66000002 27 jeff 666 1
可以看到通过使用信号量实现了父子进程之间的同步与互斥功能。
标签:semid,int,System,number,信号量,Linux,操作,sem From: https://blog.csdn.net/Mr_Jaychong/article/details/140083578