一、子进程与父进程的关系

1.基本概念

在 Linux 中,fork() 系统调用会创建一个新进程(子进程),并在父子进程中分别返回不同的值:
父进程中:返回新创建的子进程的 PID(一个大于 0 的整数)。
子进程中:返回值为 0。
创建失败时:返回值为 -1,不会创建任何进程。
因此,父进程中 fork() 的返回值是子进程的 PID。

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("这是子进程,fork返回值:%d,自身PID:%d\n", pid, getpid());
    } else if (pid > 0) {
        printf("这是父进程,fork返回值:%d,自身PID:%d\n", pid, getpid());
    } else {
        printf("fork创建失败!\n");
    }
    return 0;
}

2.fork返回值

返回值 含义 对应进程
>0 新创建的子进程 PID 父进程
0 代表 “当前是子进程” 子进程
-1 创建失败(如进程数达到上限、内存不足) 无进程创建

易错点:fork() 调用一次,返回两次(父子进程各返回一次),这是它最特殊的地方。

3.核心特质

1. 资源复制与独立性
子进程会拷贝父进程的地址空间(代码段、数据段、栈、堆、文件描述符等),父子进程的内存完全独立,修改互不影响。
优化机制:写时复制(Copy-On-Write, COW)
刚创建时,父子进程共享同一份物理内存。
只有当任意一方尝试修改内存时,才会真正复制一份副本,避免不必要的开销。
2. 执行顺序
父子进程谁先执行,完全由操作系统调度器决定,顺序不确定。
想要控制顺序,需要用 wait() / waitpid() 让父进程阻塞等待子进程退出。
3. 父子进程的 PID 关系
子进程的 getppid() = 父进程的 getpid()。
父进程退出后,子进程会变成孤儿进程,被 init 进程(PID=1)收养。
子进程先退出、父进程未调用 wait() 时,子进程会变成僵尸进程(PCB 未释放,占用 PID 资源)。

n 次 fork() 后,总进程数 = 2^n(前提是所有 fork() 都成功)

4.父子进程共享的资源
✅ 共享:文件描述符表、文件偏移量(比如父子进程同时写同一个文件,会互相覆盖)
❌ 不共享:变量、栈、堆、进程状态、PID

4.常见考点

1.为什么 fork() 要返回子进程的 PID 给父进程?
父进程需要通过 PID 管理子进程(如 wait() 回收、kill() 发送信号),所以必须知道子进程的 PID。
2.僵尸进程和孤儿进程的区别?
孤儿进程:父进程先退出,子进程被 init 收养,无危害。
僵尸进程:子进程先退出,父进程未调用 wait() 回收,PCB (process control block  进程控制块)残留,占用 PID 资源,大量僵尸进程会导致系统无法创建新进程。
3.如何避免僵尸进程?
父进程调用 wait() / waitpid() 阻塞等待子进程退出。
父进程捕获 SIGCHLD 信号,在信号处理函数中调用 waitpid() 回收所有子进程。

5.核心模板代码

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程逻辑
        printf("Child process: PID=%d, PPID=%d\n", getpid(), getppid());
        sleep(2); // 模拟子进程执行
        printf("Child process exiting\n");
        return 0;
    } else {
        // 父进程逻辑
        printf("Parent process: PID=%d, Child PID=%d\n", getpid(), pid);
        // 等待子进程退出,避免僵尸进程
        int status;
        waitpid(pid, &status, 0);
        printf("Parent process: Child exited with status %d\n", WEXITSTATUS(status));
    }
    return 0;
}

写时复制:只有写操作才会触发复制,只读访问完全不需要拷贝,大幅提升 fork() 性能。
子进程刚创建时和父进程共享内存,修改时才会 “分家”,这就是 “写时复制” 的由来。

二、linux0/1/2 号进程 核心考点清单

进程 PID 名称 核心角色 父进程 主要职责
0 号进程 0 swapper/idle 内核根进程(所有进程的祖先) 无(内核创建) 1. 系统启动时创建,运行在内核态2. 初始化系统,创建 1 号和 2 号进程3. 系统空闲时作为 idle 进程调度
1 号进程 1 init/systemd 用户态进程的 “祖先” 0 号进程 1. 接管用户空间初始化,启动系统服务2. 收养所有孤儿进程(父进程退出的进程)3. 负责管理和回收僵尸进程
2 号进程 2 kthreadd 内核线程管理器 0 号进程 1. 负责创建和管理所有内核线程(如kworkerkswapd)2. 内核线程的父进程都是 2 号进程3. 内核态线程不会进入用户空间

