Files
Operating-System/Thread/kernel_or_user.md
2026-03-22 17:40:44 +08:00

8.6 KiB
Raw Permalink Blame History

这是一个非常深入的问题。在虚拟内存空间的视角下,用户级线程和内核级线程的**“可见性”布局**完全不同。

1. 虚拟内存空间的变化

在理解变化之前,我们要明确一个核心点:所有的线程都共享所属进程的地址空间(代码段、全局数据、堆),但每个线程必须拥有独立的栈 (Stack)

用户级线程 (ULT) 的空间布局

对于内核来说,整个进程只有一个堆栈。用户级线程库(如早期的线程库)会在进程的堆 (Heap) 区或者数据段中手动划分出几块区域,作为各个用户线程的“私有栈”。

  • 内核视角:只看到一个连续的地址空间和一个栈指针。
  • 变化:创建线程时,虚拟内存空间没有实质性的系统级变化,只是在用户态内存中多了几块被当作栈使用的区域。

内核级线程 (KLT/Linux pthread) 的空间布局

在 Linux 中,每个 pthread 都是一个独立的调度实体。

  • 内核视角:内核通过 mmap 系统调用在进程的虚拟地址空间中(通常在栈和堆之间的共享映射区)为新线程分配一块独立的内存作为用户栈
  • 变化:每创建一个线程,虚拟内存中就会多出一个独立的栈区域,且内核会为该线程维护一个专门的内核栈(位于内核空间),用于处理该线程进入内核态时的执行环境。

2. 空间布局对比图

下面通过图示展示两者在虚拟内存中的分布差异:

用户级线程 (ULT) 模型

内核只认为这是一个单线程进程,线程库在内部“模拟”分片。

内核级线程 (KLT/Linux pthread) 模型

每个线程在进程地址空间内拥有独立的栈,且在内核空间有对应的内核栈。


3. 核心区别总结表

特性 用户级线程 (ULT) 内核级线程 (Linux pthread)
用户栈位置 通常在进程的堆区或静态内存区 在进程地址空间的映射区 (mmap)
内核栈 整个进程共用一个内核栈 每个线程拥有独立的内核栈
栈保护 很难实现硬件级的栈溢出保护 内核可以在栈末端设置 Guard Page(保护页)
页表变化 基本无变化 内核会更新任务描述符,分配新的栈内存映射

为什么 Linux 这么做?

这种设计允许每个线程独立地陷入内核(比如一个线程在读磁盘,另一个线程在做计算)。如果共用栈,当多个线程同时触发系统调用时,内核将无法区分和保存它们的上下文,导致程序崩溃。

既然提到了 mmap 分配栈空间,你想了解一下 Linux 是如何通过 Guard Page(红区)来防止线程栈溢出导致内存破坏的吗?


这个问题问得很关键,而且很多人这里会“想歪”——以为线程会改变虚拟内存布局,但其实核心点是:

👉 线程 ≠ 独立虚拟内存空间(在同一进程内) 👉 真正改变虚拟内存结构的是“进程”,不是线程

我给你用“进程 + 用户线程 + 内核线程”的对比图讲清楚👇


一、先给结论(你脑子里要有这个模型)

  • 用户线程pthread / std::thread

    • 共享整个进程的虚拟内存
    • 只新增:👉 线程栈stack
  • 内核线程Linux task_struct

    • 也共享同一个虚拟地址空间(同一个 mm_struct
    • 内核里有独立的 kernel stack
    • 用户空间 看不出来区别

二、一个进程(初始状态)

假设你运行一个简单程序:

int main() { while(1); }

虚拟内存是这样👇

用户虚拟地址空间Process A
┌──────────────────────────┐ 高地址
│        栈 (main线程)       │
│--------------------------│
│        共享库 (libc)      │
│--------------------------│
│           堆             │
│--------------------------│
│     数据段 (.data/.bss)   │
│--------------------------│
│        代码段 (.text)     │
└──────────────────────────┘ 低地址

┌──────────────────────────┐
│      内核空间(所有进程共享) │
└──────────────────────────┘

三、创建“用户线程”之后(重点!)

比如你调用:

pthread_create(...)

👉 虚拟内存变化如下:

用户虚拟地址空间(仍然是同一个!)
┌──────────────────────────┐
│     线程2 栈 (new thread) │  ← 新增
│--------------------------│
│     线程1 栈 (main)       │
│--------------------------│
│        共享库             │
│--------------------------│
│           堆              │ ← 所有线程共享
│--------------------------│
│        数据段             │ ← 共享
│--------------------------│
│        代码段             │ ← 共享
└──────────────────────────┘

关键点:

  • 只有 stack 增加了
  • 其他全部共享
  • 没有新的虚拟地址空间

四、那“内核线程”在这里长什么样?

现在重点来了,你最关心的👇

Linux里 pthread_create 实际上是:

👉 clone() 创建一个 内核调度实体task_struct

但它的参数是:

CLONE_VM  ← 共享虚拟内存

👉 所以结果是:

用户空间(你能看到的)👇

完全一样 👇

┌──────────────────────────┐
│   线程栈(多个)          │
│--------------------------│
│   堆 / 数据 / 代码(共享) │
└──────────────────────────┘

👉 你根本看不出“内核线程 vs 用户线程”的区别


👉 内核视角(真实情况)👇

内核里其实是这样:

task_struct (Thread 1)
  ├── mm_struct ─────┐
  ├── kernel stack   │
                     │  ← 共享
task_struct (Thread 2)
  ├── mm_struct ─────┘
  ├── kernel stack

五、用一张“总图”帮你彻底打通

                用户空间(一个进程)
┌────────────────────────────────────┐
│  线程1栈    线程2栈    线程3栈      │
│------------------------------------│
│        堆malloc  ←共享           │
│------------------------------------│
│        全局变量      ←共享           │
│------------------------------------│
│        代码段        ←共享           │
└────────────────────────────────────┘

                ↓ 系统调用

┌────────────────────────────────────┐
│            Linux 内核              │
│------------------------------------│
│ task_struct (线程1)                │
│   ├ kernel stack                  │
│   ├ mm_struct (指向同一块) ◄──────┐
│------------------------------------│
│ task_struct (线程2)                │
│   ├ kernel stack                  │
│   ├ mm_struct (同一个) ────────────┘
└────────────────────────────────────┘

六、那“用户线程 vs 内核线程”真正的区别在哪?

不是在虚拟内存,而是在👇

1 调度权

  • 内核线程:内核调度
  • 用户线程:用户态调度(如协程)

2 是否能并行

  • 内核线程: 多核并行
  • 用户线程: 单核(除非映射到内核线程)

3 是否进入内核

  • 内核线程:直接参与调度
  • 用户线程:需要 runtimeGo / lib

七、一句话总结(帮你压缩成记忆点)

👉 线程不会创建新的虚拟内存空间,只会在已有空间中“加栈”

👉 Linux里所谓“线程”本质就是共享 mm_struct 的多个 task_struct


如果你还想更狠一点

我可以给你画一个更底层的版本:

👉 包含页表 / TLB / CR3 / 上下文切换时寄存器变化的那种图

那个会直接把你带到“操作系统高手区” 😏