六十天Linux从0到项目搭建(第十一天)(阻塞、挂起、进程状态、退出码)

阻塞(Blocking)


1. 阻塞的定义

阻塞 是指进程因等待某种资源(如磁盘I/O、网络数据、锁等)暂时无法继续执行,从而进入“暂停”状态,直到资源就绪后被唤醒。

  • 核心特点

    • 进程 主动放弃CPU(不再被调度)。

    • 一定是因为 需要等待资源(如数据未到达、设备忙)。


2. 阻塞的底层原理

(1) 进程如何被阻塞?

  • 步骤

    1. 进程请求资源(如 read() 读取磁盘数据)。

    2. 若资源未就绪(如磁盘忙),OS 将进程的 PCB(task_struct)加入等待队列

    3. 进程状态改为 TASK_INTERRUPTIBLE(可中断睡眠)或 TASK_UNINTERRUPTIBLE(不可中断睡眠)。

    4. 调度器选择其他就绪进程运行,当前进程 暂停执行

  • 关键数据结构

    // 设备驱动中的等待队列(Linux内核示例)
    struct device {
        struct task_struct *wait_queue;  // 阻塞在该设备上的进程队列
        // 其他设备属性...
    };

(2) 阻塞的代码表现

// 用户态调用 read() 触发阻塞(底层通过系统调用)
char buf[100];
int fd = open("file.txt", O_RDONLY);
ssize_t n = read(fd, buf, 100);  // 若数据未就绪,进程阻塞在此!

3. 阻塞的管理模型

(1) 先描述,再组织

  • 描述:用 task_struct 表示一个进程,记录其状态、资源需求等。

  • 组织:将PCB放入 不同的队列 中管理:

    • 就绪队列:等待CPU调度的进程。

    • 阻塞队列:等待特定资源的进程(如磁盘、网卡各自的等待队列)。

(2) 资源与进程的关联

  • 每个资源(如磁盘、网卡)维护一个等待队列

    struct disk_device {
        struct list_head wait_queue;  // 阻塞在该磁盘上的进程链表
        // 磁盘的其他属性...
    };
  • 当资源就绪时(如磁盘数据读取完成):

    1. 从队列中唤醒一个/所有进程。

    2. 将进程PCB移回 就绪队列


现实类比

  • 场景:你去银行柜台办业务,但柜员正在处理前一个客户。

    • 你(进程):因“柜员忙”这一资源不可用,主动排队阻塞

    • 叫号机(OS):将你的号码(PCB)加入等待队列。

    • 柜员(资源):完成后叫下一个号(唤醒进程)。


4. 为什么需要阻塞?

原因 示例 解决方案
资源竞争 多个进程争用同一磁盘 阻塞队列让进程有序等待。
避免忙等待 进程不断轮询“数据到了吗?”浪费CPU 阻塞释放CPU给其他进程。
依赖外部事件 网络包未到达、用户输入未完成 进程休眠直到事件触发。

5. 阻塞 vs 非阻塞 vs 异步

模式 行为 典型接口
阻塞 进程暂停,直到资源就绪。 read()write()
非阻塞 立即返回,若资源未就绪则报错。 O_NONBLOCK + read()
异步 进程继续执行,资源就绪后通过回调通知。 aio_read()epoll

总结

  1. 阻塞的本质:进程因等待资源 主动让出CPU,进入等待队列。

  2. 管理方式

    • 描述:用 task_struct 表示进程。

    • 组织:将PCB放入资源对应的阻塞队列。

  3. 设计意义

    • 避免忙等待,提高CPU利用率。

    • 实现资源的有序共享。

挂起(Suspend)详解:进程的“深度冻结”


挂起的定义

挂起 是指操作系统 主动将进程从内存移到磁盘(交换区),以释放内存资源。与阻塞不同,挂起是 由OS强制触发 的,而非进程主动等待资源。

  • 核心特点

    • 进程的 代码和数据被换出到磁盘,仅保留PCB在内存。

    • 进程状态标记为 TASK_STOPPED 或 TASK_TRACED

    • 恢复时需重新加载到内存,导致较大延迟。


挂起 vs 阻塞

特性 挂起(Suspend) 阻塞(Block)
触发方 操作系统(强制) 进程自身(主动)
内存状态 代码和数据被换出到磁盘 代码和数据保留在内存
恢复条件 OS需要内存或手动唤醒 等待的资源就绪(如I/O完成)
延迟 高(需从磁盘 reload) 低(内存中直接恢复)

挂起的底层原理

1. 为什么需要挂起?

  • 内存不足:当物理内存紧张时,OS将不活跃的进程挂起,腾出空间。

  • 系统负载均衡:避免过多进程竞争CPU和内存。

  • 用户控制:用户可手动挂起进程(如 Ctrl+Z 触发 SIGTSTP 信号)。