高频考点 & 易错点


1. 进程创建关系
0 号进程是唯一的父进程:1 号和 2 号进程都是由 0 号进程创建的,二者是兄弟关系,不是父子关系。
验证方式:用 ps -ef 查看 PPID(父进程号),PID=1 和 PID=2 的 PPID 都是 0。
2. 孤儿进程 & 僵尸进程的处理
孤儿进程:父进程先退出,子进程被 1 号进程收养,1 号进程会成为它的新父进程,避免子进程成为 “无主进程”。
僵尸进程:子进程退出后,父进程未调用wait()/waitpid()回收,子进程 PCB 残留。1 号进程会定期调用wait()回收孤儿进程的僵尸状态,但用户进程的僵尸进程需要父进程主动处理。
3. 内核线程 vs 用户进程
内核线程(由 2 号进程创建):
运行在内核态,没有独立的用户地址空间,共享内核地址空间。
不执行用户代码,只执行内核函数,不能被用户直接管理。
用户进程(由 1 号进程及其后代创建):
运行在用户态,有独立的地址空间,可通过系统调用切换到内核态。
可被用户创建、管理、终止。
4. 0 号进程的特殊性
它不是普通进程,是内核在启动阶段创建的内核线程,没有用户态上下文。
当 CPU 没有任务可调度时,会切换回 0 号进程运行,也就是 “idle 状态”。


Linux 进程完整生命周期

整条链路:
创建 → 运行 → 阻塞 / 就绪 → 终止 → 资源回收

一、进程创建:fork /vfork/clone


0 号进程 最先存在,创建 1 号 (systemd)、2 号 (kthreadd)
普通用户进程:
用户进程调用 fork()
复制父进程页表、栈、数据、缓冲区
写时复制 COW,只读共享,写才拷贝
子进程:从fork 返回处继续执行,不重跑前面代码
fork 一次,两次返回
父:返回子进程 PID
子:返回 0
失败:返回 -1
补充:
vfork:子进程共享父进程地址空间,父进程阻塞
clone:Linux 底层,可定制共享资源,线程也靠它

二、进程替换:exec 系列


fork 只是复制代码,子进程和父进程跑一样的程序。
想要跑新程序 → 调用 exec
覆盖当前进程代码段、数据段、堆
保留:PID、PPID、文件描述符、进程属性
执行成功无返回,失败才返回 - 1
典型搭配:

fork() 创建子进程 → 子进程调用 exec 跑新程序

三、进程三种基本状态(必考)


就绪态
一切准备好,等 CPU 调度
运行态
正在 CPU 上执行代码
阻塞态 (等待)
等资源、等 IO、等信号、sleep
不占用 CPU,唤醒后回到就绪
状态切换:
就绪 ↔ 运行
运行 → 阻塞
阻塞 → 就绪

四、进程终止 3 种方式


正常退出
return
exit() // 库函数,刷新缓冲区、调用退出钩子
异常退出
段错误、除 0、非法指令 → 内核发信号杀死
人为终止
kill 命令 / 信号终止

五、退出后:僵尸进程 & 孤儿进程


1. 僵尸进程
子进程先退出,父进程没调用 wait/waitpid
子进程:代码、资源全部释放
只剩 PCB 进程控制块 残留,记录退出状态
危害:占用 PID,大量僵尸会导致无法新建进程
2. 孤儿进程
父进程先退出,子进程没人管
自动被 1 号进程 (systemd) 收养
无害,1 号进程会自动回收它

六、资源回收(收尾)


父进程通过:
wait()
waitpid()
获取子进程退出状态,释放 PCB,彻底消灭僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    int status;

    // 1. 创建子进程:fork()
    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // ----------------------
        // 子进程:替换程序 + 执行 + 退出
        // ----------------------
        printf("Child: PID=%d, Parent PID=%d\n", getpid(), getppid());

        // 2. exec() 替换为新程序(这里用 ls 命令举例)
        // execlp(程序名, 命令, 参数, NULL);
        execlp("ls", "ls", "-l", NULL);

        // 只有 exec 失败才会执行到这里
        perror("execlp failed");
        // 3. exit() 终止进程
        exit(EXIT_FAILURE);
    } else {
        // ----------------------
        // 父进程:等待子进程 + 回收资源
        // ----------------------
        printf("Parent: PID=%d, Child PID=%d\n", getpid(), pid);

        // 4. wait() 阻塞等待子进程退出
        wait(&status);

        // 解析子进程退出状态
        if (WIFEXITED(status)) {
            printf("Parent: Child exited normally, exit code=%d\n",
                   WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Parent: Child killed by signal %d\n",
                   WTERMSIG(status));
        }
    }

    return 0;
}

