【进程管理】进程与线程

先搞懂"操作系统为什么需要进程",再搞懂"进程是什么"


前言:操作系统为什么需要"进程"

答案是:程序不能并发执行。

如果没有进程,程序只能一个一个排队执行(单道程序时代),CPU大部分时间在等I/O,资源利用率惨不忍睹。进程的诞生,让多道程序得以并发 执行,让CPU、内存、I/O设备能够并行工作,从而把计算机的潜能榨干。

  1. 隔离------把不同程序装进独立"房间",一个崩溃不影响其他。
  2. 切换------记录每个程序的运行状态,让CPU快速切换,营造"同时运行"的假象。
  3. 限制------限制程序权限,防止普通应用随意破坏系统内核。

一、进程的概念

(一) 程序是什么?

程序是存储在磁盘上的一段指令序列,它包含:

  • 代码段(Text) :可执行的机器指令
  • 数据段(Data) :已初始化的全局变量
  • 只读数据段(ROData) :常量字符串等
  • BSS段:未初始化的全局变量(不占用磁盘空间,加载时清0)

程序是死的,是一份"菜谱"。

(二) 进程是什么?

进程是程序的一次执行过程 ,它是活的

进程包含 程序段、数据段、进程控制块 PCB,其中PCB包含或指向:

  • 拥有独立的地址空间(虚拟内存)
  • 拥有执行上下文(寄存器、程序计数器、栈)
  • 拥有资源清单(打开的文件、信号处理器、挂起的信号)

二、PCB(进程控制块):进程的"身份证"与"档案袋"

(一) 内核如何"记住"一个进程?

操作系统管理进程,不是靠进程的名字或路径,而是靠一个叫做PCB(Process Control Block) 的数据结构。

在Linux内核中,这个结构体叫 struct task_struct,定义在include/linux/sched.h中。它的体积极为庞大------在Linux 6.x内核中,task_struct已经超过了 10KB

(二) PCB里存了什么?(五类核心信息)

以Linux为例,

① 进程标识信息

  • pid_t pid:进程ID(用户空间getpid()返回的是TGID)
  • pid_t tgid:线程组ID(主线程的PID)
  • struct task_struct *parent:父进程指针
  • struct list_head children:子进程链表

