目录
[一、进程创建:fork 函数](#一、进程创建:fork 函数)
[1.1 fork 函数初识](#1.1 fork 函数初识)
[1.2 fork 内核执行流程](#1.2 fork 内核执行流程)
[1.3 写时拷贝(Copy-On-Write)](#1.3 写时拷贝(Copy-On-Write))
[1.4 fork 常规用法](#1.4 fork 常规用法)
[1.5 fork 调用失败原因](#1.5 fork 调用失败原因)
[2.1 进程退出三大场景](#2.1 进程退出三大场景)
[2.2 进程正常退出方法](#2.2 进程正常退出方法)
[2.3 异常退出](#2.3 异常退出)
[2.4 退出码与 ?](#2.4 退出码与 ?)
[3.1 为什么需要进程等待?](#3.1 为什么需要进程等待?)
[3.2 进程等待两个核心](#3.2 进程等待两个核心)
[(1)wait 函数](#(1)wait 函数)
[(2)waitpid 函数](#(2)waitpid 函数)
[3.3 status 位图解析](#3.3 status 位图解析)
[3.4 阻塞 vs 非阻塞等待](#3.4 阻塞 vs 非阻塞等待)
[四、进程程序替换:exec 函数族](#四、进程程序替换:exec 函数族)
[4.1 替换原理](#4.1 替换原理)
[4.2 exec 函数族 6 个接口](#4.2 exec 函数族 6 个接口)
[4.3 命名规律(一秒记住)](#4.3 命名规律(一秒记住))
[4.4 函数特性](#4.4 函数特性)
[五、综合实践:动手实现微型 Shell](#五、综合实践:动手实现微型 Shell)
[5.1 Shell 底层原理](#5.1 Shell 底层原理)
[5.2 微型 Shell 代码实现](#5.2 微型 Shell 代码实现)
[Linux 进程控制 面试题](#Linux 进程控制 面试题)
[1. fork 为什么给子进程返回 0,父进程返回子 PID?](#1. fork 为什么给子进程返回 0,父进程返回子 PID?)
[2. 什么是写时拷贝(COW)?好处是什么?](#2. 什么是写时拷贝(COW)?好处是什么?)
[3. exit 和 _exit 有什么区别?](#3. exit 和 _exit 有什么区别?)
[4. 什么是僵尸进程?危害?如何解决?](#4. 什么是僵尸进程?危害?如何解决?)
[5. 进程等待的作用是什么?](#5. 进程等待的作用是什么?)
[6. wait 和 waitpid 区别?](#6. wait 和 waitpid 区别?)
[7. exec 函数族调用成功为什么不返回?](#7. exec 函数族调用成功为什么不返回?)
[8. Shell 底层是怎么执行命令的?](#8. Shell 底层是怎么执行命令的?)
上篇博客我们讲解了进程的一下额基础概念,作为Linux 系统最核心的抽象概念,一切运行中的程序都以进程的形式存在。掌握进程创建、终止、等待、程序替换。
一、进程创建:fork 函数
1.1 fork 函数初识
fork 是 Linux 中创建子进程的核心系统调用,从已存在进程(父进程)中克隆出新进程(子进程)。
#include <unistd.h>
pid_t fork(void);
返回值规则:
- 子进程中返回 0
- 父进程中返回 子进程 PID
- 创建失败返回 -1
1.2 fork 内核执行流程
当进程调用 fork 后,内核会完成四件事:
- 为子进程分配新的内存块与内核数据结构(PCB)
- 拷贝父进程部分数据结构到子进程
- 将子进程加入系统进程列表
fork返回,由调度器决定父子谁先执行
关键结论:fork 之前父进程单独执行,fork 之后父子两个执行流分开运行,谁先执行由调度器决定。
1.3 写时拷贝(Copy-On-Write)
fork 后父子进程默认代码共享,数据在不修改时也共享。

- 任意一方尝试写入数据时,操作系统会以写时拷贝的方式,为写入方复制一份独立副本
- 优势:节约内存,提升
fork执行效率
1.4 fork 常规用法
- 父进程复制自身,让父子执行不同代码分支(如服务端父进程监听,子进程处理请求)
- 子进程
fork后调用exec执行全新程序
1.5 fork 调用失败原因
- 系统中进程总数达到上限
- 当前用户可创建的进程数超过限制
二、进程终止:退出场景与三种退出方式
2.1 进程退出三大场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止(如段错误、被信号杀死)
2.2 进程正常退出方法
Linux 提供三种正常退出方式,行为差异极大:
| 退出方式 | 核心行为 | 特点 |
|---|---|---|
return |
main 函数中 return n |
等价于 exit(n),最常用 |
exit |
先清理,再终止 | 执行用户清理函数、冲刷缓冲区、关闭流 |
_exit |
直接终止进程 | 不处理缓冲区,不做任何清理 |
代码示例:exit 与 _exit 区别
cpp
// exit:会输出 hello
int main() {
printf("hello");
exit(0);
}
// _exit:无输出,缓冲区未冲刷
int main() {
printf("hello");
_exit(0);
}
2.3 异常退出
- 终端:
Ctrl + C - 信号终止(如
kill -9 PID)
2.4 退出码与 $?
- 进程退出码仅低 8 位有效,范围 0~255
- 终端使用
echo $?查看上一个进程的退出码
三、进程等待:避免僵尸进程,回收子进程资源
3.1 为什么需要进程等待?
- 子进程退出后,父进程不回收会产生僵尸进程,造成内存泄漏
- 僵尸进程无法被
kill -9杀死(进程已死,仅保留 PCB)- 父进程需要获取子进程的退出结果(成功 / 失败 / 异常)
3.2 进程等待两个核心
(1)wait 函数
cpp
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
- 作用:阻塞等待任意一个子进程退出
- 返回值:成功返回子进程 PID,失败返回 -1
status:输出型参数,获取子进程退出状态,不关心可传 NULL
(2)waitpid 函数
cpp
pid_t waitpid(pid_t pid, int *status, int options);
参数说明:
pid = -1:等待任意子进程(等价于 wait)pid > 0:等待指定 PID 的子进程options = WNOHANG:非阻塞等待,子进程未退出返回 0
返回值:
- 正常返回:子进程 PID
- 非阻塞模式无子进程退出:返回 0
- 调用失败:返回 -1
3.3 status 位图解析
status 不能当作普通整数,是低 16 位位图:
- 正常终止:高 8 位存储退出状态码
- 异常终止:低 7 位存储终止信号

宏函数使用:
WIFEXITED(status):判断是否正常退出WEXITSTATUS(status):提取正常退出码
3.4 阻塞 vs 非阻塞等待
- 阻塞等待:父进程挂起,直到子进程退出,简单但无法处理其他任务
- 非阻塞等待:父进程轮询检查,子进程未退出可执行其他逻辑
四、进程程序替换:exec 函数族
4.1 替换原理
fork 创建的子进程默认和父进程执行相同代码,使用 exec 函数族可让进程执行全新的程序。
- 调用
exec后,不创建新进程,PID 不变- 进程用户空间的代码、数据被新程序完全替换
4.2 exec 函数族 6 个接口
cpp
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
4.3 命名规律(一秒记住)
l:list,参数以列表形式传递v:vector,参数以数组形式传递p:自动搜索 PATH 环境变量,无需写全程序路径e:自定义环境变量,不使用系统默认环境变量
核心:只有 execve 是真正的系统调用,其余 5 个都是库函数,最终都会调用 execve。
4.4 函数特性
- 调用成功:不返回,直接执行新程序
- 调用失败:返回 -1(只有出错返回值,没有成功返回值)
五、综合实践:动手实现微型 Shell
5.1 Shell 底层原理
我们日常使用的 bash,本质就是一个进程控制程序,执行逻辑:
- 读取用户输入的命令
- 解析命令为参数列表
fork创建子进程- 子进程调用
exec执行命令- 父进程
wait等待子进程退出,回到命令行
5.2 微型 Shell 代码实现
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define MAX_CMD 1024
char command[MAX_CMD];
// 打印命令行提示符,获取输入
int do_face() {
memset(command, 0, MAX_CMD);
printf("minishell$ ");
fflush(stdout);
if (scanf("%[^\n]%*c", command) == 0) {
getchar();
return -1;
}
return 0;
}
// 解析命令为参数列表
char **do_parse(char *buff) {
static char *argv[32];
int argc = 0;
char *ptr = buff;
while (*ptr != '\0') {
if (!isspace(*ptr)) {
argv[argc++] = ptr;
while (!isspace(*ptr) && *ptr != '\0') ptr++;
*ptr = '\0';
}
ptr++;
}
argv[argc] = NULL;
return argv;
}
// 创建子进程并执行命令
int do_exec(char *buff) {
char **argv = do_parse(buff);
if (argv[0] == NULL) return -1;
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv);
exit(-1);
} else {
waitpid(pid, NULL, 0);
}
return 0;
}
int main() {
while (1) {
if (do_face() < 0) continue;
do_exec(command);
}
return 0;
}
Linux 进程控制 面试题
1. fork 为什么给子进程返回 0,父进程返回子 PID?
答案:
- 子进程只需要知道自己是子进程,不需要知道父 PID
- 父进程需要管理多个子进程,必须拿到 PID 才能区分、等待、回收
2. 什么是写时拷贝(COW)?好处是什么?
答案 :fork 后父子进程共享同一份数据和代码,只有在写入时才复制一份。
优点:
- 节省内存
- 让 fork 执行更快
- 避免不必要的拷贝
3. exit 和 _exit 有什么区别?
答案:
- exit:会冲刷缓冲区、关闭文件、执行清理函数,再调用 _exit
- _exit:直接进入内核终止进程,不处理缓冲区
4. 什么是僵尸进程?危害?如何解决?
答案:
- 子进程退出、父进程没 wait,就会变成僵尸进程
- 危害:占用 PCB,造成内存泄漏
- 解决:父进程调用 wait /waitpid 回收
5. 进程等待的作用是什么?
答案:
- 防止僵尸进程
- 回收子进程资源
- 获取子进程退出状态
6. wait 和 waitpid 区别?
答案:
- wait:阻塞等待任意子进程
- waitpid:可以指定 PID、支持非阻塞等待(WNOHANG),更灵活
7. exec 函数族调用成功为什么不返回?
答案 :因为进程的代码段、数据段已经被完全替换,原来的代码已经不存在了。
8. Shell 底层是怎么执行命令的?
答案:
- 读取命令
- 解析命令
- fork 子进程
- exec 替换执行命令
- 父进程 wait 等待子进程结束