三、进程状态宏 总结表

场景 判断宏(先判断) 取值宏(再取值) 说明
子进程正常退出 WIFEXITED(wstatus)返回:非 0 表示真 WEXITSTATUS(wstatus)返回:子进程退出码 (0~255) 子进程调用 exit() / return 正常结束
子进程被信号杀死 WIFSIGNALED(wstatus)返回:非 0 表示真 WTERMSIG(wstatus)返回:终止信号编号 kill、段错误等信号强制终止
子进程被暂停 / 停止 WIFSTOPPED(wstatus)返回:非 0 表示真 WSTOPSIG(wstatus)返回:暂停信号编号 收到 SIGSTOP/SIGTSTP 暂停运行
子进程暂停后恢复 WIFCONTINUED(wstatus)返回:非 0 表示真 收到 SIGCONT 信号恢复运行
子进程生成 core dump WIFSIGNALED(wstatus)返回:非 0 表示真 WCOREDUMP(wstatus)返回:非 0 表示生成 被信号杀死且生成 core 文件(系统支持)

正常退出:WIFEXITED → WEXITSTATUS(退出码)
被信号杀死:WIFSIGNALED → WTERMSIG(信号号)
被暂停:WIFSTOPPED → WSTOPSIG(信号号)
恢复运行:WIFCONTINUED(无取值)
生成 core:WIFSIGNALED → WCOREDUMP

最重要规则(考试必考)
必须先判断,再取值
判断宏返回非 0 = 条件成立
取值宏只有在对应判断成立时才有意义

四、waitpid 选项常量表

选项常量 作用 具体行为 典型应用场景
0 实现阻塞等待 父进程暂定直到子进程状态变化(退出 / 暂停) 常规子进程同步等待(如批处理)
WNOHANG 实现非阻塞等待 调用 waitpid 时,若指定子进程未退出,函数立即返回 0,父进程无需阻塞等待 父进程需轮询监控子进程状态(如后台任务管理)
WUNTRACED 捕获子进程暂停状态 当子进程因信号(如 SIGSTOP)停止运行时,waitpid 返回该子进程 PID 调试场景、需处理子进程暂停逻辑的程序
WCONTINUED 捕获子进程恢复运行状态 已停止的子进程通过 SIGCONT 信号恢复运行时,waitpid 返回该子进程 PID 需跟踪子进程完整生命周期(暂停 - 恢复)场景

注:这些选项可以用 | 组合使用,比如 WNOHANG | WUNTRACED

1. waitpid(pid, &status, 0) — 阻塞等待(等价于 wait)

#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        printf("子进程:开始执行任务\n");
        sleep(3); // 模拟耗时任务
        printf("子进程:任务完成,退出\n");
        exit(0);
    } else {
        printf("父进程:等待子进程...\n");
        int status;
        // 阻塞等待子进程退出
        waitpid(pid, &status, 0);
        printf("父进程:子进程已退出,状态码:%d\n", WEXITSTATUS(status));
    }
    return 0;
}

2. WNOHANG — 非阻塞等待(父进程不卡住,可做其他事)

#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        printf("子进程:开始执行任务\n");
        sleep(3);
        printf("子进程:任务完成,退出\n");
        exit(0);
    } else {
        printf("父进程:轮询监控子进程...\n");
        int status;
        while (1) {
            // 非阻塞等待,子进程未结束时立即返回 0
            pid_t ret = waitpid(pid, &status, WNOHANG);
            if (ret == 0) {
                printf("父进程:子进程还在运行,我先做别的事...\n");
                sleep(1);
            } else if (ret > 0) {
                printf("父进程:子进程已退出,状态码:%d\n", WEXITSTATUS(status));
                break;
            } else {
                perror("waitpid error");
                exit(1);
            }
        }
    }
    return 0;
}

3. WUNTRACED — 捕获子进程暂停状态

#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        printf("子进程:运行中,等待被暂停...\n");
        while (1) sleep(1); // 子进程一直运行
    } else {
        printf("父进程:发送 SIGSTOP 暂停子进程\n");
        kill(pid, SIGSTOP); // 暂停子进程

        int status;
        // 等待子进程退出 或 被暂停
        waitpid(pid, &status, WUNTRACED);

        if (WIFSTOPPED(status)) {
            printf("父进程:子进程被信号 %d 暂停\n", WSTOPSIG(status));
        }
    }
    return 0;
}

4. WCONTINUED — 捕获子进程恢复运行状态

