Linux进程调度与等待:背后的机制与实现

个人主页:chian-ocean

文章专栏-Linux

前言:

当一个进程发起某种操作(如I/O请求、信号、锁的获取等),但该操作需要的资源暂时不可用时,进程会被操作系统挂起,进入"等待队列"或"阻塞状态"。在此期间,进程不占用CPU,但仍保留其内存、文件描述符等资源

进程等待的必要性

僵尸进程的存在

僵尸进程的成因

  • 当子进程终止后,它的退出状态需要由父进程通过调用 wait()waitpid() 系统调用回收。
  • 如果父进程未回收子进程的退出状态,子进程会以"僵尸进程"的形式保留在进程表中。

特征:

  • 在 Linux 系统中,可以用 ps 命令查看,僵尸进程的状态为 Z(Zombie)。
  • 僵尸进程是操作系统保留的一个条目,主要用于父进程检查子进程的退出状态。

如下:

从图片中可以看到一个典型的 僵尸进程 的现象:

  • 进程 27864 被强制终止(kill -9 27864),但它的父进程(27863)没有调用 wait()waitpid() 来回收其子进程的退出状态。
  • 因此,27864 被标记为 <defunct> 状态,即僵尸进程。
  • ps 输出的 STAT 列中显示 Z+,这是僵尸进程的状态标识。

进程等待

进程等待是操作系统中一种重要的状态,指的是某个进程由于资源不足或条件未满足,暂时无法继续执行而被挂起的现象。

  • 使用 wait()waitpid() 回收子进程

wait ( )

参数:

c 复制代码
int *status:
  • 用于保存子进程的状态信息(如退出码或终止信号)。
  • 如果不需要获取子进程状态,可以将其传入 NULL

返回值:

  • 成功:
    • 返回已终止的子进程的 PID。
  • 失败:
    • 返回 -1,并设置 errno
    • 常见错误包括:
      • ECHILD:当前进程没有子进程。
      • EINTR:调用被信号中断。

wait() 的作用

  1. 阻塞父进程:
    • wait() 会阻塞父进程,直到任意一个子进程状态发生变化(通常是终止)。
  2. 回收子进程资源:
    • 子进程终止后,其资源仍然保留在系统中,直到父进程调用 wait()waitpid() 回收它。
    • 如果父进程不调用 wait()waitpid(),子进程会变成 僵尸进程

示例:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

void childtast()
{
    for(int i = 0; i < 10; i++) // 循环打印从 0 到 9 的数字
    {
        cout << i << endl; // 输出当前的循环变量 i
    }
    sleep(3); // 睡眠 3 秒,模拟子进程的运行延迟
}

int main()
{
    pid_t id = fork(); // 创建子进程
    cout << "id" << ":" << id << endl;

    if(id == 0) // 判断是否是子进程
    {
        sleep(3); // 子进程先睡眠 3 秒
        childtast(); // 子进程调用 childtast(),打印数字并睡眠
    }

    // 父进程等待任意一个子进程终止
    pid_t ret = wait(NULL); // 父进程调用 wait(),阻塞等待子进程终止
    if(ret == id) // 判断 wait() 返回的进程 ID 是否是创建的子进程 ID
    {
        cout << "ret" << ":" << ret << endl; // 输出子进程的 ID
        cout << "wait success" << endl; // 输出等待成功的消息
    }

    sleep(3); // 父进程再睡眠 3 秒,模拟延迟
    return 0;
}

fork() 创建子进程

  • 父进程和子进程同时运行。
  • 父进程的 id 是子进程的 PID,子进程的 id 是 0。

子进程的任务

  • 子进程先睡眠 3 秒,然后执行 childtast(),打印 09

父进程的等待

  • 父进程调用 wait(NULL),阻塞自身,直到子进程终止。
  • 当子进程完成任务并退出后,wait() 返回子进程的 PID。

父进程的后续操作

  • 父进程输出子进程的PID和等待成功的消息。
  • 父进程再睡眠 3 秒后退出。

waitpid ( )

waitpid()wait() 的增强版本,提供了更灵活的功能,允许父进程:

  1. 等待特定的子进程。
  2. 非阻塞等待子进程。
  3. 获取子进程的状态(如退出状态或被信号终止)。
c 复制代码
pid_t waitpid(pid_t pid, int *status, int options);

参数说明

  • pid

    • pid > 0:等待特定的子进程(指定的 PID)。

    • pid == 0:等待与当前进程同一个进程组的任意子进程。

    • pid < -1:等待进程组 ID 为 |pid| 的任意子进程。

      c 复制代码
      wait(NULL) //等价于 waitpid(-1,NULL,0); 
    • pid == -1:等效于 wait(),等待任意子进程。

