一、进程的核心概念到底什么是进程1. 进程的定义简单来说进程是正在进行中的程序是程序的一次执行过程也是程序的执行实例。我们编写的 C 语言源程序如 0.c经过编译链接后会生成可执行文件如 a.out这个可执行文件只是存储在硬盘上的静态程序当它被加载到内存中运行时就成为了动态进程。2. 为什么需要进程在单片机系统中通常只有一个程序在循环运行while(1){}属于单道程序模式CPU 资源利用率极低 —— 当程序进行 IO 操作时CPU 只能空闲等待。而 Linux 作为多任务操作系统通过进程实现了多道程序运行CPU 采用时间片轮转的调度算法为每个进程分配固定的时间片比如 1s宏观上多个进程并行执行微观上 CPU 在不同进程间串行切换。这样一来CPU 在一个进程等待资源时可以处理其他进程极大提升了资源利用率实现了公平、高效的 CPU 调度。3. 进程与程序的核心区别很多人会混淆进程和程序二者的差异可以用一句话概括程序是静态的硬盘文件进程是动态的内存实体具体区别如下特性进程程序状态动态有生命周期创建 - 运行 - 结束静态长期存储存储位置内存硬盘资源占用占用 CPU、内存、文件描述符等系统资源不占用任何系统资源对应关系一个进程对应一个程序一个程序可以对应多个进程如多个 QQ 窗口对应同一个 QQ 程序4. 并发与并行进程实现了操作系统的并发执行这里要区分两个易混概念宏观并行从用户视角看多个进程同时运行微观串行在单核 CPU 中同一时间只有一个进程占用 CPU通过时间片轮转实现 “伪并行”。多核 CPU 则可以实现真正的并行多个进程可在不同核心上同时执行。二、进程的组成Linux 下进程的内存结构程序的组成是代码 数据而进程是程序加载到内存后的运行实例其组成更为复杂核心是PCB进程控制块 程序的内存段text|data|bss | 堆 | 栈二者结合才是一个完整的进程。1. 程序的五大内存段C 语言视角C 语言程序被加载到内存后会被划分为五个连续的内存区域地址从低到高依次排列各自承担不同功能代码区text存储程序的二进制机器指令只读且共享由 exec 从可执行文件中读取字符串常量区存储程序中的字符串常量同样只读全局区 / 静态区存储全局变量和静态变量又分为两部分初始化的数据段.data存储已初始化的全局 / 静态变量未初始化的数据段.bss存储未初始化的全局 / 静态变量由 exec 初始化为 0堆区用于动态内存分配如 malloc/calloc 申请的内存地址从低到高增长空间较大栈区存储局部变量、函数参数、返回地址等地址从高到低增长空间较小由系统自动分配和释放。在 Linux 系统中常将程序简化为三个核心段text代码、data初始化数据、bss未初始化数据。2. PCB进程的 “身份证” 与 “管理手册”PCBProcess Control Block进程控制块是操作系统管理进程的核心数据结构每个进程都有且仅有一个 PCB存储了进程的所有关键信息操作系统通过 PCB 识别和管理进程。Linux 下的 PCB 包含以下基本成员name/ID进程名称和进程标识符PIDPID 是进程的唯一标识Linux 中最小的 PID 为 1init/systemd 进程status进程的当前状态next指向下一个 PCB 的指针用于将所有 PCB 链接成链表start_addr程序的加载地址priority进程的优先级决定 CPU 调度顺序cpu_status现场保留区保存进程的寄存器、堆栈等上下文信息用于进程切换时恢复状态comm_info进程间通信相关信息process_family进程的家族关系父进程、子进程 PIDown_resource进程占用的系统资源如内存、文件描述符。3. 进程的虚拟地址空间Linux 为每个进程分配了独立的 4G 虚拟地址空间32 位系统分为内核空间1G和用户空间3G内核空间受操作系统保护存储内核代码、设备驱动、VFS 虚拟文件系统等用户进程无法直接读写否则会出现段错误用户空间就是上述的五大内存段每个进程的用户空间相互独立互不干扰保证了进程的安全性和可靠性。三、进程的状态Linux 进程的生命周期进程从创建到结束有完整的生命周期不同阶段对应不同的状态核心分为三态模型而 Linux 系统对进程状态做了更细致的划分状态之间通过系统调用和调度相互转换。1. 经典三态模型所有操作系统的进程都具备三个基础状态也是进程的核心状态就绪态进程已准备好运行只欠 CPU等待操作系统调度执行态进程获得 CPU 资源正在运行阻塞态进程因等待某一事件如 IO 操作、资源申请而暂停运行即使有 CPU 也无法执行事件完成后回到就绪态。状态转换触发条件就绪态 → 执行态CPU 调度为进程分配时间片执行态 → 就绪态进程时间片耗尽执行态 → 阻塞态进程发起 IO 请求或等待资源阻塞态 → 就绪态等待的事件完成如 IO 操作结束。2. Linux 下的进程详细状态Linux 通过ps等命令查看进程状态时会显示具体的状态字符对应更细致的进程状态核心状态及含义如下PROCESS STATE CODES状态字符状态名称含义R运行 / 就绪态进程正在运行或在就绪队列中等待 CPU对应经典三态的就绪 执行S可中断睡眠态浅度睡眠等待某事件完成可被信号唤醒如 SIGCONTD不可中断睡眠态深度睡眠通常为 IO 操作等待不可被信号唤醒保证 IO 操作的原子性T暂停态被作业控制信号如 SIGSTOP或调试器暂停t调试暂停态调试过程中被调试器暂停Z僵死态僵尸进程进程已终止但父进程未回收其 PCB 资源进程成为 “僵尸”X死亡态进程即将被销毁永远无法通过命令查看到此外BSD 格式下还会显示额外状态字符如高优先级、N低优先级、l多线程、前台进程组等。3. Linux 进程状态转换Linux 进程状态通过特定的系统调用和信号触发转换核心转换关系TASK_RUNNINGR由fork()创建进程、wake_up()唤醒、时间片耗尽后重新进入就绪队列触发TASK_INTERRUPTIBLES由interruptible_sleep_on()触发可被wake_up_interruptible()或 SIGCONT 信号唤醒TASK_UNINTERRUPTIBLED由sleep_on()触发仅当等待的资源到位后被wake_up()唤醒TASK_STOPPEDT/t由 SIGSTOP 信号或ptrace()调试触发可被 SIGCONT 信号恢复TASK_ZOMBIEZ由do_exit()触发进程终止后未被父进程回收所有状态最终都会通过do_exit()进入死亡态X完成进程销毁。四、Linux 进程常用操作命令在 Linux 终端中我们可以通过一系列命令查看、管理进程以下是最常用的进程命令掌握后能轻松操作进程。1. 动态查看进程toptop命令动态实时查看进程信息默认每 3 秒刷新一次会显示进程的 PID、CPU 占用率、内存占用率、状态、进程名等核心信息是监控系统进程的常用工具。常用操作按q退出 top按P按 CPU 占用率排序按M按内存占用率排序。2. 查看进程快照psps命令查看某一时刻的进程快照常用组合参数为ps aux配合管道grep可过滤指定进程如查看 a.out 进程ps aux | grep a.outps aux输出字段含义USER进程所属用户PID进程 ID%CPUCPU 占用率%MEM内存占用率VSZ虚拟内存大小RSS物理内存大小TTY进程所属终端STAT进程状态START进程启动时间TIME进程占用 CPU 的总时间COMMAND进程对应的命令 / 程序名。3. 查看进程家族关系ps -eLf pstreeps -eLf | grep a.out查看进程的 PID 和PPID父进程 PID明确进程的父子关系pstree -sp 进程PID以树形结构显示进程的家族关系-s显示父进程-p显示 PID直观看到进程的创建层级。4. 向进程发送信号kill killallLinux 中通过信号控制进程kill和killall是发送信号的核心命令常用信号有 SIGSTOP19暂停、SIGCONT18继续、SIGKILL9强制杀死。kill -l查看所有信号的编号和名称kill -信号编号 进程PID向指定 PID 的进程发送信号如暂停进程kill -19 12345 # 暂停PID为12345的进程 kill -9 12345 # 强制杀死PID为12345的进程killall 进程名向指定名称的所有进程发送信号无需知道 PID如杀死所有 a.out 进程killall a.out五、进程的创建fork () 函数的使用Linux 中通过fork()函数创建新进程这是进程编程的基础fork()的设计非常巧妙需要重点理解其工作原理和使用注意事项。1. fork () 函数原型fork()函数属于系统调用头文件和原型如下#include sys/types.h #include unistd.h // 创建子进程通过复制父进程实现 pid_t fork(void);2. fork () 的功能与返回值功能调用fork()的进程为父进程父进程通过复制自身创建一个新的子进程子进程拥有和父进程完全相同的 PCB 和内存段写时复制返回值fork () 调用一次返回两次父子进程各一次成功父进程返回子进程的 PID子进程返回0失败返回 **-1**并设置 errno。3. fork () 的核心注意事项1父子进程的执行顺序fork()成功后父子进程的执行顺序不确定最终由操作系统的 CPU 调度算法决定无法人为控制。2父子进程的执行入口fork () 成功后父子进程从 fork () 的下一条语句开始执行而非从 main 函数开头执行。例如#include stdio.h #include unistd.h int main() { printf(before fork\n); pid_t pid fork(); // 创建子进程 if (pid 0) { perror(fork fail); return -1; } // 父子进程都从这里开始执行 printf(after fork, pid %d\n, getpid()); return 0; }上述代码会输出一次before fork两次after fork父子进程各一次。3父子进程的独立内存空间fork () 创建的子进程拥有独立的 4G 虚拟地址空间父子进程的 text、data、bss、堆、栈相互独立数据之间无相互影响—— 即使子进程修改了全局变量父进程的全局变量也不会改变反之亦然这保证了进程的安全性。4写时复制Copy On Write为了提高效率fork () 并不会立即复制父进程的所有内存数据而是采用写时复制策略父子进程共享内存数据当其中一个进程修改数据时才会为该进程复制一份数据避免不必要的内存拷贝。4. 多次 fork () 的进程数量问题面试中常考多次 fork () 生成的进程数量核心规律n 次连续 fork ()生成 2ⁿ - 1 个子进程总进程数为 2ⁿ。例如fork(); fork();两次 fork () 后总进程数为 41 个父进程 3 个子进程存在孙进程子进程创建的子进程进程呈树形结构。更复杂的如fork()fork()||fork()需要结合逻辑运算符的短路特性分析最终会生成 5 个子进程总进程数为 6。六、特殊进程孤儿进程与僵尸进程在进程的父子关系中若父进程和子进程的结束顺序不同会产生两种特殊进程孤儿进程和僵尸进程其中僵尸进程会占用系统资源需要重点处理。1. 孤儿进程定义父进程先结束子进程仍在运行此时子进程成为孤儿进程处理Linux 中孤儿进程会被init 进程PID1或systemd 进程收养成为后台进程由收养进程负责回收其资源危害孤儿进程无危害最终会被收养进程正常回收。实操验证编写代码父进程打印 PID 后退出子进程无限循环打印自身 PID运行程序用kill -9 父进程PID杀死父进程用top查看子进程会发现其 PPID 变为 1成为孤儿进程。2. 僵尸进程定义子进程先结束父进程未回收其 PCB 资源此时子进程的状态变为Z僵死态成为僵尸进程本质进程的实体已销毁但 PCB 仍保存在内存中占用系统资源危害若父进程一直不回收僵尸进程会持续占用 PID 和内存当系统 PID 耗尽时无法创建新进程解决父进程通过wait()或waitpid()函数回收子进程的 PCB 资源避免僵尸进程。实操验证编写代码父进程无限循环子进程打印 PID 后退出运行程序用ps aux | grep 程序名查看子进程状态为 Z杀死父进程后子进程成为孤儿进程被 init/systemd 收养并回收僵尸进程消失。七、exec 函数族让子进程执行新程序fork () 创建的子进程默认执行和父进程相同的代码若想让子进程执行一个全新的程序需要使用exec 函数族——exec 函数族会替换当前进程的镜像PCB 保留内存段全部替换为新程序的内容实现 “进程复用”。1. exec 函数族的功能exec 函数族的核心功能用一个新的程序替换当前进程的内存镜像text、data、bss、堆、栈当前进程的 PID 不变只是执行的程序变成了新程序。简单来说fork () 创建子进程exec () 让子进程执行新程序二者结合是 Linux 进程创建的经典模式。2. exec 函数族的 6 个函数exec 函数族包含 6 个函数均定义在unistd.h函数名的后缀l、v、p、e代表不同的特性原型如下// 带llist参数逐个罗列以NULL结尾 int execl(const char *path, const char *arg, .../* (char *) NULL */); int execlp(const char *file, const char *arg, .../* (char *) NULL */); int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */); // 带vvector参数组织成字符串指针数组数组以NULL结尾 int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);3. exec 函数族的后缀含义6 个函数的差异体现在后缀上掌握后缀含义就能轻松区分l vs v参数传递方式不同llist参数逐个罗列最后一个参数必须是NULL表示参数结束vvector将所有参数组织成字符串指针数组数组最后一个元素为NULL。带 ppath自动搜索环境变量 PATH不带 p需要传入新程序的绝对路径 / 相对路径如/home/linux/a.out带 p只需传入新程序的文件名如ls函数会从系统环境变量 PATH 中自动搜索程序路径。带 eenv自定义环境变量不带 e子进程继承父进程的环境变量带 e需要传入自定义的环境变量数组数组以NULL结尾覆盖系统环境变量。4. exec 函数族的返回值exec 函数族只有出错时才会返回返回值为-1若执行成功当前进程的镜像已被替换不会返回任何值。5. 常用示例1execl执行指定路径的程序// 执行/home/linux/a.out参数为a.out以NULL结尾 execl(/home/linux/a.out, a.out, NULL);2execlp执行系统命令利用 PATH// 执行ls命令参数为ls, -l, NULL自动从PATH中找ls路径 execlp(ls, ls, -l, NULL);3execle自定义环境变量// 自定义环境变量数组 char *const my_env[] {USERlinux, PASSWD12345, NULL}; // 执行env命令传入自定义环境变量 execle(/usr/bin/env, env, NULL, my_env);4execvp数组传参 搜索 PATH// 组织参数数组 char *const argv[] {ps, aux, NULL}; // 执行ps aux命令 execvp(ps, argv);八、进程的退出正常退出与异常退出进程的运行有始有终退出方式分为正常退出和异常退出不同退出方式对应不同的函数和场景核心是保证进程退出时清理资源。1. 进程的退出方式1正常退出5 种进程完成所有功能后主动退出属于正常结束从main()函数返回return 0;是最常见的正常退出方式调用exit()函数库函数退出前会清理资源调用_exit()函数系统调用立即退出不清理资源调用_Exit()函数与_exit()功能一致C 标准库函数进程的最后一个线程退出。2异常退出3 种进程因错误或外部信号被迫退出属于非正常结束调用abort()函数主动触发 SIGABRT 信号终止进程收到终止信号如 SIGKILL、SIGINT如用户按CtrlC发送 SIGINT 信号进程出现运行错误如段错误、除零错误系统自动发送信号终止进程。2. 核心退出函数exit () vs _exit ()exit()和_exit()是最常用的退出函数二者功能相似但有关键差异面试中常考。1exit () 函数#include stdlib.h // 库函数正常结束进程 void exit(int status);参数status为退出状态值仅低 8 位有效status0377父进程可通过wait()获取该值核心特性退出前会做资源清理工作执行通过atexit()注册的退出处理函数刷新并关闭所有标准 IO 流清理缓冲区调用_exit()系统调用完成进程真正的退出。2_exit () 函数#include unistd.h // 系统调用立即结束进程 void _exit(int status);参数与exit()一致status为退出状态值核心特性无任何清理工作直接终止进程关闭文件描述符释放内存将进程状态改为僵死态。3exit () 与_exit () 的核心区别特性exit()_exit()函数类型库函数系统调用资源清理清理 IO 缓冲区、执行退出处理函数无任何清理底层调用最终调用_exit ()直接完成进程退出适用场景普通进程正常退出需要清理资源子进程中退出避免父进程缓冲区被重复刷新3. 退出处理函数atexit ()atexit()函数用于注册进程退出处理函数当进程通过exit()正常退出时会自动执行注册的处理函数实现进程退出前的自定义清理工作如释放动态内存、关闭文件。#include stdlib.h // 注册退出处理函数function为函数指针无参无返回值 int atexit(void (*function)(void));返回值成功返回 0失败返回非 0核心特性注册顺序与调用顺序相反先进后出例如注册了 3 个函数退出时会按 3→2→1 的顺序执行。示例#include stdio.h #include stdlib.h void clean1() { printf(clean 1\n); } void clean2() { printf(clean 2\n); } int main() { atexit(clean1); // 先注册clean1 atexit(clean2); // 后注册clean2 exit(0); // 正常退出 }输出结果clean 2 clean 1九、进程的资源回收wait () 与 waitpid () 函数为了避免僵尸进程父进程需要主动回收子进程的 PCB 资源Linux 提供了wait()和waitpid()两个函数专门用于等待子进程状态改变并回收其资源其中waitpid()更灵活是实际开发中的首选。1. wait () 函数简单的阻塞式回收#include sys/types.h #include sys/wait.h // 阻塞等待子进程状态改变回收子进程资源 pid_t wait(int *wstatus);1功能等待当前进程的任意一个子进程状态改变结束、暂停、恢复并回收子进程的 PCB 资源主要用于回收僵死态的子进程。2参数wstatus指向整型变量的指针用于保存子进程的退出状态值若不需要获取退出状态可传入NULL。3返回值成功返回结束的子进程的 PID失败返回 - 1如当前进程无子进程。4核心特性wait()是阻塞函数若当前进程有子进程且子进程未结束wait()会一直阻塞直到有子进程结束后才返回。2. waitpid () 函数灵活的可阻塞 / 非阻塞回收waitpid()是wait()的升级版功能更强大支持指定子进程回收、非阻塞回收是实际开发中最常用的资源回收函数。#include sys/types.h #include sys/wait.h // 等待指定子进程状态改变回收资源支持非阻塞 pid_t waitpid(pid_t pid, int *wstatus, int options);1参数详解pid指定要等待的子进程取值有四种情况覆盖所有场景pid -1等待 ** 进程组 ID 为 | pid|** 的任意子进程pid -1等待当前进程的任意子进程与 wait () 功能一致pid 0等待与当前进程同进程组的任意子进程pid 0等待PID 为 pid的指定子进程最常用。wstatus与 wait () 一致保存子进程的退出状态值传入 NULL 则不获取。options设置回收模式核心取值0阻塞模式与 wait () 一致子进程未结束则阻塞WNOHANG非阻塞模式若指定的子进程未结束立即返回 0不阻塞其他取值WUNTRACED、WCONTINUED用于监控暂停 / 恢复的子进程。2返回值成功返回结束的子进程的 PID非阻塞模式下子进程未结束返回0失败返回 **-1**如无子进程、参数错误。3等价关系waitpid(-1, status, 0);完全等价于wait(status);。3. 退出状态值解析WIFEXITED、WEXITSTATUS 等宏wstatus参数保存了子进程的退出状态值该值是一个 32 位整数包含了退出方式、退出码、信号编号等信息无法直接解析Linux 提供了专门的宏来解析wstatus常用宏如下判断正常退出WIFEXITED(wstatus)若子进程正常退出return/exit/_exit返回非 0否则返回 0WEXITSTATUS(wstatus)当 WIFEXITED 为真时获取子进程的退出码即 return/exit 的参数。判断信号退出WIFSIGNALED(wstatus)若子进程被信号终止返回非 0否则返回 0WTERMSIG(wstatus)当 WIFSIGNALED 为真时获取终止子进程的信号编号。判断暂停 / 恢复WIFSTOPPED(wstatus)若子进程被暂停返回非 0WSTOPSIG(wstatus)获取暂停子进程的信号编号WIFCONTINUED(wstatus)若子进程被恢复返回非 0。4. 常用示例回收指定子进程#include stdio.h #include unistd.h #include sys/wait.h #include stdlib.h int main() { pid_t pid fork(); if (pid 0) { perror(fork fail); return -1; } else if (pid 0) { // 子进程运行5秒后退出退出码为10 sleep(5); exit(10); } else { // 父进程非阻塞回收PID为pid的子进程 int status; while (1) { pid_t ret waitpid(pid, status, WNOHANG); if (ret 0) { // 子进程正常退出 if (WIFEXITED(status)) { printf(子进程%d正常退出退出码%d\n, pid, WEXITSTATUS(status)); } // 子进程被信号终止 else if (WIFSIGNALED(status)) { printf(子进程%d被信号%d终止\n, pid, WTERMSIG(status)); } break; } else if (ret 0) { // 子进程未结束继续执行其他逻辑 printf(子进程未结束执行其他操作...\n); sleep(1); } else { // 回收失败 perror(waitpid fail); break; } } } return 0; }十、进程编程实战经典场景与练习掌握了进程的基础概念和函数后结合实际场景编程能加深理解以下是 Linux 进程编程的经典场景和练习覆盖 fork ()、exec ()、waitpid () 的综合使用。1. 场景 1创建 n 个子进程要求从键盘输入 n创建 n 个子进程每个子进程打印自身 PID 和编号父进程回收所有子进程避免僵尸进程。核心思路使用 for 循环创建 fork ()注意子进程创建后要立即退出循环否则子进程会继续创建孙进程。2. 场景 2父子进程分工拷贝文件要求父进程拷贝文件的前一半子进程拷贝文件的后一半实现文件的并行拷贝提高效率。核心注意事项若open()在 fork ()之前调用父子进程共享文件描述符包括文件偏移量和打开状态需要手动调整偏移量若open()在 fork ()之后调用父子进程拥有独立的文件描述符互不干扰。3. 场景 3无人机多任务实现无人机需要同时执行飞行、视频采集、数据传输、数据存储四个功能通过 fork () 创建四个子进程分别执行不同的功能父进程监控所有子进程子进程结束后父进程立即回收并打印信息。核心思路fork () 创建四个子进程每个子进程执行对应的功能函数do_fly/do_video/do_transmit/do_store父进程用 waitpid () 非阻塞回收所有子进程。4. 场景 4简易 Shell 实现实现一个简易的 Shell支持执行系统命令如 ls、ps、pwd核心步骤循环打印命令提示符如my_shell从键盘读取用户输入的命令用strtok()拆分命令和参数fork () 创建子进程子进程通过 execvp () 执行命令父进程用 waitpid () 回收子进程避免僵尸进程。十一、进程知识点总结Linux 进程是操作系统的核心本文从基础到实操全面讲解了进程的相关知识核心知识点总结如下进程定义正在运行的程序是程序的一次执行过程动态的内存实体进程组成PCB进程控制块 内存段text|data|bss | 堆 | 栈PCB 是进程的核心管理结构进程状态Linux 核心状态为 R/S/D/T/Z重点关注僵尸进程Z的处理进程命令top动态、ps aux快照、pstree家族关系、kill/killall发送信号进程创建fork () 复制父进程调用一次返回两次父子进程拥有独立的内存空间特殊进程孤儿进程被 init/systemd 收养无危害、僵尸进程父进程未回收需用 wait/waitpid 处理执行新程序exec 函数族替换进程镜像forkexec 是 Linux 进程创建的经典模式进程退出正常退出exit ()/return、异常退出信号 /abort ()exit () 会清理资源_exit () 立即退出资源回收wait ()阻塞回收任意子进程、waitpid ()灵活回收指定子进程支持非阻塞结合宏解析退出状态核心思想进程的出现实现了多任务并发提高了 CPU 资源利用率是 Linux 系统高效运行的基础。