进程概念与底层
进程概念与底层机制1. 冯·诺依曼体系结构与操作系统1.1 冯·诺依曼体系结构现代计算机大多遵循此体系核心原则是存储程序。五大部件输入设备键盘、鼠标、磁盘作为输入时。输出设备显示器、磁盘作为输出时。存储器 (Memory)即内存。CPU只能直接读写内存外设与CPU的数据交换必须通过内存。运算器算术与逻辑运算。控制器指挥各部件协调工作。(注运算器 控制器 中央处理器 CPU)1.2 操作系统 (OS)定位一款纯正的搞管理的软件。核心功能对下管理软硬件资源驱动管理、内存管理、文件管理、进程管理。对上为用户/应用程序提供良好的执行环境。管理哲学描述用struct结构体描述被管理对象如进程。组织用链表、红黑树等数据结构将对象组织起来。系统调用与库函数系统调用OS 暴露给上层的接口功能基础直接使用难度高。库函数对系统调用的封装如glibc便于开发者二次开发。2. 进程 (Process)2.1 进程的基本概念定义课本程序的一次执行实例。内核担当分配系统资源CPU时间、内存的实体。公式进程 内核数据结构 (PCB) 程序代码和数据描述进程 - PCB (Process Control Block)Linux 中 PCB 的具体实现是task_struct结构体。包含信息标识符 (PID)唯一标识。状态运行、睡眠、退出代码等。优先级相对于其他进程的优先权。程序计数器下一条指令的地址。内存指针指向代码、数据、共享内存的指针。上下文数据寄存器中的值用于切换恢复。I/O 状态打开的文件列表等。2.2 进程操作查看进程/proc目录查看进程详细信息如/proc/1。命令ps aux(显示所有进程),ps axj(查看进程组/会话ID),top(动态查看)。获取 IDgetpid()获取当前进程 PID。getppid()获取父进程 PPID。创建进程 -fork()特点调用一次返回两次。父进程返回子进程的 PID (0)。子进程返回 0。失败返回 -1。写时拷贝 (Copy On Write, COW)父子进程代码共享数据各自开辟空间私有。在未修改数据前物理内存是共享的一旦修改系统会复制一份副本给修改者。2.3 进程状态 (Linux 内核视角)状态码状态名称描述RRunning运行或在运行队列中等待调度。SSleeping可中断睡眠等待事件完成如信号。DDisk Sleep不可中断睡眠通常在等待 I/O 结束不能被信号打断。TStopped停止状态收到SIGSTOP信号暂停SIGCONT继续。tTracing Stop调试停止状态如 gdb 调试时。XDead死亡状态瞬间状态不可见。ZZombie僵尸状态进程退出但父进程未读取其退出码。僵尸进程 (Zombie)成因子进程退出父进程未调用wait()读取状态。危害PCB 结构体仍驻留内存导致内存泄漏。若大量产生会耗尽进程表资源。孤儿进程 (Orphan)成因父进程先于子进程退出。处理子进程被1号进程 (init/systemd)领养由 1 号进程负责回收其资源。2.4 进程优先级概念CPU 资源分配的先后顺序。PRI (Priority)进程被执行的优先级值越小优先级越高。NI (Nice)优先级的修正数值。公式PRI(new) PRI(old) NI范围-20 到 19。调整top命令中按r修改或使用nice,renice命令。并发性 vs 并行性并发多个进程在一个 CPU 上通过切换实现“同时”推进。并行多个进程在多个 CPU 上同时运行。2.5 进程切换上下文 (Context)CPU 寄存器中的数据。过程保存当前进程的上下文寄存器数据入栈。恢复下一个进程的上下文数据出栈到寄存器。跳转执行。Linux 2.6 O(1) 调度算法使用runqueue(运行队列) 和prio_array(优先级数组)。利用位图 (Bitmap) 快速查找最高优先级非空队列确保调度时间复杂度为常数 O(1)。2.6 进程调度队列与流转机制 (O(1) 调度核心)在 Linux O(1) 调度器中进程并不是杂乱无章的而是被严格分类并放入不同的队列中。理解这三个队列及其流转关系是掌握进程调度精髓的关键1. 三大核心队列优先级队列 (Priority Array)本质一个包含 140 个链表对应 140 个优先级的数组。作用存放所有**处于可运行状态R**的进程。只要进程有资格被 CPU 执行它就一定在这个数组的某个优先级链表中。等待队列 (Wait Queue)本质双向链表结构。作用存放所有**处于阻塞/睡眠状态S/D**的进程。当进程需要等待 I/O 完成、等待锁或等待信号时会被移出优先级队列挂到特定的等待队列上。过期队列 (Expired Array)本质与优先级队列结构完全相同的数组。作用存放时间片耗尽的进程。当进程在优先级队列中耗尽了分配的 CPU 时间片就会被移到过期队列中等待下一轮调度。2. 队列之间的流转关系进程状态机进程在生命周期中会在这三个队列之间不断流转就绪 - 运行调度器从优先级队列中取出最高优先级的进程分配给它 CPU 执行。运行 - 阻塞运行中的进程如果发起 I/O 请求或等待事件会被移出优先级队列放入等待队列状态变为 S 或 D。阻塞 - 就绪当 I/O 完成或事件触发时进程被从等待队列唤醒重新放回优先级队列等待下一次被调度。运行 - 过期如果进程一直在运行直到时间片用完它会被移出优先级队列放入过期队列。3. Active 与 Expired 的指针交换Active 队列当前正在被调度器使用的优先级队列存放还有时间片的进程。Expired 队列存放时间片用完的进程。核心机制当 Active 队列里的所有进程都用完了时间片Active 变空调度器不会去遍历所有进程重新计算优先级。它只需要执行一个极其简单的操作交换 Active 和 Expired 的指针Swap。结果原本的 Expired 队列瞬间变成了新的 Active 队列。因为进程在放入 Expired 时内核已经根据它的静态优先级和 Nice 值计算好了它在下一轮的优先级位置。这就保证了无论系统里有多少个进程调度时间永远是常数O(1)。2.7 优先级队列的底层划分实时与分时在 Linux O(1) 调度器的 140 个优先级队列中并非所有队列都是平等的。它们被严格划分为两个完全不同的调度体系1. 实时优先级队列 (Real-Time Queue)索引范围queue[0]到queue[99]共 100 个队列。优先级特征数值越小优先级越高。调度策略采用SCHED_FIFO先进先出或SCHED_RR时间片轮转。绝对霸权只要这 100 个队列中有任何一个进程处于就绪状态调度器就会无条件将 CPU 分配给它们。它们可以瞬间抢占任何普通进程。适用场景对响应时间要求极其苛刻的场景如汽车刹车控制、高频交易、音视频实时处理。2. 分时/普通优先级队列 (Time-Sharing/Normal Queue)索引范围queue[100]到queue[139]共 40 个队列。优先级特征数值越大优先级越低对应 nice 值 -20 到 19。调度策略采用时间片轮转机制追求宏观上的公平性。适用场景我们日常使用的绝大多数程序如 Shell、浏览器、文本编辑器、后台服务。核心调度逻辑调度器在寻找下一个要执行的进程时遵循严格的阶级壁垒先查实时通过位图Bitmap扫描0~99的队列只要有进程立刻执行。再查分时只有当0~99的队列全部为空时调度器才会去扫描100~139的队列挑选普通进程执行。3. 环境变量3.1 概念与常见变量定义操作系统用来指定运行环境的参数具有全局特性。常见变量PATH命令搜索路径。HOME用户主目录。SHELL当前 Shell。操作命令echo $NAME,export,env,unset。3.2 代码访问环境变量main 函数参数int main(int argc, char* argv[], char* env[])全局变量extern char** environ;系统调用getenv(NAME)获取变量值。putenv(NAMEVALUE)添加/修改变量。setenv(NAME, VALUE, 1)安全地设置变量推荐。 扩展putenv 与 setenv 的底层区别putenv直接引用传入的字符串指针。风险如果传入的是局部变量栈内存函数结束后内存释放环境变量表会变成悬空指针导致崩溃。setenv在内部malloc一块新内存复制字符串内容。安全不受外部变量生命周期影响。3.3 继承性环境变量具有全局属性子进程会继承父进程的环境变量。普通局部变量未 export不会被子进程继承。环境变量表如图实质上是一个字符指针数组每个指针指向⼀个以’\0’结尾的环境字符串4. 进程地址空间 (虚拟内存)4.1 现象与本质现象父子进程中同一个变量的虚拟地址相同但物理地址不同内容不同。结论C/C 代码中看到的地址都是虚拟地址。OS 负责将虚拟地址映射到物理地址。4.2 核心结构mm_struct(内存描述符)描述整个进程的用户空间地址分布。vm_area_struct(VMA)描述具体的内存区域如代码段、堆、栈通过链表或红黑树组织。4.3 为什么需要虚拟地址空间内存保护进程只能访问自己的虚拟内存无法随意读写物理内存防止木马破坏系统。解耦合进程管理task_struct与内存管理mm_struct分离。统一视图无论物理内存碎片化如何进程看到的内存布局都是连续且有序的。延迟分配malloc只是在虚拟空间申请真正访问时才分配物理内存缺页中断。4.4 内存布局 (32位系统典型分布)区域说明增长方向内核空间1G (高地址)用户不可见-栈 (Stack)局部变量、函数调用高 - 低内存映射段动态库、mmap-堆 (Heap)malloc/new分配低 - 高BSS 段未初始化全局/静态变量-数据段已初始化全局/静态变量-代码段 (Text)二进制代码、常量字符串-扩展1. 深入理解fork()的返回值机制问题为什么fork一次调用会有两个返回值原理fork内部通过系统调用创建子进程。子进程创建成功后内核会将父进程的寄存器上下文包括EAX/RAX寄存器用于存放返回值复制一份给子进程。修改副本内核在子进程的 PCB 中将复制过来的EAX寄存器的值修改为0。保留原版父进程的EAX寄存器中保留的是子进程的 PID。当调度器分别调度父子进程从fork返回时它们读取各自的寄存器就得到了不同的返回值。2. 虚拟内存与物理内存的映射 (页表)页表 (Page Table)OS 维护的数据结构记录虚拟地址到物理地址的映射关系。写时拷贝 (COW) 的页表实现fork刚结束时父子进程的页表指向同一块物理内存。此时页表属性被标记为**“只读”**。当任意一方尝试写入数据时触发缺页异常/中断。OS 捕获异常发现是写操作于是开辟新物理内存拷贝数据更新写方的页表映射并恢复写权限。3. 环境变量putenv的内存陷阱putenv*ptr可以将储存环境变量路径的指针储存金环境变量表中但是设置为局部变量可能会造成悬空指针场景voidset_env(){charvar[]MY_VAR123;// 栈内存putenv(var);// 环境变量表记录了 var 的地址}// 函数结束var 内存被回收后果环境变量表中存储的是指向已释放栈内存的指针悬空指针。后续访问该环境变量会导致段错误或读取乱码。修正使用setenv(MY_VAR, 123, 1)它会在堆上分配内存并复制字符串保证数据安全。4. 进程调度 O(1) 算法的精髓背景Linux 2.6 内核引入解决进程数增多导致调度变慢的问题。核心Active 队列还有时间片的进程。Expired 队列时间片用完的进程。Bitmap140 个优先级用 5 个 32 位整数位图表示哪些优先级队列非空。流程调度时通过ffs(find first set) 指令在 Bitmap 中直接找到最高优先级的非空队列常数时间。当 Active 队列空了直接交换指针(swap(active, expired))无需遍历所有进程重新计算优先级。5. 孤儿进程与僵尸进程的处理僵尸进程危害占用内核资源PCB。解决父进程调用wait()或waitpid()获取子进程退出状态或父进程忽略SIGCHLD信号。孤儿进程机制父进程挂掉后子进程被init(PID 1) 进程收养。回收init进程会周期性调用wait清理其收养的僵尸子进程防止系统资源泄漏。