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

三:操作系统线程管理之用户级线程与内核级线程

上一篇我们对线程概念、与进程的区别以及多线程优势的讨论,今天我们来深入探讨线程实现方式的幕后细节:用户级线程(User-Level Threads, ULT)内核级线程(Kernel-Level Threads, KLT),以及基于它们的不同线程模型

正如我们所知,线程是CPU调度的基本单位。但这个调度是由谁来完成呢?是由用户空间的库,还是由操作系统内核直接管理?不同的管理方式带来了不同的特性、优缺点,并由此衍生出了不同的线程模型。

揭秘线程的管理方式:用户说了算还是内核说了算?

1. 用户级线程 (User-Level Threads, ULT)

实现方式:

顾名思义,用户级线程是在用户空间实现的,操作系统内核对此一无所知。线程的管理工作(包括线程的创建、销毁、同步以及最重要的——调度)完全由一个用户级的线程库来完成。

想象一下,你在编写程序时,调用 create_thread()schedule_thread() 等函数,这些函数都是由这个用户级线程库提供的。当程序启动时,操作系统只会创建一个或少数几个标准的进程,并在其中运行一个或多个内核级线程(这取决于具体的线程模型)。这些内核级线程会运行用户空间的代码,而用户空间的线程库则在这个内核级线程内部管理众多的用户级线程。

举例:

一个早期的用户级线程库实现是 GNU Portable Threads (Pth)。使用这样的库时,你可以在你的C/C++代码中调用Pth提供的函数来创建、管理线程,但当你查看操作系统的进程列表时,可能只会看到你的程序对应的一个进程,以及该进程下的一个或少数几个(由OS调度)内核线程。

优缺点:

  • 优点:

    • 创建与销毁速度快: 因为完全在用户空间进行,无需调用操作系统内核函数,避免了系统调用带来的开销。就像在自家客厅里安排几个工人干活,不用去街道办事处(内核)报备。
    • 线程切换速度快: 用户级线程之间的切换也由线程库负责,仅涉及寄存器和栈的切换,同样无需内核介入,没有模式切换(用户态到内核态)的开销。就像在自家客厅里工人A放下手头的活,工人B拿起工具继续干,非常迅速。
    • 调度策略灵活: 用户级线程库可以实现各种复杂的、定制化的调度算法,因为它们运行在用户空间,不受内核调度策略的限制。
    • 与操作系统无关性强: 用户级线程库可以在不支持内核级线程的操作系统上实现多线程(尽管功能受限)。
  • 缺点:

    • 阻塞性系统调用会阻塞整个进程: 这是用户级线程最大的缺点。当一个用户级线程执行一个阻塞性的系统调用(如读文件、网络接收数据)时,因为它所在的内核级线程会因等待I/O而阻塞,而内核只知道这个内核级线程被阻塞了,它并不知道这个内核级线程内部还有其他用户级线程是可以运行的。因此,整个进程(包括所有其他用户级线程)都会被挂起,即使进程内有其他不相关的用户级线程可以执行。就像客厅里的一个工人出去办事(系统调用)被卡住了,虽然客厅里还有其他工人没事干,但因为街道办事处(内核)只知道你家(进程)派出的这个人(内核线程)被卡住了,就认为你家整个工厂(进程)都停工了,不给你家分配CPU时间了。
    • 无法利用多核CPU的优势: 由于内核只将进程调度到一个或少数几个内核级线程上运行,所有用户级线程都挤在这些内核级线程上执行。无论你的机器有多少个CPU核心,用户级线程都无法真正地并行运行,只能在少数几个核心上进行并发(通过快速切换)。
    • 调度依赖于内核: 用户级线程的调度是非抢占式的(至少在同一个进程内)。如果一个用户级线程陷入无限循环或长时间计算,它将独占其所在的内核级线程的CPU时间,导致同进程内的其他用户级线程得不到执行机会。这是因为内核只调度内核级线程,它不知道用户级线程的存在,无法像调度内核线程那样强制中断一个用户级线程的执行(除非该用户级线程主动放弃CPU或执行系统调用)。
    • 处理页面故障: 如果一个用户级线程发生页面故障(Page Fault),内核会将整个进程挂起,等待页面从磁盘加载。同样,同进程的其他用户级线程也无法运行。

2. 内核级线程 (Kernel-Level Threads, KLT)

实现方式:

内核级线程是在操作系统内核中实现的和管理的。内核知道每一个内核级线程的存在。线程的创建、销毁、同步和调度都由操作系统内核来完成。用户程序通过系统调用来请求内核进行线程操作。

举例:

