
一、线程进程服务大厅线程大厅中的100个人线程库大厅中的管理员负责把进程申请的内核级线程分配给线程内核级线程大厅中的窗口CPU调度的单位CPU的一个时间片/执行流可以根据人流量来动态创建和销毁的。人正在窗口办事一个应用线程绑定了一个内核级线程CPU:会一个一个处理窗口内核级线程工作的闪电侠当应用级线程发生IO阻塞时这个窗口的工作就会暂停内核只知道这个内核级线程窗口阻塞了于是把这个KLT挂起切换到另一个KLT排队等待CPU来处理用户态的线程库会检测到当前这个ULT发生了阻塞调用它会主动把该ULT从KLT上解绑然后立刻从候客区拉一个新的ULT绑定到这个KLT上或者绑定到另一个空闲KLT上。解释一下这里的解绑和绑定绑定把当前 ULT 的运行状态程序计数器 PC、栈指针 SP、以及它需要执行的函数地址写入这个 KLT 的寄存器上下文中。解绑场景这个KLT 正在执行 ULT-A突然 ULT-A 说“我要去读个文件可能要等一会儿。”怎么做重点来了保存现场用户态调度器立刻把当前 KLT 里的寄存器状态PC、SP原封不动地拷贝回 ULT-A 自己的内存空间用户态 PCB里保存起来。寻找替身调度器立刻从等待队列里捞出一个 ULT-B。恢复现场调度器把 ULT-B 之前保存的寄存器状态直接覆盖到当前这个 KLT 的寄存器里。继续执行CPU 根本不知道刚才发生了什么它只看到自己手里的 KLT 突然变了个样于是继续顺着新的指令往下执行。这就像快递车KLT开到半路把车上的货物 AULT-A卸在仓库然后立刻从仓库拿出货物 BULT-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寄存器 和 栈指针PC寄存器里面存的是下一条要执行的代码的内存地址。一个线程的栈内存里保存着属于这个线程的局部变量和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栈指针指向内存里的某块草稿纸。通用寄存器存着临时变量 a1, b2。当你保存了这组寄存器你就保存了一个“线程”。这就是线程这就是线程这就是线程这就是线程这就是线程这就是线程这就是线程这只是应用级线程而已所以你说“线程由寄存器决定”这是绝对真理。CPU 切换线程本质上就是换一组寄存器数值加载进去。当 CPU 不运行它时它就是一组躺在内存里的寄存器数值就像你把书签夹好把书合上。当 CPU 运行它时这组数值被加载进 CPUCPU按它的PC值找到要执行的指令通过SP找到它的存储空间........。涉及CPU执行线程步骤暂时没弄懂宏观上CPU是读者进程就是书架上的一本书线程就是书中被PC寄存器 栈指针 通用寄存器规定了的书中的一个段落。微观上你的新理解线程 PC寄存器 栈指针 通用寄存器。这组寄存器定义了“我是谁”、“我读到哪了”、“我的临时数据在哪”。代码区只是它们共同引用的“只读文本”。1. PC寄存器 书签决定“读哪一段”作用指向代码区书的具体行号。比喻CPU 翻开书书签夹在第 50 页CPU 就从第 50 页开始读。2. 栈指针 (SP) 专属草稿纸决定“在哪记笔记”作用指向线程私有的栈内存用来存局部变量、函数调用记录。比喻CPU 读这一章时需要算数、记笔记。它不会把笔记写在书上书是只读的而是写在这张专属的草稿纸上。每个线程都有自己独立的一张草稿纸。3. 通用寄存器 大脑的工作记忆决定“手里正拿着什么”作用存放 CPU 正在计算的临时数据比如a 1 2里的 1、2 和结果 3。比喻CPU 读这一句时手里正捏着的几个关键字。 终极推演CPU 是如何“阅读”的现在把比喻连起来看看操作系统是怎么工作的进程书被放到了书架上加载到内存里面有代码、有数据。线程阅读范围被定义了线程 A书签夹在第 10 页草稿纸是 A 纸。线程 B书签夹在第 50 页草稿纸是 B 纸。CPU读者开始工作时间片 1CPU 拿起书签 A翻到第 10 页拿起草稿纸 A开始疯狂阅读和计算。时间片到了CPU 突然被调度器叫停。它赶紧把书签 A 的位置更新把草稿纸 A 放好把手里的关键字放下保存上下文。时间片 2CPU 拿起书签 B翻到第 50 页拿起草稿纸 B继续读另一段内容恢复上下文。线程确实就是由寄存器定义的执行上下文。所谓的“多线程并发”在硬件层面就是 CPU 飞快地在不同组的寄存器数值之间跳来跳去。我的理解一如果把进程的代码区当作一本书那线程就是书中的一个段落PC寄存器就是书签CPU运行进程中的线程就是每一段看一会时间片到了就更新书签到最新的位置然后去看下一个书签位置看书依次循环二线程是执行流因为看”书“中的章节是一次过程可以看很多遍但书才是那个占了书架位置也就是资源的单位所以进程中增加线程很轻松代码区Text Segment是只读的它是所有线程共享的“公共财产”。不同的线程的区别在于PC寄存器程序计数器指向了哪里。所有线程共用一块代码区线程A的PC寄存器指向第10页正在读函数A。线程B的PC寄存器指向第50页正在读函数B。CPU调度时它只关心该线程中的PC寄存器标在第几页。CPU通过不断切换PC寄存器让同一段代码逻辑在不同线程的上下文中交替推进。我想表达的是线程是寄存器标记好的一块章节。注意线程不是拥有自己的内存空间而是每个线程都有自己“私有”的内存空间栈但它们同时共享同一块“公共”的内存空间堆、代码区、全局变量。PC寄存器每个人有自己的书签读到哪一页互不干扰。栈指针 (SP) 栈内存每个人有自己的专属草稿纸。你在你的草稿纸上写 i0我在我的草稿纸上写 i99完全隔离互不影响。通用寄存器每个人手里捏着的临时计算值也是独立的。⚠️ 关键点正是因为“栈”是私有的所以局部变量才是线程安全的。函数调用结束后草稿纸一撕栈帧弹出数据就没了不会污染别人。线程“共享”的部分大家共用这部分属于进程所有线程都在同一个屋檐下代码区大家读的是同一本书。不可能每个线程都复制一份二进制码那样太浪费内存了。堆大家共用的“大仓库”。比如 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。五、应用级线程与内核级线程内核给进入内存的进程创建的PCB就是一个KLT而线程是进程自己申请创建的PCB就是ULT在NM模型中MN)一个进程有N个ULTM个KLTM个KLT就是内核给进程创建的PCB。刚学进程时一个进程创建一个PCB或者说KLT来接受CPU的调度。但这样只能串行的执行如果想要让一个进程中的不同的程序要同时运行就要创建许多PCB一对一模型就是这样的而在线程引入后要想运行哪些程序进程就在自己内部创建PCB或者说ULT向内核申请M个KLT让N个ULT动态的轮流塞进KLT中这样就能得到CPU的调度从而做到多线程并发引入线程前和引入线程后的变化是一个进程需要由内核创建一个PCB到一个进程需要创建多个PCB线程轮流绑定这个KLT让CPU调度执行KLT 和 ULT 在底层结构上的巨大差异虽然它们的核心都是寄存器但它们在内存里长得不一样ULT用户级线程的 PCB它非常轻量。在内存里它通常就是一个小结构体包含一组寄存器快照 一块几 KB 的用户栈。就这么多。它不需要管操作系统所以极其小巧。KLT内核级线程的 PCB它极其庞大。在 Linux 里它叫 task_struct 。除了一组寄存器快照 一块内核栈之外它还包含了大量内核态的“家当”线程的状态运行中、睡眠中、僵尸态等优先级和调度信息vruntime打开的文件列表fd信号处理机制所属的进程组、用户组等。