Linux 进程概念初识 一、冯诺依曼体系结构计算机诞生初期程序的修改和加载非常繁琐每次运行都需要人工拨动开关或重新连线。冯诺依曼提出将程序和数据存储在同一个存储器中的思想奠定了现代计算机的基础。体系结构由以下硬件组件构成组件说明输入单元键盘、鼠标、扫描仪等中央处理器CPU包含运算器和控制器输出单元显示器、打印机等关键要点CPU 只能对内存进行读写不能直接访问外设。所有外设的输入输出也只能写入内存或从内存读取。内存是数据交换的唯一中间站。程序运行中的数据流键盘输入到内存CPU 从内存读取并处理结果写回内存再输出到显示器。整个过程数据始终经过内存中转。二、操作系统概念2.1 操作系统的定位早期的计算机程序直接运行在裸机上程序员需要手动管理所有硬件资源效率极低且容易出错。操作系统应运而生向下与硬件交互管理所有软硬件资源向上为应用程序提供良好的执行环境。其本质是一款纯正的搞管理的软件。2.2 管理的本质操作系统如何管理硬件核心方法就是先描述再组织。描述用结构体struct将被管理对象的信息封装起来。例如用 task_struct 描述进程信息。组织用链表或其他高效数据结构将描述信息组织起来方便增删改查。计算机管理硬件的本质描述用 struct组织用链表。2.3 系统调用与库函数操作系统对外暴露部分接口供上层使用这些接口称为系统调用。系统调用功能基础但使用门槛较高因此开发者将其封装为库函数方便二次开发。例如 printf 是对 write 系统调用的封装malloc 是对 brk 系统调用的封装。三、进程概念3.1 进程的定义在单道程序时代CPU 一次只能执行一个程序资源利用率极低。为了充分利用 CPU 资源并支持多任务场景操作系统中引入了进程的概念。进程是程序的一个执行实例是正在运行的程序。从内核角度看进程是担当分配系统资源CPU 时间、内存的实体。进程 内核数据结构task_struct 程序代码和数据3.2 进程控制块PCB为了管理进程操作系统需要记录每个进程的详细信息由此产生了 PCB。Linux 下称为 task_struct它包含了进程的所有属性信息(举例部分)成员说明标识符本进程的唯一标识符PID用于区分不同进程状态任务状态、退出代码、退出信号等优先级相对于其他进程的优先级决定 CPU 调度顺序程序计数器即将被执行的下一条指令的地址内存指针程序代码和进程相关数据的指针以及共享内存块指针上下文数据进程执行时处理器的寄存器中的中间数据I/O 状态信息I/O 请求、分配给进程的 I/O 设备列表3.3 查看进程进程信息可以通过 /proc 文件系统查看。例如查看 PID 为 1 的进程信息cat /proc/1/status。更常用的方式是使用 ps 和 top 工具。ps aux | grep 进程名ps axj | head -10top -d 1 -n 5测试样例编写一个死循环程序运行在另一个终端执行 ps aux 观察其状态#include stdio.h#include unistd.hint main() {while(1) { sleep(1); }return 0;}3.4 创建进程 forkfork 是 Unix 系统经典的进程创建方式。调用成功后父进程和子进程共享代码数据各自独立采用写时拷贝技术。fork 有两个返回值父进程返回子进程的 PID子进程返回 0。#include stdio.h#include unistd.hint main() {pid_t pid fork();if(pid 0) {printf(子进程运行中, PID: %d\n, getpid());} else if(pid 0) {printf(父进程运行中, 子进程 PID: %d\n, pid);}printf(这句话父子进程都会执行\n);return 0;}fork 之后通常要用 if 进行分流。注意 fork 的返回值设计使得一个变量同时出现在两个分支中成为可能这是因为父子进程的数据空间是独立的。测试样例运行后观察父子进程交替执行的输出结果注意两条 printf 都会被执行。四、进程状态4.1 七种状态多任务系统中进程在生命周期内会经历多种状态切换。操作系统通过状态管理来合理分配 CPU 和内存资源。Linux 内核源代码中定义了七种进程状态状态标识说明R运行进程在运行队列中可能正在运行或等待调度S浅度睡眠可中断睡眠等待事件完成能响应外部信号D深度睡眠不可中断睡眠通常等待 IO 完成OS 也无法杀掉T暂停收到 SIGSTOP 信号后暂停可通过 SIGCONT 恢复t追踪被调试器跟踪时因断点而暂停X死亡进程结束瞬间状态在任务列表中看不到Z僵尸进程已退出但未被父进程回收PCB 仍保留4.2 僵尸进程当子进程退出而父进程没有读取子进程的退出状态时子进程进入 Z 状态。退出状态保存在 task_struct 中因此 PCB 必须一直维护导致内存资源无法释放。如果父进程创建了大量子进程却不回收会造成严重的内存泄漏。僵死进程会以终止状态保持在进程表中等待父进程读取退出状态代码。pid_t id fork();if(id 0) {sleep(30); // 父进程休眠不回收子进程} else {exit(0); // 子进程立即退出成为僵尸}测试样例编译后在一个终端运行另一个终端用 ps aux 查看子进程状态为 Z。30 秒后父进程退出子进程被 init 回收。4.3 孤儿进程父进程先于子进程退出时子进程成为孤儿进程。孤儿进程会被 1 号 init 进程或 systemd 进程领养由它们负责回收避免无人管理的进程残留。pid_t id fork();if(id 0) {printf(子进程运行中\n);sleep(10);} else {exit(0); // 父进程先退出}测试样例运行后用 ps 查看孤儿进程的 PPID 变更为 1。4.4 进程等待为避免僵尸进程父进程应主动回收子进程的退出状态。wait 和 waitpid 是常用的系统调用。wait 等待任意子进程退出waitpid 可指定等待特定子进程。pid_t id fork();if(id 0) {int status;waitpid(id, status, 0); // 等待指定子进程if(WIFEXITED(status)) {printf(子进程退出码: %d\n, WEXITSTATUS(status));}}五、进程优先级5.1 基本概念在分时系统中多个进程竞争有限的 CPU 资源。为了让重要任务优先执行操作系统引入了优先级机制。配置进程优先级对多任务环境的 Linux 很有用可以改善系统性能。PRI 表示进程的优先级值越小越早被执行。NInice 值是优先级的修正数值。计算公式PRI(new) PRI(old) nicenice 的取值范围是 -20 到 19共 40 个级别。注意 nice 值不是进程优先级而是优先级的修正数据。5.2 修改优先级启动程序时指定 nice 值nice -n 5 ./program将优先级调低修改正在运行的进程使用 top 命令按 r 键输入 PID 和新的 nice 值也可以使用 renice 命令或系统调用 setpriority 来修改。5.3 竞争、独立、并行与并发概念说明竞争性系统进程数目众多CPU 资源有限进程间具有竞争属性独立性多进程运行期间独享各种资源互不干扰并行 vs 并发并行多 CPU 同时运行多进程。并发单 CPU 通过进程切换在一段时间内推进多进程六、环境变量与命令行参数6.1 环境变量产生的背景不同用户的系统环境各不相同例如命令搜索路径、默认工作目录等。为了让程序在不同环境下都能正常工作操作系统提供了环境变量机制。环境变量通常具有全局特性可以被子进程继承下去从而在进程间传递配置信息。6.2 常见环境变量变量名说明与用途PATH命令搜索路径让系统自动找到可执行程序。通过 echo 命令可查看当前值HOME用户的主工作目录。执行 cd ~ 时跳转至此目录SHELL当前使用的 Shell 路径通常为 /bin/bash6.3 环境变量的组织方式环境变量表本质上是一个字符指针数组每个指针指向一个以 \0 结尾的字符串格式为 变量名值。当操作系统加载一个程序时都会将一张环境表传递给该进程。6.4 通过代码获取环境变量方式一main 函数的第三个参数 envp#include stdio.hint main(int argc, char argv[][], char envp[][]) {for(int i 0; envp[i]; i)printf(%s\n, envp[i]);return 0;}方式二全局变量 environ需要 extern 声明#include stdio.hint main() {extern char environ[][];for(int i 0; environ[i]; i)printf(%s\n, environ[i]);return 0;}方式三系统调用 getenv最常用直接获取指定变量#include stdio.h#include stdlib.hint main() {char result getenv(PATH);if(result) printf(PATH value found\n);return 0;}6.5 环境变量的全局属性环境变量可以被子进程继承。测试方法在当前终端用 export 导出新变量后运行程序即可读取到该变量。若只赋值不 export则仅为 shell 本地变量子进程无法继承。# 终端执行export MYENVhello# 然后运行以下程序#include stdio.h#include stdlib.hint main() {char buf getenv(MYENV);if(buf) printf(MYENV is set\n);return 0;}测试分别尝试 export 导出和不 export 直接赋值观察程序能否读取到变量验证环境变量的继承特性。6.6 命令行参数命令行参数允许用户在启动程序时传入配置信息使同一程序可以处理不同的输入数据。main 函数的参数 argc 表示命令行参数的个数包括程序名本身argv 是参数字符串数组argv[0] 是程序名。#include stdio.hint main(int argc, char argv[][]) {printf(参数个数: %d\n, argc);for(int i 0; i argc; i)printf(argv[%d]: %s\n, i, argv[i]);return 0;}测试编译后执行 ./a.out hello world 123观察 argc 为 4argv[0] 为 ./a.outargv[1] 到 argv[3] 分别为传入的三个参数。6.7 相关命令命令作用echo显示某个环境变量的值export设置新的环境变量env显示所有环境变量unset清除指定的环境变量set显示本地定义的 shell 变量和环境变量七、进程地址空间7.1 虚拟地址空间在早期计算机中程序直接运行在物理内存上地址冲突和越界访问问题严重。虚拟地址空间机制解决了安全性和灵活性问题。在 C/C 程序中看到的地址都是虚拟地址而非物理地址。操作系统通过页表将虚拟地址映射到物理地址。验证方法父子进程修改同一全局变量后变量值不同但地址相同说明该地址是虚拟地址。int counter 0;pid_t pid fork();if(pid 0) {counter 50;printf(子进程: %d, addr: %p\n, counter, counter);} else {sleep(1);printf(父进程: %d, addr: %p\n, counter, counter);}测试样例运行后子进程将 counter 改为 50父进程仍为 0但两个进程打印的地址完全相同从而证明这些地址是虚拟地址。7.2 进程地址空间布局进程地址空间由高地址到低地址依次为内核空间、命令行参数与环境变量、栈区、共享区、堆区、未初始化数据段BSS、初始化数据段、代码段。每个进程都有独立的 mm_struct 结构体描述整个用户空间。不同的虚拟内存区域使用 vm_area_struct 结构体表示。当虚拟区较少时采用单链表组织由 mmap 指针指向当虚拟区间较多时采用红黑树管理由 mm_rb 指向。7.3 虚拟地址的作用引入虚拟地址空间带来了三大好处保护内存安全所有地址访问必须经过页表映射在操作系统监管下进行。恶意程序无法随意读写其他进程或内核的内存区域。进程与内存解耦物理内存可以任意位置加载进程管理模块和内存管理模块相互独立。程序在物理内存中的位置不再受限于编译时的地址。延迟分配malloc 申请的空间先分配虚拟地址实际物理内存在访问时才分配通过缺页中断触发页表建立。这极大提高了内存利用率。八、进程调度与切换8.1 进程切换为了让多个进程共享单个 CPU操作系统需要实现进程间的快速切换。当多任务内核决定运行另一个任务时会保存当前任务的 CPU 寄存器状态入栈然后加载新任务的寄存器状态出栈这个过程称为上下文切换context switch。时间片到达时进程就会被操作系统从 CPU 上剥离。8.2 Linux 2.6 O(1) 调度算法早期 Linux 内核使用 O(n) 调度算法每次调度都需要遍历所有进程效率随进程数增加而下降。Linux 2.6 内核引入 O(1) 调度算法时间复杂度降为常数不随进程数量增长。其核心数据结构包含活动队列active存放时间片尚未结束的进程。共有 140 个优先级队列优先级 100-139通过 bitmap 位图5 个 32 位整数加速查找非空队列。过期队列expired存放时间片已耗尽的进程结构和活动队列完全相同。active 指针和 expired 指针分别指向活动队列和过期队列。当活动队列为空时只需交换两个指针过期队列立即变为新的活动队列实现无缝调度。调度效率与进程数量无关复杂度为 O(1)。九、总结速查表操作命令或函数查看进程ps aux / ps axj / top创建进程fork()获取进程标识符getpid() / getppid()等待子进程wait() / waitpid()终止进程exit() / _exit()设置进程优先级nice / renice / setpriority查看环境变量env / getenv设置环境变量export / putenv / setenv查看进程状态cat /proc/PID/status查看内存映射cat /proc/PID/maps修改环境变量echo / export / unset