status 字段的结构

status

  • 指向一个整数的指针,用于存储子进程的状态信息(退出状态、信号等)。
  • 若不关心状态信息,可将其设为 NULL

在 Linux 系统中,status 是一个整数,表示子进程状态的多种可能性,底层通过位字段表示:

位字段 含义
位 0-7 子进程退出的信号或退出码(低 8 位)。
位 8-15 退出状态(高 8 位,存储正常退出码)。
位 16-23 暂停信号编号。

代码解析字段

cpp 复制代码
#include<iostream> 
#include<unistd.h> 
#include<sys/types.h> 
#include<sys/wait.h> 
using namespace std;
int main()
{    
    pid_t id = fork();    
        
    cout << "id" << ":" << id <<endl;    
    if(id == 0)    
    {    
        sleep(3);    
        exit(1);       
    }    
    int status;    
    pid_t ret = waitpid(-1,&status,0);                                     
    if(ret == id)    
    {
        cout << "ret" << ":" << ret <<endl;    
        cout<< "wait success" <<endl;    
    }    

    cout <<"status :" << status << endl;
    cout << "退出码" << ((status >> 8)& 0xff ) <<" "<< "信号码" << (status & 0x7f)<< endl;
    return 0;
}
完整运行流程

fork() 创建子进程

  • 父进程创建子进程,并返回子进程的 PID。

子进程逻辑

  • 子进程休眠 3 秒后正常退出,退出码为 1

父进程逻辑

  • 父进程调用 waitpid() 阻塞等待子进程终止。
  • 获取子进程的状态信息,并解析退出码和信号码。

父进程输出状态信息

  • 输出子进程的 PID、状态值、退出码和信号码。

解析逻辑

  • 退出码:

    复制代码
    (status >> 8) & 0xff
    • 获取高 8 位的退出码。
  • 信号码:

    复制代码
    status & 0x7f
    • 获取低 7 位的信号码.

    示例1:进程正常退出的退出码。

    示例2:提取被9号信号杀死的进程信号码

    cpp 复制代码
    id:1667          // 父进程输出,子进程的 PID 是 1667
    id:0             // 子进程输出,表明当前是子进程
    ret:1667         // 父进程成功等待到子进程结束,返回子进程 PID
    wait success     // 父进程确认子进程终止
    status :9        // 父进程获取子进程状态值为 9
    退出码0 信号码9   // 父进程解析状态值:
                      // - 退出码 0:子进程未通过 exit() 返回退出码
                      // - 信号码 9:子进程被 SIGKILL 信号终止
    库中提供的宏替换
    解析退出码和信号编号
    • WIFEXITED(status)
      • 如果为真,表示子进程正常退出,其退出码存储在高 8 位。
      • 使用 (status >> 8) & 0xff 提取退出码。
    • WEXITSTATUS(status)
      • 获取退出码的宏,等价于 (status >> 8) & 0xff
      • 必须确保 WIFEXITED(status) 为真后使用。
解析退出码和信号编号
  • WIFEXITED(status)
    • 如果为真,表示子进程正常退出,其退出码存储在高 8 位。
    • 使用 (status >> 8) & 0xff 提取退出码。
  • WEXITSTATUS(status) :== status & 0x7f
    • 获取退出码的宏,
    • 必须确保 WIFEXITED(status) 为真后使用。

options参数介绍

阻塞与非阻塞
特性 阻塞 非阻塞
进程状态 等待资源时挂起,无法执行其他任务。 立即返回,不会挂起,进程可执行其他任务。
适用场景 简单任务、对实时性要求不高的任务。 多任务并发、实时性要求高的任务。
复杂性 实现简单,逻辑清晰。 逻辑复杂,需要轮询或回调处理资源状态。
CPU 使用 不浪费 CPU 资源,进程处于挂起状态。 需要轮询资源状态,可能增加 CPU 占用。
资源管理 等待资源的管理交由操作系统处理。 需要程序主动检查资源状态,增加开发复杂度。

options

  • 用于指定额外的选项:
    • 0:阻塞等待。
    • WNOHANG:非阻塞等待。
    • WUNTRACED:返回暂停的子进程状态(子进程因 SIGSTOP 信号暂停)。
    • WCONTINUED:返回恢复运行的子进程状态(子进程因 SIGCONT 信号继续运行)。
WNOHANG
  • 非阻塞模式:
    • 如果没有子进程终止,waitpid() 会立即返回,而不是阻塞父进程。
  • 返回值:
    • 如果有子进程状态变化,则返回子进程的 PID。
    • 如果没有子进程状态变化,则返回 0
