Linux IPC全揭秘(五):进程同步:信号量与临界区
·
目录
一、为什么需要同步?
1. 临界资源问题
在操作系统中,临界资源是指一次仅允许一个进程(或线程)访问的资源。例如:共享内存、全局变量、文件、打印机等。当多个进程同时读写临界资源时,结果可能依赖于它们的执行顺序,这就是竞态条件(Race Condition)。
2. 数据不一致示例
考虑一个简单的共享计数器,两个进程同时对其加 1:
// 假设 counter 初始为 0
counter++; // 实际对应三条汇编指令:
// 1. load counter 到寄存器
// 2. add 1 到寄存器
// 3. store 寄存器回 counter
如果两个进程同时执行,可能出现如下交错:
- 进程 A 加载 counter (0)
- 进程 B 加载 counter (0)
- 进程 A 加 1 (1)
- 进程 B 加 1 (1)
- 进程 A 存储 (counter=1)
- 进程 B 存储 (counter=1)
最终结果应该是 2,却得到了 1。这就是典型的竞态条件,导致数据不一致。
结论:必须提供同步机制,确保对临界资源的访问是互斥的。
二、基本概念
| 概念 | 定义 |
|---|---|
| 临界区(Critical Section) | 访问共享资源的代码段,同一时刻最多允许一个执行流进入。 |
| 互斥(Mutual Exclusion) | 保证一次只有一个进程进入临界区,其他进程必须等待。 |
| 原子操作(Atomic Operation) | 不可被中断的操作,要么全部完成,要么全部不执行。 |
同步机制的目标就是:实现互斥,避免竞态。
三、信号量原理
信号量(Semaphore)是荷兰计算机科学家 Dijkstra 在 1965 年提出的同步工具,本质上是一个计数器,用来控制对共享资源的访问。
1. 核心组成
- 计数器(semaphore value):表示当前可用的资源数量。
- 正值:有这么多资源可用。
- 零:没有资源可用,申请者必须等待。
- 负值:绝对值表示等待队列中的进程数(取决于具体实现)。
- 等待队列:当进程无法获得资源时,会被挂起并加入队列。
2. 两个原子操作
- P 操作(wait / down):申请资源。
- 如果计数器 > 0,则将其减 1,进程继续执行。
- 如果计数器 == 0,则进程被阻塞,加入等待队列。
- V 操作(signal / up):释放资源。
- 将计数器加 1。
- 如果有进程正在等待该信号量,则唤醒其中一个。
注意:P 和 V 操作必须是原子的,通常由硬件指令或操作系统内核保证。
3. 二进制信号量(互斥锁)
当信号量计数器的初始值设为 1 时,它就是一个二进制信号量,用于实现互斥:
- P 操作相当于
lock(),V 操作相当于unlock()。 - 同一时刻只有一个进程能成功执行 P 操作,其他进程必须等待。
四、System V 信号量 API
System V 信号量是 Unix 系统中经典的信号量实现,支持信号量集(多个信号量原子操作)。其核心函数如下:
1. semget —— 创建或获取信号量集
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
key:IPC 键值,通常由ftok()生成。nsems:信号量集中包含的信号量个数(至少为 1)。semflg:权限标志(如0666)与IPC_CREAT、IPC_EXCL的组合。- 返回值:成功返回信号量集标识符
semid,失败返回 -1。
2. semop —— 执行信号量操作
int semop(int semid, struct sembuf *sops, size_t nsops);
semid:由semget返回的标识符。sops:指向struct sembuf数组,每个结构体描述一个操作。nsops:数组中元素个数(支持一次执行多个操作,原子性)。
struct sembuf 定义如下:
struct sembuf {
unsigned short sem_num; // 信号量在集合中的索引
short sem_op; // 操作数(负数 = P,正数 = V)
short sem_flg; // 标志,如 IPC_NOWAIT, SEM_UNDO
};
sem_op = -1:P 操作(申请资源)。sem_op = +1:V 操作(释放资源)。
3. semctl —— 信号量控制
int semctl(int semid, int semnum, int cmd, ...);
常用命令:
IPC_RMID:删除信号量集。SETVAL:设置某个信号量的值(通常用于初始化)。GETVAL:获取某个信号量的当前值。IPC_STAT:获取状态信息。
五、示例:用信号量保护共享内存
下面演示一个完整示例:两个进程通过共享内存共享一个结构体,并用二进制信号量保证互斥访问。
1. 共享数据结构
// shared_data.h
#ifndef SHARED_DATA_H
#define SHARED_DATA_H
#define SHM_SIZE 4096
#define SEM_KEY 0x1234
#define SHM_KEY 0x5678
struct shared_data {
int counter;
char buffer[100];
};
#endif
2. 初始化(由其中一个进程执行)
// init.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include "shared_data.h"
int main() {
// 创建信号量集(1个信号量)
int semid = semget(SEM_KEY, 1, IPC_CREAT | IPC_EXCL | 0666);
if (semid == -1) {
perror("semget");
return 1;
}
// 初始化信号量值为 1(二进制信号量)
semctl(semid, 0, SETVAL, 1);
// 创建共享内存
int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1) {
perror("shmget");
return 1;
}
// 附加并初始化共享数据
struct shared_data *data = shmat(shmid, NULL, 0);
data->counter = 0;
sprintf(data->buffer, "initial");
shmdt(data);
printf("Initialization done. semid=%d, shmid=%d\n", semid, shmid);
return 0;
}
3. 生产者进程(写入共享内存)
// producer.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
#include "shared_data.h"
int main() {
// 获取已有的信号量集
int semid = semget(SEM_KEY, 1, 0666);
if (semid == -1) {
perror("semget");
return 1;
}
// 获取共享内存
int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget");
return 1;
}
struct shared_data *data = shmat(shmid, NULL, 0);
// P 操作:申请资源
struct sembuf p_op = {0, -1, 0};
semop(semid, &p_op, 1);
// 进入临界区,修改共享数据
data->counter++;
sprintf(data->buffer, "Updated by producer, count=%d", data->counter);
printf("Producer: %s\n", data->buffer);
// V 操作:释放资源
struct sembuf v_op = {0, 1, 0};
semop(semid, &v_op, 1);
shmdt(data);
return 0;
}
4. 消费者进程(读取共享内存)
// consumer.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include "shared_data.h"
int main() {
int semid = semget(SEM_KEY, 1, 0666);
if (semid == -1) {
perror("semget");
return 1;
}
int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget");
return 1;
}
struct shared_data *data = shmat(shmid, NULL, 0);
// P 操作:申请资源
struct sembuf p_op = {0, -1, 0};
semop(semid, &p_op, 1);
// 临界区:读取共享数据
printf("Consumer: counter=%d, buffer=%s\n", data->counter, data->buffer);
// V 操作:释放资源
struct sembuf v_op = {0, 1, 0};
semop(semid, &v_op, 1);
shmdt(data);
return 0;
}
5. 清理(删除 IPC 对象)
// cleanup.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include "shared_data.h"
int main() {
int semid = semget(SEM_KEY, 1, 0666);
int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (semid != -1) semctl(semid, 0, IPC_RMID);
if (shmid != -1) shmctl(shmid, IPC_RMID, NULL);
printf("IPC objects removed.\n");
return 0;
}
6. 运行示例
$ ./init
Initialization done. semid=32768, shmid=4096
$ ./producer
Producer: Updated by producer, count=1
$ ./consumer
Consumer: counter=1, buffer=Updated by producer, count=1
$ ./cleanup
IPC objects removed.
如果去掉信号量保护,同时运行多个 producer 和 consumer,就会出现数据错乱。有了信号量,无论启动多少个进程,临界区的代码总是互斥执行的。
六、总结
| 知识点 | 要点 |
|---|---|
| 为什么需要同步 | 避免竞态条件,保证共享数据的一致性。 |
| 临界区与互斥 | 访问共享资源的代码段需要互斥保护。 |
| 信号量原理 | 计数器 + P/V 操作,可实现资源计数与互斥。 |
| 二进制信号量 | 初值为 1 的信号量,等同于互斥锁。 |
| System V API | semget, semop, semctl。 |
| 保护共享内存 | 使用信号量将共享内存的读写变成临界区。 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)