【操作系统】线程、进程调度、KLT和ULT
2026/8/30 7:02:55 网站建设 项目流程

一、线程

  • 进程:服务大厅
  • 线程:大厅中的100个人
  • 线程库:大厅中的管理员,负责把进程申请的内核级线程分配给线程
  • 内核级线程:大厅中的窗口,CPU调度的单位,CPU的一个时间片/执行流,可以根据人流量来动态创建和销毁的。
  • 人正在窗口办事:一个应用线程绑定了一个内核级线程
  • CPU:会一个一个处理窗口(内核级线程)工作的闪电侠

当应用级线程发生IO阻塞时,这个窗口的工作就会暂停,内核只知道这个内核级线程(窗口)阻塞了,于是把这个KLT挂起,切换到另一个KLT排队等待CPU来处理
用户态的线程库会检测到当前这个ULT发生了阻塞调用,它会主动把该ULT从KLT上解绑,然后立刻从候客区拉一个新的ULT绑定到这个KLT上(或者绑定到另一个空闲KLT上)。

解释一下这里的解绑和绑定:

  • 绑定:把当前 ULT 的运行状态(程序计数器 PC、栈指针 SP、以及它需要执行的函数地址)写入这个 KLT 的寄存器上下文中。
  • 解绑:场景:这个KLT 正在执行 ULT-A,突然 ULT-A 说:“我要去读个文件,可能要等一会儿。”怎么做(重点来了):
    1. 保存现场:用户态调度器立刻把当前 KLT 里的寄存器状态(PC、SP)原封不动地拷贝回 ULT-A 自己的内存空间(用户态 PCB)里保存起来。

      寻找替身:调度器立刻从等待队列里捞出一个 ULT-B。

    2. 恢复现场:调度器把 ULT-B 之前保存的寄存器状态,直接覆盖到当前这个 KLT 的寄存器里。

    3. 继续执行:CPU 根本不知道刚才发生了什么,它只看到自己手里的 KLT 突然变了个样,于是继续顺着新的指令往下执行。

这就像快递车(KLT)开到半路,把车上的货物 A(ULT-A)卸在仓库,然后立刻从仓库拿出货物 B(ULT-B)装上车,继续往前开。

这个调度器也是一个程序,一切动作其实都是CPU来执行的,它是怎么在这里刚好被CPU执行的?

时间片一到,可编程定时器向CPU发送一个硬件中断信号,硬件电路会自动保存当前的指令地址,把PC寄存器里的值强制修改成中断处理程序的地址,然后中断处理程序检查发现是时间片用完了,于是调用操作系统的调度器函数,然后CPU就开始执行调度器这个程序了:从内存中挑中一个进程,将这个进程PCB里保存的寄存器快照全部拷贝回CPU硬件寄存器中



二、线程库

:一段普通的程序代码,本质上就是一套运行在用户空间的线程调度算法。

操作系统只管把大块的时间分给进程,而线程库负责把这段时间切碎了,分给程序内部的多个线程比如:操作系统说:“进程,给你 10 毫秒的时间,去干活吧!”。这 10 毫秒里,线程库决定:“现在让 A 去窗口办事 3 毫秒,然后换 B 办 3 毫秒,再换 C 办 4 毫秒。


线程库只需在内部修改了一下寄存器状态和栈指针,就把活儿交接了。这就是为什么早期用户级线程切换极快,因为没有陷入内核态。它和用户的业务代码一起运行在用户空间,完全不需要经过操作系统。

那线程库具体是怎么切换应用级线程到内核级线程的呢?

比如说,在多对多的模型中,CPU正在处理一个KLT,一个ULT在这个KLT上,KLT得到的CPU的时间片是10毫秒,那这个ULT可能得到3毫秒,CPU是正在处理这个KLT的,CPU处理了3毫秒,那线程库是怎么切换ULT的呢,通过修改CPU上的 PC寄存器 和 栈指针,

  1. PC寄存器里面存的是下一条要执行的代码的内存地址。
  2. 一个线程的栈内存里保存着属于这个线程的局部变量和CPU执行到哪一步了,这样CPU下一次拿到这个线程,就能立马拿到数据,找到下一步要执行的指令去执行