② 进程状态信息

  • volatile long state:进程状态
    • TASK_RUNNING:就绪或运行(统一为可调度状态)
    • TASK_INTERRUPTIBLE:可中断睡眠(等待事件,可被信号唤醒)
    • TASK_UNINTERRUPTIBLE:不可中断睡眠(通常因I/O等待,D状态
    • TASK_ZOMBIE:僵尸状态(已终止,等待父进程wait()收尸)
    • TASK_STOPPED:暂停状态(被SIGSTOP等信号挂起)

③ 调度信息

  • int prio, static_prio, normal_prio:优先级
  • unsigned int policy:调度策略(SCHED_NORMALSCHED_FIFOSCHED_RR等)
  • u64 vruntime虚拟运行时间(CFS调度器的核心度量指标)

④ 内存管理信息

  • struct mm_struct *mm:进程的地址空间描述符
  • struct mm_struct *active_mm:内核线程借用的内存描述符

⑤ 文件与资源信息

  • struct files_struct *files:已打开的文件描述符表
  • struct fs_struct *fs:当前工作目录、根目录信息
  • struct signal_struct *signal:信号处理器和挂起信号

(三) PCB的"生死簿"属性

进程的创建、调度、消亡,本质都是内核在操作PCB。

  • 创建 :分配一个task_struct,填入初始值,挂入就绪队列
  • 调度 :调度器从队列中选一个task_struct,恢复其上下文
  • 消亡 :释放大部分资源,但保留task_struct(变成僵尸),等待父进程读取退出码后彻底销毁

三、进程三态模型:生命周期的"状态机"

(一) 三种基本状态

进程在其生命周期中,必然处于以下三种状态之一:

scss 复制代码
                        ┌─────────────┐
                        │   就绪态     │
                        │  (Ready)    │
                        └──────┬──────┘
                               │ 调度器分配CPU
                               ▼
                        ┌─────────────┐
           时间片用完 ──▶│   运行态      │──▶ 等待I/O/事件
                        │  (Running)  │    (阻塞操作)
                        └──────┬──────┘
                               │ I/O完成/事件发生
                               ▼
                        ┌─────────────┐
                        │   阻塞态     │
                        │  (Blocked)  │
                        └─────────────┘

(二) 状态转换的触发条件

转换 触发条件 说明
就绪 → 运行 进程调度程序选中该进程 获得CPU时间片
运行 → 就绪 时间片用完 / 被更高优先级进程抢占 主动或被动让出CPU
运行 → 阻塞 请求I/O、申请资源失败、等待信号 进程主动放弃CPU
阻塞 → 就绪 I/O完成、资源可用、信号到达 被唤醒,重新排队等待CPU


四、上下文切换:不仅仅是"保存寄存器"

(一) 什么是上下文切换?

上下文就是:CPU 当前运行现场。

主要包括:

复制代码
PC 程序计数器
SP 栈指针
通用寄存器
浮点寄存器
状态寄存器

上下文切换(Context Switch)是指CPU从一个进程切换到另一个进程的过程。

上下文切换时内核需要做三件事:

  1. 保存当前进程的上下文(CPU寄存器、程序计数器、栈指针等)到它的PCB中
  2. 恢复下一个进程的上下文(从它的PCB中读出)
  3. 切换页表 (将cr3寄存器指向新进程的页目录)

(二) 开销的构成

① 直接开销(CPU周期消耗)

  • 保存/恢复数十个通用寄存器和专用寄存器
  • 刷新流水线(Pipeline Flush)
  • 切换页表(cr3寄存器切换)------这是最大的开销

② 间接开销(微架构级)

  • TLB(Translation Lookaside Buffer)失效:切换页表导致TLB中的所有缓存的虚拟地址↔物理地址映射全部失效,后续内存访问需要重新进行多级页表遍历(Page Walk),延迟从几纳秒暴增至几百纳秒
  • Cache污染:新进程的数据会逐渐替换掉L1/L2/L3缓存中的旧数据,导致cache miss率陡增

这就是为什么:

  • Nginx采用固定数量的Worker进程(避免频繁创建/销毁进程)
  • Redis采用单线程+IO多路复用(完全避免上下文切换)
  • Go使用Goroutine(用户态协程) ,在用户态完成调度,将上下文切换的开销从微秒级降到纳秒级

五、进程 vs 线程:两种并发模型的本质差异

(一) 为什么需要线程?

进程解决了"多道程序并发执行"的问题,但存在明显的局限性:

  • 进程间通信成本高:进程拥有独立的地址空间,相互隔离,想要交换数据必须借助管道、消息队列、共享内存等IPC机制,涉及内核介入和数据拷贝,效率低下。
  • 进程切换开销大:切换进程需要切换页表、刷新TLB,代价沉重。如果大量并发任务频繁切换,系统性能将严重劣化。

于是,线程被设计出来,作为"进程内部更轻量的执行单元",使得同一进程内的多个线程能够:

  • 共享地址空间:可以直接读写同一块内存,通信效率极高(无需内核介入)。
  • 轻量级切换:线程切换无需切换页表(TLB保留),代价远低于进程切换。

(二) 进程与线程的核心区别

线程其实就是轻量级的进程。

  1. 进程是系统进行资源分配和调度的基本单位,线程是 CPU 调度和分派的基本单位;
  2. 线程依赖于进程而存在,一个进程至少有一个线程;
  3. 进程有自己的独立地址空间,线程共享所属进程的地址空间;
  4. 进程可以独立拥有资源,线程并不独立拥有资源。
  5. 线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多
比较维度 进程 线程
资源拥有 拥有独立的地址空间、文件表、信号处理器等资源 共享所属进程的全部资源,自身几乎不拥有资源
通信方式 需要通过IPC机制(管道、共享内存、消息队列等),需经内核 直接读写进程内的共享内存,用户态即可完成
创建/销毁开销 大(需复制页表、建立内存映射、分配内核数据结构) 小(仅需分配线程栈和少量线程局部存储)
上下文切换 昂贵(需切换页表,TLB刷新) 相对廉价(页表不变,TLB保持有效)
系统调用影响 一个进程阻塞,不影响其他进程 一个线程阻塞(如read()),整个进程内的所有线程都无法继续执行
崩溃影响 进程崩溃,不影响其他进程 线程崩溃(如段错误),整个进程崩溃

(三) 关键差异深度剖析

① 通信效率的本质差异

两个进程之间要传递一个整数,数据路径是:进程A → 内核缓冲区 → 进程B,涉及两次数据拷贝和系统调用。两个线程之间传递一个整数,直接通过共享内存读写,只是两次内存访问操作------差了几个数量级

② 为什么线程切换"轻量"?

线程切换时,地址空间(页表)保持不变,因此:

  • TLB不需要刷新,虚拟地址到物理地址的映射缓存仍然有效
  • Cache命中率更高,线程共享代码段和数据段,缓存中已有的热数据可以直接复用

而进程切换时,页表变了,TLB全部失效,cache被迫冷启动------这才是进程切换"重"的真正根源。

需要切换:

③ "一个线程阻塞会拖垮整个进程"为何成立?

当线程A执行read()系统调用时,内核将整个进程(包括线程B、C)置入阻塞态,CPU被让出。因为操作系统调度的是进程(或其内的所有线程),而非单个线程。 这是内核级线程的天然特性,而用户态协程(如Go的Goroutine)通过"不阻塞内核线程"的方式规避了这个问题------具体留待后续篇章详解。

六、Linux视角下的"轻量级进程"真相

(一) 教科书定义 vs Linux实现

教科书定义:

  • 进程:资源分配的基本单位
  • 线程:CPU调度的基本单位

Linux内核的实现(这才是真相):

在Linux内核源码中,你找不到 struct thread,只有struct task_struct

内核并不区分进程和线程,它只调度"任务(Task)"。

(二) 什么是"轻量级进程(LWP)"?

所谓的线程,在Linux内核中就是一个与父进程共享了部分资源的特殊子进程

创建进程和创建线程,底层都调用了同一个系统调用------clone(),区别在于传入的标志位(flags) 不同:

创建方式 底层调用 关键标志 资源关系
fork() clone(SIGCHLD, 0) 不共享任何资源 父子进程资源独立(写时复制)
vfork() `clone(CLONE_VFORK CLONE_VM SIGCHLD, ...)` 共享内存,子进程先运行 主要用于exec()前的临时进程
pthread_create() `clone(CLONE_VM CLONE_FS CLONE_FILES CLONE_SIGHAND CLONE_THREAD ...)` 共享一切 线程间共享地址空间、文件表、信号处理器

(三) 线程到底"轻量"在哪里?

所谓"轻量",体现在资源不拷贝,只共享

  • 不拷贝页表 :所有线程共享同一个mm_struct(内存描述符),切换线程时不需要刷新TLB
  • 不拷贝文件表 :所有线程共享files_struct,打开一个文件所有线程都能用
  • 不拷贝信号处理器 :共享sighand_struct

但调度上并不"轻" :在Linux中,线程和进程的调度开销几乎相同------因为它们都是task_struct,都在同一个运行队列(runqueue)中竞争CPU。

(四) 核心结论

在Linux内核眼中:

  • 进程 = 一个拥有独立地址空间的 task_struct
  • 线程 = 一个共享了父进程地址空间的 task_struct

它们本质上是同一种东西,只是共享资源的程度不同。

这就是为什么Linux程序员常说:"Linux没有真正的内核线程,只有轻量级进程。"


本文是「进程管理」系列的第二篇,欢迎关注,持续更新。如有任何疑问或指正,请在评论区留言讨论。

下篇预告:当10个进程同时处于就绪态时,CPU到底该选谁?

我们将深入Linux CFS(完全公平调度器)的实现,揭秘 vruntime是如何让每个进程"看起来公平地"获得CPU时间的。

相关推荐
free351 小时前
【进程管理】生命周期与调度算法
操作系统
free351 小时前
【进程管理】Linux下的进程管理
操作系统
charlie1145141913 天前
Cinux · 第一次跳进 Ring 3:用户态与特权隔离
开发语言·c++·操作系统·开源项目
程序猿乐锅3 天前
【操作系统 | 第一章】从资源管理到虚拟机
操作系统
CoovallyAIHub5 天前
一个集装箱从船到火车的两个小时,AI 搭档 Coco在中间做了什么
操作系统·agent·资讯
驱动探索者5 天前
RISC-V 指令集深度解析:从 ISA 到 Zephyr
网络·计算机·操作系统·os
码农小韩6 天前
Linux操作系统(六)——软件包管理
linux·操作系统·linux驱动·嵌入式软件开发·linux应用
纵有疾風起6 天前
I/O 软件层次结构:从用户态到硬件中断的完整旅程
操作系统·408·中断·lut·驱动程序·设备独立性
TechEdu2026066 天前
[操作系统]操作系统文件系统与输入输出:架构、行为与调优
操作系统·计算机科学·os