现代操作系统,如 Windows、Linux (使用 NPTL, Native POSIX Thread Library)、macOS 等,都提供了内核级线程的支持。当你使用C++的 std::thread、Java的 Thread、Python的 threading 模块(在大多数实现中)创建线程时,底层通常都是通过操作系统提供的内核级线程接口来实现的。

优缺点:

  • 优点:

    • 可以利用多核CPU的优势: 内核可以将不同的内核级线程调度到不同的CPU核心上,实现真正的并行计算,显著提高多处理器系统的性能。就像工厂里派出的每个工人都直接向街道办事处(内核)登记了,街道办事处可以把不同工人安排到不同的工作区域(CPU核心)去干活。
    • 阻塞性系统调用不会阻塞整个进程: 当一个内核级线程执行阻塞性系统调用时,只有该线程会被阻塞。内核可以调度同一个进程中的其他非阻塞内核级线程继续执行。就像工厂里一个工人出去办事被卡住了,但其他工人是直接向街道办事处登记的,街道办事处知道还有其他工人在,可以继续给他们安排活干,工厂(进程)的其他部分不会停工。
    • 更好的调度公平性: 内核调度器可以公平地分配CPU时间给系统中所有进程和进程内的所有内核级线程。
    • 处理页面故障: 如果一个内核级线程发生页面故障,只有该线程被挂起,等待页面加载。同一进程中的其他线程可以继续运行。
  • 缺点:

    • 创建与销毁速度慢: 线程的创建和销毁需要通过系统调用,进入内核模式,由内核完成资源分配和管理,开销较大。就像工厂要新招一个工人需要去街道办事处(内核)走审批流程,比较慢。
    • 线程切换速度慢: 线程切换需要内核介入,涉及模式切换(用户态到内核态再回到用户态),开销比用户级线程切换大。就像工人A和B换班需要先去街道办事处备案并等其调度安排,流程比较复杂。
    • 内核开销: 管理和调度大量内核级线程会给内核带来额外的负担。

对比总结:

特性用户级线程 (ULT)内核级线程 (KLT)
管理者用户空间的线程库操作系统内核
内核感知无感知完全感知
创建/销毁快速
切换速度
阻塞调用阻塞整个进程只阻塞当前线程
多核利用无法利用可以利用
调度库定制,内核无感知(同进程内非抢占)内核调度,系统范围内公平
实现复杂度库实现较易(对应用开发者),内核无需改动涉及修改内核

线程模型:用户与内核的映射关系

了解了用户级线程和内核级线程后,我们可以看到它们各有优劣。为了试图结合两者的优点或适应不同的需求,人们提出了不同的线程模型,本质上是定义了用户级线程与内核级线程之间的映射关系

1. 多对一模型 (Many-to-One Model)

  • 描述: 多个用户级线程映射到一个或极少数个内核级线程。
  • 实现: 所有用户级线程的管理和调度都由用户空间的线程库在一个内核级线程上完成。
  • 关系: 这实际上就是纯用户级线程的实现方式。一个进程只对应一个内核级线程。
  • 优缺点: 同用户级线程的优缺点。
  • 比喻: 一个大房间里有很多工人在干活(用户级线程),但整个房间只通过一个窗口与外界(内核)联系,并且只有一个“房间主管”(内核级线程)负责把里面的情况汇报给“街道办事处”(内核)。一旦房间主管出去办事被卡住,整个房间里的工人都得停下来。
  • 示例: GNU Pth, Solaris 的 Green Threads (已废弃)。

2. 一对一模型 (One-to-One Model)

  • 描述: 每个用户级线程都映射到一个独立的内核级线程。
  • 实现: 用户在应用程序中创建的每一个线程,都会向内核请求创建一个相应的内核级线程。线程的管理和调度完全由内核负责。
  • 关系: 这实际上就是纯内核级线程的实现方式。进程中的每一个线程都由操作系统调度。
  • 优缺点: 同内核级线程的优缺点。
  • 比喻: 房间里的每个工人都直接向“街道办事处”(内核)登记,并且都有一个专属的通道与街道办事处沟通。街道办事处直接管理每一个工人。即使一个工人去办事被卡住,街道办事处知道还有其他工人,可以继续安排他们工作,并且可以把不同的工人安排到不同的工作区域(CPU核心)。
  • 示例: 现代 Linux (NPTL)、Windows、macOS。这是目前最常用的线程模型,特别是在多核系统普及后。