线程库把 PC 寄存器的值,从“线程A的代码地址”改成了“线程B的代码地址”。CPU 下一瞬间就会去执行线程B的代码。栈指针的作用:它指向当前线程的栈内存,切换时,线程库把会 SP 寄存器(栈指针)指向线程B的栈内存。


早期的线程库,实现了逻辑上的并发,因为在给定的CPU时间片内,A、B、C线程确实都得到了运行,但也因为得不到操作系统的真正支持,导致了“一人阻塞,全团陪绑”的致命缺陷。这也正是后来为什么必须要把线程引入内核(内核级线程)的根本原因。


三、不同的线程模型

  • 多对一模型
    大厅里有100个人在排队,但只有1个窗口开放。不管排队的人怎么换,窗口就那一个。如果正在办事的人突然要去查个档案(阻塞操作/IO),后面99个人全得干等着。

  • 一对一模型
    大厅里100个人,对应开了100个窗口。每个人都有自己的专属窗口,互不干扰。一个人去查档案,不影响别的窗口办事。但缺点是:开窗口太费资源了(创建内核线程成本高)

  • 多对多模型
    大厅里有100个人,但只开了10个窗口

    • 高效切换:排队的人只需换个位置(用户级线程切换),不需要窗口工作人员操作,速度极快。
    • 并行处理:10个窗口可以同时给10个人办事(真正的并行)。
    • 阻塞问题:如果正在办事的人突然说“我要等个电话”(发生IO阻塞),这个窗口就空出来了。这时候,调度器会将这个ULT解绑,然后绑定另一个ULT【用户态调度器立刻把当前 KLT 里的寄存器状态(PC、SP)原封不动地拷贝回 线程自己的内存空间(用户态 PCB)里保存起来。调度器立刻从等待队列里捞出一个 线程,把线程的PCB里之前保存的寄存器状态,直接覆盖到当前这个 KLT 的寄存器里。】

在多对多模型的设计中,操作系统会给每一个进程分配好合适数量的KLT(假如是m个),也就是 m 个CPU时间片/执行流,进程可以用这些KLT来执行自己的线程。这 m 个内核级线程一旦分配给这个进程,就成为了这个进程的专属资产。其他进程不能用


线程是CPU调度的基本单位。
它就是一块独立的栈空间加上一组寄存器状态(PC、SP、通用寄存器)构成的执行上下文。当你把这组寄存器状态和栈保存下来,你就保存了一个线程。

线程的本质,就是那一组寄存器的状态。

  • PC(程序计数器):指向代码区的第 100 行。
  • SP(栈指针):指向内存里的某块草稿纸。
  • 通用寄存器:存着临时变量 a=1, b=2。

当你保存了这组寄存器,你就保存了一个“线程”。

这就是线程!这就是线程!这就是线程!这就是线程!这就是线程!这就是线程!这就是线程!

这只是应用级线程而已

