Linux之线程概念

[复制链接]
发表于 2026-10-3 14:26:21 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

Linux 线程概念:从堆区细粒度划分讲到 task_struct

线程是面试和 Linux 后台开发绕不开的话题。这篇按"为什么需要线程"这条线讲:先看 OS 怎么把堆区、物理内存做细粒度管理,再看虚拟地址怎么映射到物理地址,最后落到线程是什么、优缺点、以及 Linux 为什么用进程 PCB 来模拟线程。

一、先理解 OS 的细粒度管理

1. 堆区的细粒度划分

C/C++ 里用户自己申请的空间在地址空间的堆区上。但堆区是一整块连续空间,每次 malloc 只划走一小块,你拿到的只是起始地址。问题来了:申请多次之后,你怎么知道第一次申请的是从堆区哪里到哪里、第二次又从哪里到哪里?

所以 OS 必须精细管理堆区。Linux 下每从堆区申请一块空间,OS 就创建一个 struct vm_area_struct,里面记录这块空间的起止位置等属性。于是"管理用户申请的空间"就变成了"管理一个 VMA 链表"——链表整体范围落在堆区范围内。这样每一小块空间的归属一目了然。

2. 物理内存与可执行程序的细粒度划分

先明确三件事:第一,a.exe 是个文件;第二,它在磁盘上已经按地址空间的方式编译好了;第三,它已经被切成若干个 4KB 的小块,叫页帧(page frame)。

进程刚启动时,OS 先建 PCB、地址空间、页表,但还没通过页表建立虚拟地址到物理地址的联系,而是让地址空间直接关联到磁盘上的可执行程序。页表里有一列标记数据是否在内存(通常着色显示)。执行到具体代码时进程必然要通过页表访问物理内存,一看标记"不在内存",OS 就把 a.exe 那一段载入物理内存,再通过页表与地址空间建立联系——这个过程叫缺页中断(page fault)。

物理内存同样被切成 4KB 的小格子,叫页框(page frame)。载入过程就是做 IO:把 4KB 的页帧放进 4KB 的页框。OS 也要管理这些页框——每个页框对应一个 struct page,里面有各种属性,其中 flag 成员标记该页框是否被占用;这些结构体存进 struct page mem[] 数组,数组下标就是页框编号。

3. 虚拟地址到物理地址的转换

我们平时说的地址是虚拟地址,共 32 个比特位(经典两/三级页表模型下)。页表是 key-value 结构:用前 10 位找到一级页表的 key,靠 value 定位到二级页表;再用中间 10 位在二级页表里找到 key,value 给出物理页的起始地址;最后用后 12 位做页内偏移,定位到该页中的确切位置。也就说一个虚拟地址靠"两级页表 + 页内偏移"完成一次地址翻译。

二、线程的概念

1. 基本定义

  • 一个程序里的一条执行路径就叫线程(thread)。更准确的说法:线程是进程内部的一个控制序列;每个进程至少有一个执行线程。
  • 线程在进程内部运行,本质上是在该进程的地址空间内运行。
  • 透过进程虚拟地址空间看得到进程的大部分资源,把资源合理分配给每个执行流,就形成线程执行流。

每个进程都有自己独立的地址空间和独立页表,进程的独立性由此而来。那么换个思路:创建"进程"时只新建一个 PCB,让它和已有 PCB 共享地址空间、页表等资源——这就得到了线程。每个线程是进程里的一个执行流,拥有并使用该进程的一部分资源。

所以结论是:从内核角度,进程是分配系统资源的基本实体(线程的资源都是从进程那里拿的);从用户角度,进程 = 一个或多个执行流(PCB)+ 地址空间 + 页表等内核数据结构 + 内存中的代码和数据。

而 线程是 CPU 调度的基本单位。CPU 早期拿的是 PCB 来决定调度谁;现在拿的是一个线程,因为 Linux 以 task_struct 为单位调度,而这个 task_struct 比过去的进程 PCB 更轻量化(Linux 下进程也叫轻量级进程),每个 task_struct 只管理进程的一部分资源,是进程下的一个执行流。

2. Linux 用 PCB 模拟线程

Linux 设计者并没有为线程单独设计一个类似进程 PCB 的数据结构。原因是线程所需属性和进程高度相似,直接复用 PCB 来表示内核里的"线程"最省事;真要专门抽象一个"线程"概念,OS 就得额外管一次"先描述再组织",维护成本上去了。Linux 内核里没有严格意义上的独立线程概念,是用进程 PCB 模拟出来的——这是 Linux 特有的设计选择。

3. 线程的优点

  • 创建一个新线程比创建新进程代价小得多(不用重新建地址空间、页表)。
  • 线程切换比进程切换要少做很多工作(页表不用换,TLB 不刷新)。
  • 线程占用的资源比进程少得多。
  • 能充分利用多处理器的可并行数目。
  • 等待慢速 I/O 的同时,程序可以执行其他计算任务。
  • 计算密集型应用:把计算拆到多线程,跑满多核。
  • I/O 密集型应用:让 I/O 重叠,一个线程等 I/O 时别的线程继续跑。

4. 线程的缺点

  • 性能损失:一个很少被阻塞的计算密集型线程,往往没法和其他线程共享同一个处理器。计算型线程数超过可用核数时,同步与调度开销反而拖慢整体——资源没多,开销先多了。
  • 健壮性降低:写多线程要考虑的面比单线程宽得多,一个时间分配的微小失误、或共享了不该共享的变量,影响面比想象中大,线程之间缺乏保护。
  • 缺乏访问控制:进程才是访问控制的基本粒度,一个线程里调用某些 OS 函数会对整个进程造成影响。
  • 编程难度提高:写和调多线程程序比单线程困痛苦得多(竞争、死锁、伪共享)。

5. 线程异常的影响

单个线程如果出现除零、野指针等问题导致线程崩溃,整个进程也会跟着崩。线程是进程的执行分支,线程出异常会触发信号机制终止进程,进程一终止,它内部所有线程随即退出。这个特性在写多线程服务时要格外小心——一个工作线程的段错误能带跑整个服务。

#include <pthread.h>
#include <unistd.h>
#include <iostream>
using namespace std;

void* thread_run(void* argc)
{
    while (true) {
        sleep(1);
        int a = 10;
        a /= 0;        // 除零:SIGFPE 发给线程所在进程
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_run, (void*)"thread1");
    while (true) {
        // 主线程也在跑
    }
}

三、小结

  • OS 靠 VMA 链表管堆区、靠 struct page 数组管页框、靠多级页表做地址翻译,这是理解虚拟内存的基础。
  • 线程 = 进程地址空间内的一条执行流,资源共享、调度单位是线程。
  • Linux 用 task_struct(PCB)模拟线程,所以"轻进程"这个说法才有意义。
  • 线程快但脆:切换便宜,一个崩全崩,同步全靠人自觉。

qidao123.com企服评测-应用市场 编辑提示

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表