2. 挂起的管理流程

  1. 选择目标进程

    • OS根据算法(如LRU)选择不活跃的进程。

  2. 换出到磁盘

    • 将进程的代码、数据、堆栈写入 交换分区(Swap)

  3. 保留PCB

    • 仅保留 task_struct 在内存,记录进程状态和磁盘位置。

  4. 恢复时换入

    • 重新加载代码和数据到内存,继续执行。

3. 代码示例(手动挂起)

# 在终端挂起进程(Ctrl+Z发送SIGTSTP)
$ sleep 100
^Z  # 按下Ctrl+Z
[1]+  Stopped                 sleep 100

# 查看挂起进程
$ jobs
[1]+  Stopped                 sleep 100

# 恢复挂起进程(前台继续)
$ fg %1

现实类比

  • 阻塞:像在餐厅排队等位(你还在现场,只是暂时不行动)。

  • 挂起:像餐厅满员时,服务员让你先去逛街,有空位再叫你(你被“请出”现场)。


挂起的应用场景

  1. 内存优化

    • 服务器在高负载时挂起低优先级进程。

  2. 进程调试

    • 开发者挂起进程检查状态(如 gdb 调试)。

  3. 交互式控制

    • 用户暂停后台任务(如 vim 中按 Ctrl+Z 切到shell)。


总结

  1. 挂起是OS的“资源调控”手段:通过换出进程缓解内存压力。

  2. 与阻塞的关键区别

    • 阻塞是进程 主动等待,挂起是OS 强制腾挪

  3. 恢复开销大:需从磁盘重新加载,性能敏感场景慎用

进程状态与 task_struct 深度解析


1. task_struct 中的状态管理

Linux 内核用 task_struct 结构体描述进程,其 status 字段(如 volatile long state)标识进程状态。关键状态包括:

状态 符号 含义 是否占用CPU
运行 TASK_RUNNING (R) 进程 在运行队列中,可能正在CPU执行或等待调度。 ❌(可能在排队)
可中断睡眠 TASK_INTERRUPTIBLE (S) 进程 阻塞,等待资源(如I/O),可被信号唤醒。
不可中断睡眠 TASK_UNINTERRUPTIBLE (D) 进程 阻塞,等待关键资源(如磁盘写入),不可被信号唤醒
暂停 TASK_STOPPED (T) 进程被调试器暂停(如 gdb 断点)或收到 SIGSTOP 信号。
僵尸 EXIT_ZOMBIE (Z) 进程已终止,但父进程未回收其资源(PCB残留)。
死亡 EXIT_DEAD (X) 进程最终状态,资源已完全释放。

关键问题:R 状态 ≠ 正在运行!

  • 误解R 状态常被误认为“正在CPU上执行”。

  • 真相

    • R 状态表示进程 在运行队列中,可能处于两种子状态:

      1. 正在CPU上运行(实际占用CPU)。

      2. 就绪等待调度(在运行队列中排队)。

    • 验证方法

      top -p   # 查看进程的 `%CPU` 使用率

2. 为什么创建进程?——关注结果 vs 不关注结果

(1) 需要获取结果

#include 
#include 
#include 

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程执行任务
        sleep(2);
        return 42;  // 退出码
    } else {
        // 父进程等待并获取结果
        int status;
        waitpid(pid, &status, 0);  // 阻塞等待
        printf("Child exit code: %d\n", WEXITSTATUS(status));  // 输出 42
    }
    return 0;
}
  • 用途:父进程通过 wait() 获取子进程退出码(如脚本执行状态码)。

(2) 不关心结果(异步任务)

int main() {
    if (fork() == 0) {
        // 子进程独立执行(如后台服务)
        daemon(0, 0);  // 脱离终端
        while (1) { /* 长期运行 */ }
    }
    return 0;  // 父进程直接退出
}
  • 用途:启动守护进程(如Web服务器)、忽略子进程退出状态。


3. 进程退出码(Exit Code)

  • 作用:进程通过退出码向父进程报告执行结果(0 表示成功,非 0 表示错误)。

  • 获取方法

    • Shell 中通过 $? 获取上一条命令的退出码:

      ./a.out
      echo $?  # 输出 main() 的 return 值
    • 父进程通过 wait() 系统调用获取(见上例)。

常见退出码约定

退出码 含义
0 成功
1 通用错误
2 命令行参数错误
127 命令未找到

现实类比

  • R 状态:像在超市收银台排队,你可能正在结账(占用CPU),也可能只是站在队列里(等待调度)。

  • 退出码:像员工完成任务后提交的报告编号(0=顺利完工,1=遇到问题)。


总结

  1. R 状态是“就绪”而非“运行”:需结合 %CPU 判断是否实际占用CPU。

  2. 创建进程的两种目的

    • 同步任务:父进程等待结果(如 wait())。

    • 异步任务:父进程不关心结果(如后台服务)。

  3. 退出码是进程的“遗言”:通过 return 或 exit() 传递,父进程/Shell 可捕获

你可能感兴趣的:(Linux,linux)