所以你说“线程由寄存器决定”,这是绝对真理。CPU 切换线程,本质上就是换一组寄存器数值加载进去。

  • 当 CPU 不运行它时,它就是一组躺在内存里的寄存器数值(就像你把书签夹好,把书合上)。
  • 当 CPU 运行它时,这组数值被加载进 CPU,CPU按它的PC值找到要执行的指令,通过SP找到它的存储空间........。(涉及CPU执行线程步骤,暂时没弄懂
  1. 宏观上CPU是读者,进程就是书架上的一本书,线程就是书中被PC寄存器 + 栈指针 + 通用寄存器规定了的书中的一个段落
  2. 微观上(你的新理解):线程 =PC寄存器 + 栈指针 + 通用寄存器
    • 这组寄存器定义了“我是谁”、“我读到哪了”、“我的临时数据在哪”。
    • 代码区只是它们共同引用的“只读文本”。

1. PC寄存器 = 书签(决定“读哪一段”)

  • 作用:指向代码区(书)的具体行号。
  • 比喻:CPU 翻开书,书签夹在第 50 页,CPU 就从第 50 页开始读。

2. 栈指针 (SP) = 专属草稿纸(决定“在哪记笔记”)

  • 作用:指向线程私有的栈内存,用来存局部变量、函数调用记录。
  • 比喻:CPU 读这一章时,需要算数、记笔记。它不会把笔记写在书上(书是只读的),而是写在这张专属的草稿纸上。每个线程都有自己独立的一张草稿纸。

3. 通用寄存器 = 大脑的工作记忆(决定“手里正拿着什么”)

  • 作用:存放 CPU 正在计算的临时数据(比如a = 1 + 2里的 1、2 和结果 3)。
  • 比喻:CPU 读这一句时,手里正捏着的几个关键字

💡 终极推演:CPU 是如何“阅读”的?

现在把比喻连起来,看看操作系统是怎么工作的:

  1. 进程(书)被放到了书架上(加载到内存),里面有代码、有数据。
  2. 线程(阅读范围)被定义了:
    • 线程 A:书签夹在第 10 页,草稿纸是 A 纸。
    • 线程 B:书签夹在第 50 页,草稿纸是 B 纸。
  3. CPU(读者)开始工作:
    • 时间片 1:CPU 拿起书签 A,翻到第 10 页,拿起草稿纸 A,开始疯狂阅读和计算。
    • 时间片到了:CPU 突然被调度器叫停。它赶紧把书签 A 的位置更新,把草稿纸 A 放好,把手里的关键字放下(保存上下文)。
    • 时间片 2:CPU 拿起书签 B,翻到第 50 页,拿起草稿纸 B,继续读另一段内容(恢复上下文)。

线程确实就是由寄存器定义的执行上下文。所谓的“多线程并发”,在硬件层面,就是 CPU 飞快地在不同组的寄存器数值之间跳来跳去。


我的理解:
一:如果把进程的代码区当作一本书,那线程就是书中的一个段落,PC寄存器就是书签,CPU运行进程中的线程,就是每一段看一会,时间片到了,就更新书签到最新的位置,然后去看下一个书签位置看书,依次循环
二:线程是执行流,因为看”书“中的章节是一次过程,可以看很多遍,但书才是那个占了书架位置也就是资源的单位,所以进程中增加线程很轻松


代码区(Text Segment)是只读的,它是所有线程共享的“公共财产”。
不同的线程的区别在于PC寄存器(程序计数器)指向了哪里。
所有线程共用一块代码区
线程A的PC寄存器指向第10页(正在读函数A)。
线程B的PC寄存器指向第50页(正在读函数B)。
CPU调度时,它只关心该线程中的PC寄存器标在第几页。CPU通过不断切换PC寄存器,让同一段代码逻辑在不同线程的上下文中交替推进。

我想表达的是线程是寄存器标记好的一块章节。


注意:线程不是拥有自己的内存空间,而是每个线程都有自己“私有”的内存空间(栈),但它们同时共享同一块“公共”的内存空间(堆、代码区、全局变量)。

  • PC寄存器:每个人有自己的书签,读到哪一页互不干扰。
  • 栈指针 (SP) && 栈内存:每个人有自己的专属草稿纸。你在你的草稿纸上写 i=0,我在我的草稿纸上写 i=99,完全隔离,互不影响。
  • 通用寄存器:每个人手里捏着的临时计算值也是独立的。

⚠️ 关键点:正是因为“栈”是私有的,所以局部变量才是线程安全的。函数调用结束后,草稿纸一撕(栈帧弹出),数据就没了,不会污染别人。


线程“共享”的部分(大家共用)这部分属于进程,所有线程都在同一个屋檐下:

  • 代码区:大家读的是同一本书。不可能每个线程都复制一份二进制码,那样太浪费内存了。
  • 堆:大家共用的“大仓库”。比如 new 出来的对象、动态分配的内存,所有线程都能访问。
  • 全局/静态变量:写在书边上的“公共笔记”,谁都能看,谁都能改。

每个进程都有自己的 KLT 池
进程A有10个KLT,进程B有8个KLT,它们在内核调度器眼里是完全独立的调度实体。内核不会把进程A的KLT拿去给进程B用。
ULT 永远不能跨进程
进程A的ULT只能在进程A的KLT上运行,绝不可能“跑到”进程B的窗口上办事。ULT的生命周期被牢牢锁死在自己的进程内。
多核并行是“进程级+线程级”双重并行
进程A和进程B可以同时在不同CPU核心上运行(进程级并行)
进程A内部的多个KLT也可以同时在多个核心上运行(线程级并行)
两者叠加,才是现代系统真正的并发能力
IPC (进程间通信)是多进程的“唯一桥梁”
既然内存隔离了,进程间想交换数据就必须走“官方渠道”。这就像两个大厅之间不能直接喊话,必须通过大楼内部的传菜窗口(管道)、公告栏(共享内存段)或电话系统(Socket)来通信,且所有通信都经过内核监管。


KLT 到底是怎么“生”出来的?
KLT 的诞生只有两个途径,且都是进程主动发起的:
一、进程自己显式创建(最常见)
当你的代码调用线程创建 API (应用程序编程接口)

API是函数名,函数的具体实现在库里,系统调用在函数具体实现的某一行

时:
Linux: pthread_create()
Windows: CreateThread()
Java: new Thread().start()
这些系统调用会陷入内核,内核收到请求后:
在内核空间为该进程分配一个 TCB(线程控制块)
分配独立的内核栈(通常 8KB~16KB)
将该 KLT 加入该进程的线程列表和全局调度队列
返回一个线程 ID 给用户态
💡 关键点:这个 KLT 从出生那一刻起就绑定在该进程的地址空间内,但它占用的内核资源(TCB、内核栈)是从系统全局池里分配的,不是预先划好的“专属配额”。
二、进程启动时内核自动创建(仅一次)
当你执行 fork() + exec() 或 main() 启动时,内核会自动为这个新进程创建第一个 KLT(即主线程)。这是唯一一个“被动获得”的 KLT,之后所有的 KLT 都必须由进程自己主动申请。

KLT 的完整生命周期


四、进程调度

高级调度(作业调度):没在内存中->到内存中

中级调度(内存调度):在内存中->外存中 / 原先在内存中,现在在外存->内存中

低级调度(进程调度):在内存中->在CPU中

进程调度:决定哪个进程在什么时候使用CPU。


五、应用级线程与内核级线程

  1. 内核给进入内存的进程创建的PCB就是一个KLT,而线程是进程自己申请创建的PCB,就是ULT
  2. 在N:M模型中(M<N),一个进程有N个ULT,M个KLT,M个KLT就是内核给进程创建的PCB。刚学进程时,一个进程创建一个PCB或者说KLT来接受CPU的调度。但这样只能串行的执行,如果想要让一个进程中的不同的程序要同时运行,就要创建许多PCB(一对一模型就是这样的),而在线程引入后,要想运行哪些程序,进程就在自己内部创建PCB或者说ULT,向内核申请M个KLT,让N个ULT动态的轮流塞进KLT中,这样就能得到CPU的调度,从而做到多线程并发
  3. 引入线程前和引入线程后的变化是:一个进程需要由内核创建一个PCB到一个进程需要创建多个PCB,线程轮流绑定这个KLT让CPU调度执行
  4. KLT 和 ULT 在底层结构上的巨大差异,虽然它们的核心都是寄存器,但它们在内存里长得不一样:
    • ULT(用户级线程)的 PCB:它非常轻量。在内存里,它通常就是一个小结构体,包含:一组寄存器快照 + 一块几 KB 的用户栈。就这么多。它不需要管操作系统,所以极其小巧。
    • KLT(内核级线程)的 PCB:它极其庞大。在 Linux 里,它叫 task_struct 。除了一组寄存器快照 + 一块内核栈之外,它还包含了大量内核态的“家当”:
      • 线程的状态(运行中、睡眠中、僵尸态等)
      • 优先级和调度信息(vruntime)
      • 打开的文件列表(fd)
      • 信号处理机制
      • 所属的进程组、用户组等。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询