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};

安全运行流程

  1. 3 个进程各抢占 1 个资源,消耗 3 个

  2. 系统剩余 1 个空闲资源

  3. 系统随机一个进程获取最后资源,凑满 2 个

  4. 该进程运行结束,释放 2 个资源

  5. 剩余阻塞进程被唤醒,依次执行、依次退出

  6. 主进程正常回收所有子进程,程序完美退出

8. 公式代码级终极结论(入行必背)

资源数量

代码现象

系统状态

n(m-1)

所有进程卡在第二次P操作,无V执行

必然死锁

n(m-1)+1

存在进程可完成、可释放资源、链式唤醒

绝对安全

9. 关键边界总结(高频坑点)

  • 该公式只适用于单一同种资源,多资源不能用;

  • 公式是靠资源总量兜底防死锁,和资源有序分配、银行家算法无关;

  • 死锁的本质代码特征:全部进程P阻塞、无任何V释放;

  • +1 的意义:强行制造一个可跑完的进程,打破闭环等待。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