进程:创建、终止

进程:创建、终止

1. 进程创建基础 (fork 函数)

1.1 函数接口与头文件

  • 函数名称: fork
  • 所需头文件:
    • #include <sys/types.h>
    • #include <unistd.h>
  • 函数原型: pid_t fork(void);
  • 参数: 无参数。
  • 返回值: pid_t (进程 ID 类型,通常为整型)。
    • 父进程 (Parent Process): 返回子进程的 PID (大于 ​0)。
    • 子进程 (Child Process): 返回 ​0
    • 错误: 返回 ​-1

1.2 基本执行逻辑

  • 调用 fork() 后,系统会创建一个新的进程(子进程)。
  • 代码执行点: fork() 之后的代码会被两个进程(父进程和子进程)同时执行。
  • 示例代码:
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main() {
        printf("Hello\n");
        fork();
        printf("Hello\n");
        return 0;
    }
    
  • 现象:
    • fork() 之前的 printf 执行一次。
    • fork() 之后的 printf 执行两次(因为有两个进程)。
    • 输出顺序不确定: 由于进程调度,父进程、子进程和终端 Shell (Bash) 可能交错输出。例如,可能先打印父进程内容,然后 Bash 打印提示符,最后子进程打印内容。

1.3 进程 ID (PID) 与返回值判断

  • 通过判断 fork() 的返回值可以区分当前代码是在父进程还是子进程中运行。
  • 典型用法:
    pid_t pid = fork();
    if (pid < 0) {
        // 错误处理
        perror("fork error");
        return -1;
    } else if (pid > 0) {
        // 父进程逻辑 (pid 为子进程的 PID)
        printf("Parent PID: %d\n", pid);
    } else if (pid == 0) {
        // 子进程逻辑
        printf("Child PID: 0\n");
    }
    
  • 注意: 在多进程环境下,共享终端输出时,内容可能会 interleaved (交错)。

2. 多次调用 fork 与进程结构控制

2.1 循环中多次 fork 的问题

  • 如果在循环中直接调用 fork(),进程数量会呈指数级增长。
  • 示例:
    for (int i = 0; i < 4; i++) {
        fork();
    }
    
  • 结果: 并非只创建 4 个进程。因为子进程创建后也会继续执行循环,导致进程数量爆炸(理论上接近 ​2^n 级别,具体取决于循环逻辑)。
  • 验证方法:
    • 使用 ps -ef | grep <进程名> 查看进程树。
    • 使用 pstree -p <PID> 查看进程层级关系。
    • 使用 kill -9 <PID> 清理残留进程。

2.2 构建进程链 (Process Chain)

  • 需求: 父进程生子进程,子进程生孙进程,形成线性链条 (Parent -> Child -> Grandchild)。
  • 实现逻辑: 父进程创建子进程后,父进程退出循环,只有子进程继续循环
  • 代码关键:
    for (int i = 0; i < 4; i++) {
        pid_t pid = fork();
        if (pid > 0) {
            // 父进程:跳出循环,不再创建新进程
            break; 
        }
        // 子进程:继续循环,创建下一代
    }
    
  • 效果: 每次循环只有当前最新的子进程会继续执行 fork,形成单链结构。

2.3 父进程创建多个子进程 (One-to-Many)

  • 需求: 一个父进程直接创建多个子进程,子进程不再创建后代 (星型结构)。
  • 实现逻辑: 子进程创建后,子进程退出循环,只有父进程继续循环
  • 代码关键:
    for (int i = 0; i < 4; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            // 子进程:跳出循环,不再创建新进程
            break; 
        }
        // 父进程:继续循环,创建下一个子进程
    }
    
  • 效果: 父进程循环 4 次,创建 4 个独立的子进程,所有子进程的父进程 ID (PPID) 相同。

3. 进程终止方式 (return, exit, _exit)

3.1 main 函数中的 return

  • main 函数中,return 0; 等同于调用 exit(0);
  • 底层逻辑是调用 exit 函数来结束进程。
  • 返回值检查: 在 Shell 中可通过 echo $? 查看上一个进程的退出状态码。