#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        printf("子进程:运行中\n");
        while (1) sleep(1);
    } else {
        printf("父进程:发送 SIGSTOP 暂停子进程\n");
        kill(pid, SIGSTOP);

        sleep(2);
        printf("父进程:发送 SIGCONT 恢复子进程\n");
        kill(pid, SIGCONT);

        int status;
        // 等待子进程退出/暂停/恢复
        waitpid(pid, &status, WUNTRACED | WCONTINUED);

        if (WIFCONTINUED(status)) {
            printf("父进程:子进程已恢复运行\n");
        }
    }
    return 0;
}

五、回收僵尸进程方法

回收僵尸子进程是系统编程中常见的问题。僵尸进程本身不占用内存或CPU,但会占用进程ID(PID)表项,若大量存在会导致系统无法创建新进程。

以下是回收僵尸子进程的所有有效方法,按使用场景分类:

1. 父进程主动回收(最推荐)

这是最标准、最可控的方式。父进程在子进程退出后,应主动调用 wait()waitpid() 系统调用,以获取子进程的退出状态并释放其进程表项。

  • 阻塞式回收wait(NULL) 会阻塞父进程,直到任意一个子进程结束。
  • 非阻塞式回收waitpid(-1, NULL, WNOHANG) 会立即返回,若没有子进程结束则返回0,适合在父进程主循环中轮询使用。
  • 信号驱动回收:在父进程中注册 SIGCHLD 信号处理函数。当子进程退出时,内核会向父进程发送 SIGCHLD 信号,处理函数中调用 waitpid 回收所有已退出的子进程。这是异步、高效的处理方式。

2. 父进程退出,由 init 进程接管

如果父进程意外退出或设计缺陷未回收子进程,子进程会成为“孤儿进程”,并被 PID 为 1 的 init 进程(现代系统中通常是 systemd)收养。init 进程会定期调用 wait 回收其所有子进程,包括这些被接管的僵尸进程。这是一种“兜底”机制,但不应作为常规手段,因为它依赖于父进程的异常退出。

3. 系统级处理(极端情况)

当僵尸进程的父进程是 init(即 PPID=1),但仍未被回收时,可能是内核或系统层面的异常。此时可尝试:

  • 检查系统日志(如 dmesg/var/log/syslog),排查硬件或驱动问题。
  • 在极端情况下,重启系统是最终解决方案。

简易背诵版:

父进程主动回收:通过wait()或waitpid()阻塞等待子进程结束,获取其退出状态并释放资源。其中waitpid()可通过WNOHANG参数实现非阻塞回收。
信号处理机制:父进程注册SIGCHLD信号处理函数,当子进程退出时自动触发wait()回收,避免阻塞主线程。
init 进程收养:若父进程先于子进程退出,子进程会被 init 进程(PID=1)收养,init 会定期回收其僵尸子进程。

重要提醒

不要尝试用 kill -9 命令杀死僵尸进程。僵尸进程已经“死亡”,它不响应任何信号,包括 SIGKILL。发送信号对僵尸进程无效,只会徒劳无功。

综上,回收僵尸进程的核心在于父进程的主动管理,通过 wait 系列系统调用或信号处理机制,是健壮程序设计的基本要求。

守护进程

六、守护进程的基本流程

1创建子进程, 让父进程退出

因为父进程有可能是组长进程,不符合条件,也没有什么利用价值,退出即可
子进程没有任何职务, 目的是让子进程最终变成一个会话, 最终就会得到守护进程
2通过子进程创建新的会话,调用函数 setsid(),脱离控制终端, 变成守护进程

3改变当前进程的工作目录 (可选项, 不是必须要做的)

某些文件系统可以被卸载, 比如: U盘, 移动硬盘,进程如果在这些目录中运行,运行期间这些设备被卸载了,运行的进程也就不能正常工作了。

修改当前进程的工作目录需要调用函数 chdir()

4重新设置文件的掩码 (可选项, 不是必须要做的)

掩码: umask, 在创建新文件的时候需要和这个掩码进行运算, 去掉文件的某些权限

设置掩码需要使用函数 umask()

5关闭/重定向文件描述符 

启动一个进程, 文件描述符表中默认有三个被打开了, 对应的都是当前的终端文件

因为进程通过调用 setsid() 已经脱离了当前终端, 因此关联的文件描述符也就没用了, 可以关闭

重定向文件描述符(和关闭二选一): 改变文件描述符关联的默认文件, 让他们指向一个特殊的文件/dev/null,只要把数据扔到这个特殊的设备文件中, 数据被被销毁了

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