当前位置: 首页 > news >正文

进程概念与底层

进程概念与底层机制

1. 冯·诺依曼体系结构与操作系统

1.1 冯·诺依曼体系结构

现代计算机大多遵循此体系,核心原则是存储程序

  • 五大部件
    • 输入设备:键盘、鼠标、磁盘(作为输入时)。
    • 输出设备:显示器、磁盘(作为输出时)。
    • 存储器 (Memory):即内存。CPU只能直接读写内存,外设与CPU的数据交换必须通过内存。
    • 运算器:算术与逻辑运算。
    • 控制器:指挥各部件协调工作。
    • (注:运算器 + 控制器 = 中央处理器 CPU)

1.2 操作系统 (OS)

  • 定位:一款纯正的搞管理的软件。
  • 核心功能
    1. 对下:管理软硬件资源(驱动管理、内存管理、文件管理、进程管理)。
    2. 对上:为用户/应用程序提供良好的执行环境。
  • 管理哲学
    • 描述:用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(动态查看)。
  • 获取 ID
    • getpid():获取当前进程 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 寄存器中的数据。
  • 过程
    1. 保存当前进程的上下文(寄存器数据入栈)。
    2. 恢复下一个进程的上下文(数据出栈到寄存器)。
    3. 跳转执行。
  • 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. 队列之间的流转关系(进程状态机)

进程在生命周期中,会在这三个队列之间不断流转:

  1. 就绪 -> 运行:调度器从优先级队列中取出最高优先级的进程,分配给它 CPU 执行。
  2. 运行 -> 阻塞:运行中的进程如果发起 I/O 请求或等待事件,会被移出优先级队列,放入等待队列,状态变为 S 或 D。
  3. 阻塞 -> 就绪:当 I/O 完成或事件触发时,进程被从等待队列唤醒,重新放回优先级队列,等待下一次被调度。
  4. 运行 -> 过期:如果进程一直在运行,直到时间片用完,它会被移出优先级队列,放入过期队列
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、浏览器、文本编辑器、后台服务)。
核心调度逻辑

调度器在寻找下一个要执行的进程时,遵循严格的阶级壁垒:

  1. 先查实时:通过位图(Bitmap)扫描0~99的队列,只要有进程,立刻执行。
  2. 再查分时:只有当0~99的队列全部为空时,调度器才会去扫描100~139的队列,挑选普通进程执行。

3. 环境变量

3.1 概念与常见变量

  • 定义:操作系统用来指定运行环境的参数,具有全局特性。
  • 常见变量
    • PATH:命令搜索路径。
    • HOME:用户主目录。
    • SHELL:当前 Shell。
  • 操作命令echo $NAME,export,env,unset

3.2 代码访问环境变量

  1. main 函数参数int main(int argc, char* argv[], char* env[])
  2. 全局变量extern char** environ;
  3. 系统调用
    • getenv("NAME"):获取变量值。
    • putenv("NAME=VALUE"):添加/修改变量。
    • 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 为什么需要虚拟地址空间?

  1. 内存保护:进程只能访问自己的虚拟内存,无法随意读写物理内存(防止木马破坏系统)。
  2. 解耦合:进程管理(task_struct)与内存管理(mm_struct)分离。
  3. 统一视图:无论物理内存碎片化如何,进程看到的内存布局都是连续且有序的。
  4. 延迟分配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_VAR=123";// 栈内存putenv(var);// 环境变量表记录了 var 的地址}// 函数结束,var 内存被回收
  • 后果:环境变量表中存储的是指向已释放栈内存的指针(悬空指针)。后续访问该环境变量会导致段错误或读取乱码。
  • 修正:使用setenv("MY_VAR", "123", 1),它会在堆上分配内存并复制字符串,保证数据安全。

