为每个线程单独的栈

发布: (2026年1月10日 GMT+8 16:31)
13 min read
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the full text of the post (the paragraphs, headings, and any non‑code content) in order to do so. Could you please paste the article’s content here? I’ll keep the source line unchanged and preserve all formatting, markdown, and code blocks as you requested.

核心概念

每个线程需要自己的栈,但同一进程内的线程共享 代码数据。操作系统内核负责协调一切。

什么是进程?

A process 是一个正在运行的程序,并且与其他进程相互隔离。
当你打开 Chrome、Spotify 等时,操作系统会创建一个进程。它从 一个线程主线程)开始,并拥有自己的内存空间。内核会创建一个 Process Control Block (PCB) 来跟踪该进程的所有信息。

什么是线程?

一个 线程 是进程内的执行路径。
多个线程可以存在于同一个进程中,并 共享代码和数据,但每个线程必须拥有其 独立的栈。内核为每个线程创建一个 线程控制块 (TCB) 来跟踪其状态。

进程内存布局

类型共享?备注
代码已编译的程序指令✅ 所有线程只读,固定大小
数据全局变量✅ 所有线程固定大小,已初始化
动态内存(malloc/new✅ 所有线程向上增长,由程序员管理
局部变量,函数调用❌ 每个线程向下增长,针对每个线程隔离

为什么要分离栈?

如果两个线程共享同一个栈,它们的函数调用会相互冲突并损坏对方的数据。

Thread A 调用函数 → 创建一个 stack frame。
Thread B 调用函数 → 将其 frame 添加到同一个栈 → frames 重叠 → 数据被覆盖。

Thread B 返回并弹出它的 frame 时,Thread A 的数据就会被损坏。

解决方案: 每个线程都有 自己的 stack 用于函数调用和局部变量。这使得线程能够同时执行不同的函数而不会相互干扰。

堆栈帧与 LIFO

堆栈帧遵循 LIFO(后进先出)顺序:

main() → functionA() → functionB()
  1. main() 调用 functionA() → 推入一个新帧。
  2. functionA() 调用 functionB() → 再推入一个帧。
  3. functionB() 返回 → 弹出其帧,显示 functionA() 的帧。
  4. functionA() 返回 → 弹出其帧,返回到 main()

每个线程都有自己的堆栈,因此多个线程可以同时调用相同的函数而不会相互干扰各自的帧。

线程创建:主线程与子线程

  • 当进程启动时,操作系统会自动创建 一个线程 —— 主线程 —— 它从 main() 开始执行。
  • 主线程可以使用 pthread_create() 创建额外的线程。这些线程称为 子线程
  • 创建后,在内核看来 所有线程都是平等的;主线程不再拥有任何特殊权限。

线程层次结构

Process (created by OS)

Main Thread (created automatically by kernel)
    ├─→ Sub‑thread 1 (created by main thread)
    ├─→ Sub‑thread 2 (created by any thread)
    └─→ Sub‑thread 3 (created by any thread)

每个子线程会获得:

  • 它自己的栈(由内核分配)
  • 它自己的 TCB(线程控制块)
  • 对进程的代码、数据和堆的共享访问

如果需要,子线程也可以创建更多子线程。

线程层次结构图

Music Player Example

ThreadResponsibility
Main thread处理 UI 和用户交互
Audio thread连续解码并播放音频
I/O thread从文件系统加载歌曲
Timer thread更新播放计时器显示

所有线程共享播放器的代码和数据(歌曲列表、设置等),但每个线程都有自己的栈用于局部变量和函数执行。内核会快速在它们之间切换,为每个线程分配时间片,从而使这些任务看起来是同时运行的。

操作系统内核:中心协调者

内核 是操作系统的核心管理者。它负责所有资源的分配和进程、线程的协调。

Kernel diagram

进程管理

  • 内核为每个进程创建一个 进程控制块(PCB)
  • PCB 存储进程 ID、状态(运行、等待、就绪等)、内存布局、文件描述符、信号处理程序以及其他元数据。
  • 这些信息使内核能够管理、隔离并保护每个进程。

线程管理

  • 对于每个线程,内核创建一个 线程控制块(TCB)
  • TCB 包含线程 ID、状态、CPU 寄存器(程序计数器、栈指针)、栈地址和大小、线程本地存储以及调度信息。
  • 内核使用 TCB 正确地调度和管理线程。

内存管理

  • 内核使用系统调用(如 mmap())为线程栈分配内存。
  • 它为每个栈保留虚拟地址空间,确保栈之间不重叠,并为每个线程的栈帧提供安全区域。

Thread Stack

内核在栈边界创建保护页,以检测栈溢出情况。
当线程实际使用其栈时,内核会按需分配物理内存 on demand.

CPU 调度

内核决定哪个线程在何种 CPU 核心上运行以及运行多长时间。
它使用调度算法在所有线程之间公平分配 CPU 时间。
当线程的时间片用尽或需要等待 I/O 时,内核会执行 上下文切换

Context Switching

在上下文切换期间,内核:

  1. 将当前线程的 CPU 状态(Program Counter、Stack Pointer 以及所有 CPU 寄存器)保存到线程的 TCB 中。
  2. 从下一个线程的 TCB 中加载已保存的状态到 CPU 寄存器。
  3. 从恢复后的 Program Counter 继续执行,实际上将控制权转移到下一个线程。

恢复后的线程会继续执行,就好像它从未被中断过一样。

默认堆栈大小(按操作系统)

(这些数值指的是 虚拟地址空间,而非物理内存。)

操作系统 / 运行时默认堆栈大小备注
Linux每线程 8 MB预留虚拟空间;物理内存仅在需要时分配。
Windows每线程 1 MB在稳定性与资源使用之间取得平衡。
macOS每线程 512 KB对内存更为保守。
Java每线程 1 MB由 JVM 管理。
Go每协程约 2 KB协程堆栈会动态增长/收缩;由 Go 运行时而非操作系统管理。

为什么有不同的大小?

  • Linux (8 MB) – 支持在复杂应用中进行深度递归和使用大型局部变量。
  • Windows (1 MB) – 在稳定性和内存消耗之间提供折中。
  • macOS (512 KB) – 体现了注重资源效率的设计理念。
  • Go (≈ 2 KB) – 小栈能够工作是因为 Go 运行时在内存管理和上下文切换方面比内核更高效。

虚拟内存 vs. 物理内存

当内核为线程预留 8 MB 的栈空间时,它仅 预留虚拟地址空间
物理 RAM 则 按需分配(需求分页)。

示例: 如果一个线程只使用了其 8 MB 栈中的 100 KB,那么实际消耗的 RAM 也只有约 100 KB;其余的 7.9 MB 保持未使用。

这种做法使得内核能够在不浪费内存的情况下分配大量栈,即使在拥有数百甚至数千个线程的系统上也是如此。

内核数据结构

进程控制块 (PCB)

  • 进程 ID
  • 进程状态
  • 内存布局(代码段、数据段、堆、栈)
  • 文件描述符表
  • 信号处理程序
  • 进程中所有线程的列表

PCB 让内核能够管理进程的生命周期和资源。

线程控制块 (TCB)

  • 线程 ID
  • 线程状态
  • CPU 寄存器(PC、SP 等)
  • 线程的 用户模式栈 的地址和大小
  • 线程局部存储信息
  • 调度优先级
  • 指向 内核栈 的指针

TCB 在上下文切换时用于保存和恢复线程状态。

内核栈 与 用户栈

用途所在位置
用户模式栈保存用户代码的局部变量、函数参数、返回地址等。进程的虚拟地址空间(例如 Linux 上的 8 MB)。
内核模式栈内核在处理系统调用、中断等时使用。内核内存(对用户代码受保护)。

当线程调用系统调用(例如 read()write())时,CPU 会切换到内核模式,加载该线程的内核栈指针,并在该栈上执行内核代码。调用结束后,CPU 返回用户模式并恢复用户模式栈指针。

关键要点

  • 进程是隔离的 – 每个进程都有自己的受保护内存空间。
  • 线程共享资源 – 代码、数据和堆对进程中的所有线程都是公共的。
  • 每个线程都有自己的栈 – 这是进程内部唯一的线程专属内存区域。
  • 主线程仅在初始时特殊 – 它从 main() 开始,但创建后所有线程都成为平等的同伴。
  • 线程创建 – 通过 pthread_create()(或等效的 API)完成。
  • 内核分配所有栈 – 默认大小因操作系统而异(Linux 8 MB,Windows 1 MB,macOS 512 KB)。
  • 内核管理调度 – 决定哪个线程运行、运行多长时间以及在何种 CPU 核心上。
  • 上下文切换实现多任务 – 快速切换在单核上产生并行执行的幻象。
  • 栈帧实现后进先出 – 函数调用压入帧,返回时弹出。
  • 需求分页节省内存 – 虚拟栈空间仅在实际使用时才占用物理内存。
  • TCB 跟踪所有线程状态 – 内核使用 TCB 在整个生命周期中管理线程。

了解这点的重要性

了解操作系统层面线程的工作原理是计算机科学的基础。它揭示了代码在硬件层面的实际执行方式。你不仅仅是在使用线程 API——而是理解使并发编程成为可能的真实机制。这一基础对于构建高效、安全的多线程应用以及掌握更高级的并发概念(如 Go 语言中的 goroutine)至关重要。

Back to Blog

相关文章

阅读更多 »