信号量实现互斥
2026/8/25大约 3 分钟
Linux 进程间通信:System V 信号量实现进程互斥
1. 什么是进程互斥
多个进程同时访问共享资源时:
同一时间只能有一个进程进入临界区。
本例中,父子进程都执行:
printf("%c", ch);
sleep(rand() % 3);
printf("%c", ch);要求两个字符必须连续输出:
aa
bb不能被另一个进程插入。
2. 核心:信号量初始化为 1
sem_id = semget(ftok(".", 'a'), 1, 0666 | IPC_CREAT);
init_sem(sem_id, 1);此时:
信号量 = 1把它理解成:
只有一把钥匙,拿到钥匙的进程才能进入临界区。
3. P 操作:进入临界区
struct sembuf p_op = {0, -1, SEM_UNDO};
semop(sem_id, &p_op, 1);相当于:
P()
↓
申请资源
↓
信号量 1 → 0
↓
进入临界区如果另一个进程同时执行 P:
信号量 = 0
↓
无法继续
↓
阻塞等待信号量不会真的变成 -1。
4. V 操作:离开临界区
struct sembuf v_op = {0, 1, SEM_UNDO};
semop(sem_id, &v_op, 1);相当于:
V()
↓
释放资源
↓
信号量 0 → 1
↓
唤醒等待的进程5. 临界区
本例核心代码:
sem_p(sem_id);
printf("%c", ch);
fflush(stdout);
sleep(rand() % 3);
printf("%c", ch);
fflush(stdout);
sem_v(sem_id);其中:
P()
↓
┌─────────────────┐
│ 临界区 │
│ printf() │
│ sleep() │
│ printf() │
└─────────────────┘
↓
V()P 和 V 之间就是临界区。
6. 为什么会成对输出
假设父进程先获得信号量:
信号量:1 → 0父进程进入临界区:
a
sleep()
a此时子进程执行:
sem_p(sem_id);但信号量已经是 0:
P()
↓
阻塞等待父进程执行:
sem_v(sem_id);信号量:
0 → 1子进程被唤醒:
b
sleep()
b所以最终会看到:
aabb
bbaa
aabb具体谁先执行不确定,但:
一个进程的两次打印不会被另一个进程插入。
7. 进程互斥的标准模式
sem_p(sem_id); // 获取资源
// 临界区
// 访问共享资源
sem_v(sem_id); // 释放资源就是:
P → 加锁
V → 解锁8. 同步和互斥的区别
进程同步:
信号量初值 = 0
解决“谁先执行”
进程互斥:
信号量初值 = 1
解决“谁能进入”本质机制相同:
semop()
↓
P / V
↓
阻塞 / 唤醒只是使用目的不同。
9. 一句话记忆
进程互斥 = 信号量初值为 1,进入临界区前 P,离开临界区后 V,保证同一时间只有一个进程访问共享资源。
核心 API:
semget() → 创建/获取信号量
semctl() → 初始化/删除
semop() → 执行 P/V完整代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>
int sem_id;
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;
};
int init_sem(int semid, int init_value){
union semun sem_union;
sem_union.val = init_value;
if(semctl(semid, 0, SETVAL, sem_union)==-1){
perror("Initialize semaphore");
return -1;
}
return 0;
}
int del_sem(int semid){
union semun sem_union;
if(semctl(semid, 0, IPC_RMID, sem_union)==-1){
perror("Delete semaphore");
return -1;
}
return 0;
}
int sem_p(int semid){
struct sembuf p_op = {0, -1, SEM_UNDO};
if(semop(semid, &p_op, 1)==-1){
perror("P operation");
return -1;
}
return 0;
}
int sem_v(int semid){
struct sembuf v_op = {0, 1, SEM_UNDO};
if(semop(semid, &v_op, 1)==-1){
perror("V operation");
return -1;
}
return 0;
}
void print(char ch){
srand(getpid());
int i;
for(i=0; i<10; i++){
sem_p(sem_id);
printf("%c", ch);
fflush(stdout);
sleep(rand()%3);
printf("%c", ch);
fflush(stdout);
sem_v(sem_id);
sleep(rand()%2);
}
}
int main(){
sem_id = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT);
init_sem(sem_id, 1);
pid_t pid = fork();
if(pid>0){
print('a');
wait(NULL);
del_sem(sem_id);
}
else if(pid==0){
print('b');
}
exit(0);
}
/***
可以看到输出都是成对出现的字符。
分析:信号量初始为1。假设父进程先被调度执行,父进程执行P操作,此时信号量为0。
子进程在父进程睡眠期间被调度的时候尝试P操作,信号量为-1,子进程阻塞。
父进程打印完毕,执行V操作,信号量为0,唤醒子进程。
子进程的P操作返回,打印字符睡眠后再执行V操作,信号量为1。
当然,在子进程睡眠的时候父进程可能也在尝试P操作,故就一直循环往复下去。
*/