3.2 exit 函数 (标准库函数)

  • 头文件: #include <stdlib.h>
  • 性质: C 标准库函数 (POSIX 标准)。
  • 功能:
    1. 刷新缓冲区: 会刷新 stdio 流的缓冲区 (Buffer Flushing)。
    2. 终止进程: 调用底层系统调用结束进程。
  • 示例现象:
    • 若代码中有 printf("Hello") (无 \n),数据留在缓冲区。
    • 调用 fork() 后,父子进程各自拥有一份缓冲区副本。
    • 若父子进程都调用 exit(),缓冲区会被刷新两次,导致 "Hello" 打印两次。

3.3 _exit 函数 (系统调用)

  • 头文件: #include <unistd.h>
  • 性质: 系统调用 (System Call)。
  • 功能:
    1. 不刷新缓冲区: 直接终止进程,遗留缓冲区数据。
    2. 终止进程: 由内核直接结束进程。
  • 示例现象:
    • 若子进程中调用 _exit(0),子进程缓冲区不刷新。
    • 只有父进程正常结束时刷新缓冲区,"Hello" 可能只打印一次。
  • 区别总结:
    • exit: 库函数,刷新缓冲区,底层调用 _exit
    • _exit: 系统调用,不刷新缓冲区,直接终止。

3.4 退出状态码的限制

  • 有效位数: 进程退出状态码只有低 8 位 有效 (0-255)。
  • 现象:
    • exit(1000),Shell 获取到的值可能是 1000 % 256 的结果。
    • 例如 exit(256) 可能返回 ​0exit(257) 可能返回 ​1
  • 建议: 返回值应在 ​0-255 范围内,通常 ​0 表示成功,非 ​0 表示错误。

4. 异常终止与信号

4.1 正常终止 vs 异常终止

  • 正常终止: 通过 return, exit, _exit 主动结束。
  • 异常终止: 通过信号 (Signal) 强制结束。
    • 例如:在终端按 Ctrl + C 发送 SIGINT 信号。
  • 返回值差异:
    • 正常退出返回设定的状态码 (如 ​0)。
    • 信号终止返回的状态码通常大于 ​128 (例如 128 + 信号值),表示非正常退出。

4.2 父进程管理子进程

  • 父进程需要知道子进程是如何结束的 (状态码、是否异常)。
  • 这涉及到进程等待与状态收集 (Wait/Waitpid),将在后续课程中探讨。

5. 实验与调试技巧

5.1 进程查看命令

  • ps -ef | grep <进程名>: 查找特定进程。
  • pstree -p <PID>: 以树状图显示进程层级及 PID。
  • ps -o pid,ppid,cmd: 查看进程 ID、父进程 ID 和命令。

5.2 后台运行与清理

  • 后台运行: 编译运行后按 Ctrl + Z 暂停,再输入 bg 让进程在后台运行,避免阻塞终端。
  • 清理进程:
    • 使用 kill -9 <PID> 强制杀死进程。
    • 注意清理父进程及其所有子进程,避免僵尸进程或残留进程干扰后续实验。

5.3 缓冲区实验细节

  • 代码示例:
    printf("Hello"); // 无换行符,数据在缓冲区
    fork();
    exit(0); // 父子进程都会刷新缓冲区,输出两次 Hello
    
  • 修改为 _exit:
    printf("Hello");
    fork();
    if (pid == 0) {
        _exit(0); // 子进程不刷新缓冲区
    }
    // 只有父进程最后退出时刷新,输出一次 Hello
    
  • 睡眠测试: 若子进程 sleep(10) 后再 _exit,可观察到父进程先结束输出,10 秒后子进程结束但无额外输出(因为 _exit 不刷新)。

6. 总结

  1. fork 返回值判断: 必须严格判断 pid > 0, pid == 0, pid < 0 三种情况。
  2. 循环 fork 控制: 务必使用 break 控制父子进程的循环行为,否则会导致进程数量失控。
  3. 终止函数选择:
    • 一般情况使用 exitreturn,确保缓冲区数据写入。
    • 特殊场景(如守护进程、避免重复输出)可使用 _exit
  4. 资源清理: 实验结束后务必使用 kill 清理残留进程,保持环境干净。
  5. 状态码规范: 遵循 ​0-255 的返回值规范,便于 Shell 脚本判断执行结果。
进程、程序 2026-03-04

评论区