3. 多对多模型 (Many-to-Many Model)

  • 描述: 多个用户级线程映射到数量小于或等于用户级线程数的内核级线程。
  • 实现: 这是一个更灵活的模型。用户空间的线程库创建和管理用户级线程,并将它们“绑定”到一组内核级线程上。内核负责调度这些内核级线程,而用户级线程库则负责在这些可用的内核级线程之间调度和切换用户级线程。
  • 关系: 试图结合前两者的优点。既享受用户级线程切换的快速性,又能利用内核级线程的多核并行和非阻塞特性。
  • 优缺点:
    • 优点:
      • 克服了多对一模型的阻塞问题(因为有多个内核线程)。
      • 可以利用多核CPU(最多可用的内核线程数)。
      • 用户级线程切换开销小于一对一模型。
      • 对应用开发者而言,可以创建大量的用户级线程,而无需担心创建太多内核线程导致的系统开销。
    • 缺点:
      • 实现最复杂,需要在用户空间和内核之间进行协调。
      • 性能提升可能不如预期,取决于线程库和内核调度器的协调效率。
  • 比喻: 房间里有很多工人(用户级线程),房间里有几个“房间主管”(内核级线程,数量比工人少)。工人之间换班(用户级切换)很快,不需要街道办事处介入。街道办事处只管理这几个房间主管,可以把不同的房间主管安排到不同的工作区域(CPU核心),并且某个主管去办事被卡住时,不影响其他主管和他们管理的工人。
  • 示例: Solaris (早期版本)、一些高性能计算环境、Go 语言的 Goroutines 在运行时层面实现了类似多对多(或更准确说是M:N调度)的模型,但Go的实现更高级,有自己的调度器和轻量级协程。

哪种模型更胜一筹?现代的选择

历史上,不同的操作系统和编程语言运行时尝试过这几种模型。

  • 早期的系统或对内核要求较低的环境可能采用多对一模型。
  • 多对多模型在理论上提供了一个很好的平衡,但其实现和维护复杂性较高。
  • 随着多核处理器的普及和操作系统内核对线程管理的性能优化,一对一模型由于其直接、清晰的映射关系以及能够充分利用多核资源的优势,成为了现代主流操作系统和编程语言(如Java, C#, Python, C++11及更高版本等)的默认或首选线程实现模型。它虽然创建和切换开销相对用户级线程大,但相比于进程已大幅降低,且其能够真正实现并行计算的优点在多核时代变得尤为突出,弥补了其缺点。

结语

用户级线程和内核级线程是实现多线程的两种基本方式,它们各自的优缺点直接影响了在其上构建的线程模型的特性。从简单的多对一,到直接的一对一,再到复杂的协调型多对多,这些模型是操作系统和运行时环境在性能、灵活性、资源开销和并发能力之间进行权衡的体现。在当今多核普及的时代,一对一模型凭借其充分利用并行能力的优势,已成为最常见的选择。理解这些底层机制,有助于我们更好地理解多线程程序的行为,并在需要时做出合适的架构选择。

相关文章:

  • Milvus(25):搜索迭代器、使用分区密钥
  • 为实时数据构建WebSocket解决方案的挑战
  • Git在与远程仓库建立连接时,不小心输错密码导致连接失败,之后无法弹出用户名密码的输入框解决方案
  • 面试题总结二
  • 记录一次修改nacos安全问题导致服务调用出现404
  • KnowCard:我的知识卡片生成器是怎么炼成的?
  • web中路径问题
  • 能力验证及大练兵活动第一期
  • LeetCode Hot100刷题——除自身以外数组的乘积
  • MyBatis-Plus-Join联表查询
  • C 语言学习笔记(函数)
  • 【Linux】第十九章 管理SELinux安全性
  • 【Linux驱动】Linux 按键驱动开发指南
  • 【回溯 剪支 状态压缩】# P10419 [蓝桥杯 2023 国 A] 01 游戏|普及+
  • 【第三篇】 SpringBoot项目中的属性配置
  • 动态规划(4)可视化理解:图形化思考
  • SparkSQL基本操作
  • ​在 ASP.NET 中,HTTP 处理程序(HttpHandler)是处理 HTTP 请求的核心组件​
  • 嵌入式通信协议(二)——IIC总线
  • Flink Table SQL
  • 最高法:依法惩治损害民营企业合法权益的串通投标行为
  • 三人在共享单车上印小广告被拘,北京警方专项打击非法小广告
  • 国家统计局:消费对我国经济增长的拉动有望持续增长
  • 习近平:坚持科学决策民主决策依法决策,高质量完成“十五五”规划编制工作
  • 1块钱解锁2万部微短剧还能日更,侵权盗版难题怎么破?
  • 李洋谈美国黑帮电影与黑帮文化