非阻塞轮询
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <chrono>
#include <thread>
using namespace std;
int main() {
    pid_t pid = fork(); // 创建子进程

    if (pid == 0) {
        // 子进程逻辑
        cout << "Child process running..." << endl;
        sleep(5); // 模拟子进程任务,延迟 5 秒
        cout << "Child process exiting..." << endl;
        exit(42); // 子进程以退出码 42 正常退出
    } else if (pid > 0) {
        // 父进程逻辑
        int status;
        while (true) {
            pid_t ret = waitpid(-1, &status, WNOHANG); // 非阻塞检查子进程状态
            if (ret == 0) {
                // 子进程尚未终止,父进程继续其他工作
                cout << "Child process still running. Parent doing other work..." << endl;
                this_thread::sleep_for(chrono::seconds(1)); // 模拟父进程任务
            } else if (ret > 0) {
                // 子进程已终止,解析状态
                if (WIFEXITED(status)) {
                    cout << "Child process " << ret << " exited with code " << WEXITSTATUS(status) << endl;
                } else if (WIFSIGNALED(status)) {
                    cout << "Child process " << ret << " was terminated by signal " << WTERMSIG(status) << endl;
                }
                break; // 结束轮询
            } else {
                // waitpid 出错
                perror("waitpid failed");
                break;
            }
        }
    } else {
        // fork 失败
        perror("fork failed");
        return 1;
    }

    return 0;
}

执行结果:

多进程下的进程等待

阻塞等待多个子进程

示例代码:等待所有子进程完成

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

int main() {
    // 创建多个子进程
    for (int i = 0; i < 3; ++i) {
        pid_t pid = fork();
        if (pid == 0) {
            // 子进程
            cout << "Child " << i << " (PID: " << getpid() << ") running..." << endl;
            sleep(2 + i); // 每个子进程休眠不同时间
            cout << "Child " << i << " (PID: " << getpid() << ") exiting..." << endl;
            exit(i); // 子进程以其序号为退出码
        }
    }

    // 父进程:等待所有子进程完成
    int status;
    while (true) {
        pid_t ret = wait(&status); // 阻塞等待任意一个子进程结束
        if (ret == -1) {
            // 没有子进程可等待时退出循环
            cout << "All child processes have finished." << endl;
            break;
        
        // 解析子进程状态
        if (WIFEXITED(status)) {
            cout << "Child process " << ret << " exited with code: " << WEXITSTATUS(status) << endl;
        } else if (WIFSIGNALED(status)) {
            cout << "Child process " << ret << " was terminated by signal: " << WTERMSIG(status) << endl;
        }
    }

    return 0;
}

代码执行:

非阻塞轮询等待多个子进程

示例代码:非阻塞等待多个子进程

通过 waitpid() 配合 WNOHANG 实现父进程的非阻塞轮询,定期检查是否有子进程完成。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <chrono>
#include <thread>
using namespace std;

int main() {
    // 创建多个子进程
    for (int i = 0; i < 3; ++i) {
        pid_t pid = fork();
        if (pid == 0) {
            // 子进程
            cout << "Child " << i << " (PID: " << getpid() << ") running..." << endl;
            sleep(2 + i); // 每个子进程休眠不同时间
            cout << "Child " << i << " (PID: " << getpid() << ") exiting..." << endl;
            exit(i); // 子进程以其序号为退出码
        }
    }

    // 父进程:非阻塞轮询等待所有子进程完成
    int status;
    int completed = 0; // 已完成的子进程计数
    while (completed < 3) {
        pid_t ret = waitpid(-1, &status, WNOHANG); // 非阻塞检查子进程状态
        if (ret > 0) {
            // 有子进程状态变化
            completed++;
            if (WIFEXITED(status)) {
                cout << "Child process " << ret << " exited with code: " << WEXITSTATUS(status) << endl;
            } else if (WIFSIGNALED(status)) {
                cout << "Child process " << ret << " was terminated by signal: " << WTERMSIG(status) << endl;
            }
        } else if (ret == 0) {
            // 没有子进程状态变化,父进程继续其他工作
            cout << "No child process exited yet. Parent doing other work..." << endl;
            this_thread::sleep_for(chrono::seconds(1)); // 模拟其他任务
        } else {
            // 错误处理
            perror("waitpid failed");
            break;
        }
    }

    cout << "All child processes have finished." << endl;
    return 0;
}

代码执行:

相关推荐
大树8817 分钟前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠21 分钟前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质44 分钟前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush41 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz1 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工2 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智2 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩2 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_2 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化