解析死锁最小安全资源公式 n(m-1)+1
1. 文章概述
本文完全抛弃伪代码,基于标准 Linux System V 信号量 API(ftok、semget、semctl、semop),通过多进程并发实验,真机复现死锁与安全状态,从底层代码层面彻底讲透操作系统经典公式:
不死锁最小资源数 = n(m-1) + 1
本文所有代码可直接编译、运行、复现现象,适配操作系统课程、期末实验、面试原理题。
2. 公式严格定义(单资源模型专属)
适用场景:系统只有一种同质资源、多个进程竞争。
-
n:并发进程数
-
m:单个进程运行完成需要的最大资源数
-
n(m-1):临界危险资源数(最大可死锁资源)
-
n(m-1)+1:系统绝对安全的最小资源数
核心原理(代码级真相)
死锁的唯一成因:所有进程全部拿到 m-1 个资源,系统资源耗尽,全部卡在第二次 P 操作,无任何进程可以 V 释放资源,形成永久阻塞。
只要多 1 个资源:必有一个进程凑满 m 个资源、跑完、释放资源、盘活全局。
3. 实验环境与参数设定
-
进程数 n = 3
-
单进程最大资源需求 m = 2
-
死锁临界资源:3×(2-1) = 3
-
安全最小资源:3×(2-1)+1 = 4
信号量初值 = 系统总资源数
4. 基础封装(标准 System V PV 操作)
基于你所学原生 API,封装标准 P、V,无任何自定义语法。
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/sem.h>
#include <errno.h>
// IPC Key 生成参数
#define SEM_PATH "/home/china"
#define PROJ_ID 100
#define SEM_NUM 1 // 只用1个信号量:代表【单一同类资源】
/**
* P操作:申请资源 sem-1
* 资源不足时,进程阻塞
*/
void sem_p(int semid)
{
struct sembuf op;
op.sem_num = 0;
op.sem_op = -1; // P: 占用1个资源
op.sem_flg = 0; // 阻塞式等待
semop(semid, &op, 1);
}
/**
* V操作:释放资源 sem+1
* 唤醒等待该资源的进程
*/
void sem_v(int semid)
{
struct sembuf op;
op.sem_num = 0;
op.sem_op = 1; // V: 归还1个资源
op.sem_flg = 0;
semop(semid, &op, 1);
}
5. 子进程统一业务逻辑(真实进程行为)
每个进程必须拿到 2 个资源 才能结束,否则阻塞。
/**
* 子进程工作逻辑
* 固定需求:需要2个资源才能运行完成并释放资源
*/
void proc_work(int semid, int no)
{
int hold = 0;
// 第一步:先拿 m-1 = 1 个资源(最坏抢占状态)
printf("进程%d:获取第1个资源\n", no);
sem_p(semid);
hold = 1;
printf("进程%d:持有1个资源,缺1个资源完成,等待资源...\n", no);
// 第二步:尝试获取最后1个资源
// 死锁场景:全部进程卡死在此行
sem_p(semid);
hold = 2;
// 成功集齐资源,运行任务
printf("进程%d:资源齐全,开始运行任务\n", no);
sleep(1);
// 运行结束:释放全部持有的资源
sem_v(semid);
sem_v(semid);
printf("进程%d:任务结束,释放全部资源,退出\n", no);
_exit(0);
}
6. 场景一:资源数 = n(m-1) = 3(死锁场景)
信号量初值设为 3,完全命中死锁临界条件。
int main()
{
// 1. 生成IPC key
key_t key = ftok(SEM_PATH, PROJ_ID);
if(key == -1){
perror("ftok");
return -1;
}
// 2. 创建信号量集
int semid = semget(key, SEM_NUM, IPC_CREAT | IPC_EXCL | 0777);
if(semid == -1){
if(errno == EEXIST){
printf("信号量已存在,复用旧信号量\n");
semid = semget(key, 0, 0);
}else{
perror("semget");
return -1;
}
}
// 3. 设置资源总数 = 3 【n(m-1) 必死锁临界值】
unsigned short init_val[SEM_NUM] = {3};
semctl(semid, 0, SETALL, init_val);
printf("系统总资源:3 (死锁临界状态)\n\n");
// 4. 创建3个并发进程 n=3
for(int i = 0; i < 3; i++){
if(fork() == 0){
proc_work(semid, i);
}
}
// 主进程永久等待,卡死
for(int i = 0; i < 3; i++){
wait(NULL);
}
semctl(semid, 0, IPC_RMID);
return 0;
}
死锁现象(可复现)
-
3 个进程全部成功拿到 1 个资源
-
系统资源变为 0
-
所有进程卡在第二个
sem_p -
没有任何进程能释放资源
-
主进程卡死,程序永久挂起 → 标准死锁
7. 场景二:资源数 = n(m-1)+1 = 4(安全无死锁)
仅修改一行资源初值,其余代码完全不变。
// 安全最小资源:多出1个冗余资源打破死锁
unsigned short init_val[SEM_NUM] = {4};
安全运行流程
-
3 个进程各抢占 1 个资源,消耗 3 个
-
系统剩余 1 个空闲资源
-
系统随机一个进程获取最后资源,凑满 2 个
-
该进程运行结束,释放 2 个资源
-
剩余阻塞进程被唤醒,依次执行、依次退出
-
主进程正常回收所有子进程,程序完美退出
8. 公式代码级终极结论(入行必背)
|
资源数量 |
代码现象 |
系统状态 |
|---|---|---|
|
n(m-1) |
所有进程卡在第二次P操作,无V执行 |
必然死锁 |
|
n(m-1)+1 |
存在进程可完成、可释放资源、链式唤醒 |
绝对安全 |
9. 关键边界总结(高频坑点)
-
该公式只适用于单一同种资源,多资源不能用;
-
公式是靠资源总量兜底防死锁,和资源有序分配、银行家算法无关;
-
死锁的本质代码特征:全部进程P阻塞、无任何V释放;
-
+1 的意义:强行制造一个可跑完的进程,打破闭环等待。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)