4. 进程调度 O(1) 算法的精髓

  • 背景:Linux 2.6 内核引入,解决进程数增多导致调度变慢的问题。
  • 核心
    • Active 队列:还有时间片的进程。
    • Expired 队列:时间片用完的进程。
    • Bitmap:140 个优先级,用 5 个 32 位整数(位图)表示哪些优先级队列非空。
  • 流程
    • 调度时,通过ffs(find first set) 指令在 Bitmap 中直接找到最高优先级的非空队列(常数时间)。
    • 当 Active 队列空了,直接交换指针(swap(active, expired)),无需遍历所有进程重新计算优先级。

5. 孤儿进程与僵尸进程的处理

  • 僵尸进程
    • 危害:占用内核资源(PCB)。
    • 解决:父进程调用wait()waitpid()获取子进程退出状态;或父进程忽略SIGCHLD信号。
  • 孤儿进程
    • 机制:父进程挂掉后,子进程被init(PID 1) 进程收养。
    • 回收init进程会周期性调用wait清理其收养的僵尸子进程,防止系统资源泄漏。
http://www.jsqmd.com/news/1245774/

相关文章:

  • 2026年7月浪琴公布唐山最新网点地址与售后热线电话信息 - 浪琴官方售后服务中心
  • C++Builder Excel导出实战:从COM自动化到性能优化
  • 公考面试班选型参考:粉笔、华图、中公通过率与产品对比
  • 2026年7月最新宝玑成都青羊万达广场维修保养服务电话 - 亨得利钟表维修中心
  • 权威发布:劳力士长沙售后网点地址与客户服务热线2026年7月更新 - 劳力士服务中心
  • 2026 年现阶段,新邵评价高的长途非急救救护车转运平台推荐,紧急救护车之外,长途转运的秘密揭秘 - 行业推荐【认证官】
  • C++时间处理实战:从chrono库原理到高精度计时与避坑指南
  • 现代C++高性能订单匹配引擎:架构、算法与极致优化实践
  • 零基础Python游戏开发入门:从环境搭建到贪吃蛇实战
  • 现代C:ABI 与 API 究竟有什么区别?
  • 2026年7月劳力士福州售后网点地址与全国服务热线通知 - 劳力士官方服务中心
  • C++整合YOLO与RabbitMQ构建实时视频分析流水线
  • 2026 年现阶段宣城专业的水下管道厂家哪家可靠,揭秘城市地下迷宫:水下管道的惊人秘密-茂驰水下沉管工程 - 企业推荐官【认证】
  • 手机状态栏图标优化指南:提升续航与隐私保护
  • 劳力士服务项目及价格查询|维修地址及售后热线权威信息声明(2026年7月最新) - 劳力士服务中心
  • 仅限本月开放:基于127家企业的AI工具复盘基线数据集(含行业分位值),复盘前不校准=无效迭代
  • 5款高效开源工具提升数字化生产力
  • 江诗丹顿中国售后服务中心|网点地址及服务电话权威信息通知(2026年7月最新) - 江诗丹顿服务中心
  • RSA与AES混合加密:原理、实现与工程实践详解
  • 2026 年至今,南平比较好的广州酒家月饼企业哪个好,别再买普通月饼!这才是广州酒家月饼的隐藏真相 - 行业鉴选官
  • 2026年成都空调维修红黑榜:2家真实测评数据公开 - 简单到家
  • 多Agent协作系统在内容创作中的高效实践
  • Claude Code(24):code-review-skill —— 让 AI 成为你的代码审查专家
  • 从托管到共建:技术自主化的演进与实践
  • VC++获取Windows系统路径:从API演进到实战源码解析
  • 高效亲戚朋友人际关系网2.0一款高效管理与人脉信息名片管理和人脉管理软件
  • 泰格豪雅福州声明:二零二六年七月最新网点地址及全国客户服务售后热线电话 - 亨得利钟表维修中心
  • LangChain 结构化输出实战指南:从 StrOutputParser 到 with_structured_output 的完整解决方案
  • 2026 年当下,柳州靠谱的信号屏蔽仪品牌深度剖析,手机信号死寂?这台小设备如何瞬间帮你“断网”-密境卓安电子 - 企业推荐官【认证】
  • JavaScript与D3.js实现树状图数据可视化