【Linux】线程概念与控制(一):线程本质与虚拟地址空间

文章目录

    • Linux线程概念与控制(一):线程本质与虚拟地址空间
    • 一、什么是线程
      • [1.1 线程的定义](#1.1 线程的定义)
      • [1.2 从进程到线程的演进](#1.2 从进程到线程的演进)
    • 二、虚拟地址空间与分页机制
      • [2.1 为什么需要虚拟地址空间](#2.1 为什么需要虚拟地址空间)
        • [2.1.1 没有虚拟内存的困境](#2.1.1 没有虚拟内存的困境)
        • [2.1.2 虚拟内存的解决方案](#2.1.2 虚拟内存的解决方案)
      • [2.2 物理内存管理](#2.2 物理内存管理)
        • [2.2.1 页和页框的概念](#2.2.1 页和页框的概念)
        • [2.2.2 struct page结构](#2.2.2 struct page结构)
      • [2.3 页表机制详解](#2.3 页表机制详解)
        • [2.3.1 单级页表的问题](#2.3.1 单级页表的问题)
        • [2.3.2 两级页表的设计](#2.3.2 两级页表的设计)
      • [2.4 地址转换全流程](#2.4 地址转换全流程)
        • [2.4.1 MMU的工作流程](#2.4.1 MMU的工作流程)
        • [2.4.2 TLB快表优化](#2.4.2 TLB快表优化)
      • [2.5 缺页异常](#2.5 缺页异常)
        • [2.5.1 什么是缺页异常](#2.5.1 什么是缺页异常)
        • [2.5.2 缺页异常的类型](#2.5.2 缺页异常的类型)
    • 三、线程的优缺点
      • [3.1 线程的优点](#3.1 线程的优点)
      • [3.2 线程的缺点](#3.2 线程的缺点)
      • [3.3 线程异常](#3.3 线程异常)
      • [3.4 线程的用途](#3.4 线程的用途)
    • 四、进程vs线程深入对比
      • [4.1 资源分配与调度](#4.1 资源分配与调度)
      • [4.2 线程私有的资源](#4.2 线程私有的资源)
      • [4.3 线程共享的进程资源](#4.3 线程共享的进程资源)
      • [4.4 理解"单线程进程"](#4.4 理解"单线程进程")
    • 五、本篇总结
    • 六、承上启下

Linux线程概念与控制(一):线程本质与虚拟地址空间

💬 开篇引入:在前面的进程学习中,我们已经掌握了进程的概念、创建、终止、等待等知识。但实际开发中,我们常常听到"多线程编程"这个词。线程到底是什么?它和进程有什么区别?为什么需要线程?更重要的是,线程究竟在进程的哪里运行?本篇将从最基础的线程概念出发,深入剖析虚拟地址空间与分页机制,为后续理解线程的地址空间布局打下坚实基础!

👍 学习目标:本篇是线程系列的第一篇,我们将理解线程的本质、掌握虚拟地址空间的分页机制、理解页表的工作原理、了解线程与进程的区别与联系。这些都是理解后续线程控制API的理论基础!

🚀 循序渐进:本篇从零开始,不需要线程的前置知识,但需要你掌握基本的进程概念。


一、什么是线程

1.1 线程的定义

在深入学习线程之前,我们先来看一个简单的定义:

bash 复制代码
线程(thread):在一个程序里的一个执行路线

更准确的定义:线程是"一个进程内部的控制序列"

这个定义可能有点抽象,我们用一个生活中的例子来理解:

bash 复制代码
把进程比作一个工厂:
- 工厂有厂房、设备、原材料(进程的资源)
- 工厂里的工人就是线程(执行流)

一个工厂可以有多个工人同时工作:
- 工人A在车间1生产零件
- 工人B在车间2组装产品  
- 工人C在仓库管理物料

这些工人共享工厂的资源(厂房、设备、原材料)
但每个工人有自己的工作台(线程栈)、工具(寄存器)

📌 核心理解:

bash 复制代码
1. 一切进程至少都有一个执行线程(主线程)
   - 单线程进程:只有main函数这一个执行流
   - 多线程进程:除了main函数,还有其他执行流

2. 线程在进程内部运行,本质是在进程地址空间内运行
   - 线程不能脱离进程单独存在
   - 线程共享进程的大部分资源

3. 在Linux系统中,线程的实现比较特殊
   - CPU眼中看到的PCB都是轻量级进程(LWP)
   - Linux不严格区分进程和线程

1.2 从进程到线程的演进

让我们回顾一下进程的概念,这样更容易理解线程:

进程的特点:

bash 复制代码
优点:
1. 进程之间相互独立,互不干扰
2. 进程崩溃不会影响其他进程
3. 资源隔离性好,安全性高

缺点:
1. 创建进程开销大(需要复制父进程的资源)
2. 进程间通信复杂(需要管道、共享内存等)
3. 进程切换开销大(需要切换地址空间)

正是因为进程的这些缺点,线程应运而生:

线程的设计目标:

bash 复制代码
1. 保留进程的并发执行能力
2. 降低创建和切换的开销
3. 简化"进程"间的通信(同一进程内的线程共享资源)

二、虚拟地址空间与分页机制

💡 重要提示:要理解线程在哪里运行,必须先深刻理解进程的虚拟地址空间!这一节是整篇文章的核心基础,请务必耐心阅读。

2.1 为什么需要虚拟地址空间

2.1.1 没有虚拟内存的困境

假设没有虚拟内存和分页机制,每个程序在物理内存上的空间必须是连续的:

bash 复制代码
物理内存(4GB):
┌─────────────────────────────────┐
│ 程序A(100MB)                    │  0x00000000 - 0x06400000
├─────────────────────────────────┤
│ 程序B(200MB)                    │  0x06400000 - 0x12C00000
├─────────────────────────────────┤
│ 程序C(50MB)                     │  0x12C00000 - 0x15E00000
├─────────────────────────────────┤
│ 空闲(3650MB)                    │  0x15E00000 - 0xFFFFFFFF
└─────────────────────────────────┘

这种方式会带来严重的问题:

bash 复制代码
问题1: 内存碎片
- 程序B退出后,中间留下200MB的空洞
- 如果新程序需要250MB连续空间,就无法使用这个空洞
- 即使总空闲内存够,但不连续就无法使用

问题2: 地址冲突
- 每个程序编译时不知道自己会被加载到哪里
- 程序A的代码可能写着"跳转到地址0x1000"
- 但0x1000可能已经被其他程序占用

问题3: 没有保护
- 程序A可以直接访问程序B的内存
- 恶意程序可以随意读写其他程序的数据
2.1.2 虚拟内存的解决方案

为了解决这些问题,操作系统引入了虚拟内存分页机制:

bash 复制代码
核心思想:
1. 给每个进程提供连续的虚拟地址空间(0 ~ 4GB on 32位)
2. 把物理内存按固定大小(4KB)切分成页框(Page Frame)
3. 把虚拟地址空间也按4KB切分成页(Page)
4. 通过页表建立虚拟页到物理页框的映射

好处:
✓ 每个进程看到的都是连续的地址空间
✓ 物理内存可以是离散的,解决碎片问题
✓ 不同进程的虚拟地址可以映射到不同物理地址,实现隔离

示意图:

bash 复制代码
进程A的虚拟地址空间          物理内存
┌────────────────┐           ┌────────────┐
│ 0x00000000     │           │  页框0     │
│ 0x00001000  ───┼──────────→│  页框1     │
│ 0x00002000  ───┼─────┐     │  页框2     │
│ 0x00003000     │     │     │  页框3  ←──┼─┐
│     ...        │     │     │  页框4     │ │
│ 0xFFFFFFFF     │     │     │  页框5  ←──┼─┤
└────────────────┘     │     │   ...      │ │
                       │     └────────────┘ │
进程B的虚拟地址空间    │                     │
┌────────────────┐     │                     │
│ 0x00000000     │     │                     │
│ 0x00001000  ───┼─────┤                     │
│ 0x00002000  ───┼─────┘                     │
│     ...        │                           │
└────────────────┘                           

两个进程的虚拟地址0x00001000映射到不同的物理页框
实现了地址空间的隔离!

2.2 物理内存管理

2.2.1 页和页框的概念
bash 复制代码
页框(Page Frame):
- 物理内存按固定长度划分的存储区域
- 在32位系统中,通常是4KB
- 页框是物理内存的最小分配单位

页(Page):
- 虚拟地址空间按固定长度划分的数据块
- 大小等于页框(4KB)
- 页可以存放在任何页框或磁盘中

重要区分:
- 页框是"容器"(物理内存中的位置)
- 页是"内容"(虚拟地址空间的数据)

计算页框数量:

bash 复制代码
假设物理内存有4GB:
页框大小 = 4KB = 4096 Bytes
页框数量 = 4GB / 4KB 
        = 4 * 1024 * 1024 KB / 4KB
        = 1048576 个页框
        = 1M 个页框
2.2.2 struct page结构

操作系统需要管理这么多页框,Linux内核用struct page结构表示每个物理页:

c 复制代码
/* 简化版的 struct page */
struct page {
    unsigned long flags;          // 页的状态标志
    atomic_t _mapcount;           // 页表映射计数
    void *virtual;                // 虚拟地址(如果已映射)
    
    // 页可能在不同的链表中
    struct list_head lru;         // 用于页面回收
    struct address_space *mapping;// 页的所有者
    pgoff_t index;                // 在文件中的偏移
    // ... 更多字段
};

📌 关键理解:

bash 复制代码
1. 每个物理页框都有一个struct page结构来描述
2. 这个结构本身也要占用内存

计算开销:
- 假设struct page占40字节
- 1M个页框需要 1048576 * 40 = 40MB
- 相对于4GB内存,只占1%,代价可以接受

3. flags字段记录页的状态:
   - PG_locked: 页是否被锁定
   - PG_uptodate: 页数据是否有效
   - PG_dirty: 页是否被修改过
   
4. _mapcount记录有多少个页表项指向这个页
   - -1表示没有人使用,可以回收
   - >= 0表示正在被使用

2.3 页表机制详解

2.3.1 单级页表的问题

有了物理页框的管理,接下来需要建立虚拟地址到物理地址的映射,这就是页表的作用。

我们先看最简单的单级页表:

bash 复制代码
32位系统的虚拟地址空间: 4GB = 2^32 字节
页的大小: 4KB = 2^12 字节
需要的页表项数量: 2^32 / 2^12 = 2^20 = 1048576 项

每个页表项占4字节(存储物理页框号)
页表总大小: 1048576 * 4 = 4MB

单级页表结构:

bash 复制代码
虚拟地址(32位):
┌────────────────────┬────────────────┐
│  页号(20位)        │  页内偏移(12位) │
└────────────────────┴────────────────┘

页表:
┌────────┬──────────────────┐
│ 索引   │  物理页框号      │
├────────┼──────────────────┤
│   0    │  0x12345         │ ← 虚拟页0映射到物理页框0x12345
│   1    │  0xABCDE         │
│   2    │  0x67890         │
│  ...   │  ...             │
│1048575 │  0x11111         │
└────────┴──────────────────┘

地址转换:
1. 取虚拟地址的高20位作为页号
2. 在页表中查找页号对应的物理页框号
3. 物理地址 = 物理页框号 << 12 | 页内偏移

在这里插入图片描述

单级页表的问题:

bash 复制代码
问题1: 需要连续的物理内存
- 4MB的页表需要1024个连续的物理页框
- 这和我们引入分页机制的初衷矛盾!

问题2: 空间浪费
- 大部分程序只使用很小一部分虚拟地址空间
- 例如只用了10MB,实际只需要3个页表项
- 但必须分配完整的4MB页表,浪费严重

问题3: 无法按需分配
- 程序启动时就要分配全部页表
- 不能随着程序运行动态增长
2.3.2 两级页表的设计

为了解决单级页表的问题,引入两级页表 :

bash 复制代码
核心思想:
把4MB的大页表拆分成1024个小页表,每个小页表4KB

1024个小页表 * 4KB = 4MB (总容量不变)
但现在可以:
1. 按需分配小页表(程序用多少分配多少)
2. 小页表可以离散存放(每个只需要连续的4KB)
3. 用一个页目录表来管理这1024个小页表

两级页表结构

text 复制代码
页目录表(Page Directory):
┌────────┬──────────────────┐
│ 索引   │  页表物理地址    │
├────────┼──────────────────┤
│   0    │  页表0的地址     │
│   1    │  页表1的地址     │
│   2    │  NULL(未分配)    │
│  ...   │  ...             │
│  1023  │  NULL(未分配)    │
└────────┴──────────────────┘
   ↑
   │
 CR3 寄存器指向页目录表

虚拟地址结构(32 位,两级页表)

text 复制代码
┌────────────┬────────────┬─────────────┐
│一级页号(10)│二级页号(10)│页内偏移(12) │
└────────────┴────────────┴─────────────┘
 页目录索引   页表索引     字节偏移

示例:虚拟地址 0x00401234

1️⃣ 转成二进制(32 位)

bash 复制代码
00000000 01000000 00010010 00110100

10-10-12 划分:

bash 复制代码
0000000001 | 0000000001 | 001000110100
   ↑            ↑              ↑
 一级页号=1    二级页号=1      页内偏移=0x234

2️⃣ 地址转换过程

text 复制代码
一级页号 = 1
→ 查页目录表[1]
→ 得到 页表1 的物理地址

二级页号 = 1
→ 查页表1[1]
→ 得到 物理页框号

页内偏移 = 0x234

物理地址 = (物理页框号 << 12) | 0x234

📌 两级页表的优势:

bash 复制代码
优势1: 按需分配
- 10MB的程序只需要3个页表(12KB)
- 不需要的虚拟地址区域,对应的页表根本不分配

优势2: 离散存储
- 每个页表只需要连续的4KB
- 页目录也只需要4KB
- 容易在物理内存中找到空间

优势3: 节省内存
- 大部分进程使用的虚拟地址空间很小
- 实际分配的页表远小于4MB

计算示例:
程序占用10MB虚拟地址空间
需要的虚拟页数 = 10MB / 4KB = 2560页
需要的二级页表 = 2560 / 1024 ≈ 3个
内存开销 = 3 * 4KB + 4KB(页目录) = 16KB
(相比单级页表的4MB,节省了99.6%!)

2.4 地址转换全流程

2.4.1 MMU的工作流程

MMU(Memory Management Unit,内存管理单元)是硬件电路,负责地址转换:

bash 复制代码
虚拟地址 → MMU → 物理地址

步骤1: CPU发出虚拟地址
   ├─ 从指令中获取,或计算得到
   └─ 例如: 0x00401234

步骤2: MMU从CR3寄存器获取页目录基址
   ├─ CR3存储当前进程的页目录物理地址
   └─ 每个进程有独立的页目录

步骤3: 提取一级页号,查页目录
   ├─ 一级页号 = 虚拟地址[31:22]
   ├─ 页目录项地址 = CR3 + 一级页号 * 4
   └─ 读取得到二级页表的物理地址

步骤4: 提取二级页号,查页表
   ├─ 二级页号 = 虚拟地址[21:12]
   ├─ 页表项地址 = 页表基址 + 二级页号 * 4  
   └─ 读取得到物理页框号

步骤5: 组合物理地址
   ├─ 页内偏移 = 虚拟地址[11:0]
   └─ 物理地址 = (物理页框号 << 12) | 页内偏移

步骤6: 发送到总线,访问物理内存

完整示例:

bash 复制代码
虚拟地址: 0x00401234
二进制:   0000000001 0000000001 001000110100

步骤1: 一级页号 = 0x001 = 1
CR3 = 0x10000(假设)
页目录项地址 = 0x10000 + 1 * 4 = 0x10004
读取 [0x10004] = 0x20000 (二级页表地址)

步骤2: 二级页号 = 0x001 = 1
页表项地址 = 0x20000 + 1 * 4 = 0x20004
读取 [0x20004] = 0x12345 (物理页框号)

步骤3: 页内偏移 = 0x234
物理地址 = 0x12345000 | 0x234 = 0x12345234

步骤4: 访问物理地址0x12345234
2.4.2 TLB快表优化

你可能注意到,每次地址转换需要访问内存2次(页目录+页表),这会严重影响性能!

TLB(Translation Lookaside Buffer)是MMU内置的缓存,用来加速地址转换:

bash 复制代码
TLB的工作原理:
┌─────────────────────────────────┐
│  虚拟页号  │  物理页框号  │ 权限│
├─────────────────────────────────┤
│  0x00401   │  0x12345     │ RW  │
│  0x00402   │  0xABCDE     │ RW  │
│  0x7FFFF   │  0x66666     │ RW  │
│    ...     │    ...       │ ... │
└─────────────────────────────────┘

地址转换流程(带TLB):
1. MMU收到虚拟地址
2. 查TLB(非常快,几个时钟周期)
   ├─ TLB命中 → 直接得到物理地址,完成!
   └─ TLB未命中 → 查页表(慢,几十个时钟周期)
                  └─ 更新TLB,下次就快了

TLB的特点:
✓ 容量小(几十到几百项)
✓ 速度快(硬件实现)
✓ 基于局部性原理(程序倾向于访问相近的地址)
✓ 命中率高(通常>95%)

2.5 缺页异常

2.5.1 什么是缺页异常

当MMU在TLB和页表中都找不到虚拟地址对应的物理页时,就会触发缺页异常(Page Fault):

bash 复制代码
触发条件:
1. 页表项不存在(虚拟页没有映射到物理页)
2. 页表项存在但权限不足(如写只读页)
3. 页被换出到磁盘(Swap)
2.5.2 缺页异常的类型

1. 硬缺页(Hard Page Fault / Major Page Fault):

bash 复制代码
物理内存中没有对应的页,需要从磁盘读取

处理流程:
1. 暂停当前进程
2. 分配一个空闲的物理页框
3. 从磁盘读取数据到物理页框
4. 更新页表,建立映射
5. 恢复进程执行

特点: 开销大(涉及磁盘I/O,毫秒级)

2. 软缺页(Soft Page Fault / Minor Page Fault):

bash 复制代码
物理内存中已经有这个页,只是当前进程的页表没有建立映射

典型场景:
- 多个进程共享同一个页(如共享库代码段)
- 进程A已经把页读入内存
- 进程B访问时,只需要建立映射即可

处理流程:
1. 找到已存在的物理页
2. 更新页表,建立映射
3. 恢复进程执行

特点: 开销小(只需更新页表,微秒级)

3. 无效缺页(Invalid Page Fault):

bash 复制代码
访问非法地址,如:
- 空指针解引用
- 数组越界
- 栈溢出

处理:
内核发送SIGSEGV信号给进程 → 段错误,进程终止

三、线程的优缺点

理解了虚拟地址空间后,我们回到线程本身。线程作为进程的轻量级替代方案,有哪些优缺点呢?

3.1 线程的优点

优点1: 创建开销小

bash 复制代码
进程创建:
- fork需要复制父进程的资源(代码段、数据段、堆、栈)
- 复制页表
- 分配新的PID
- 开销: 毫秒级

线程创建:
- 只需要分配线程栈和TCB(线程控制块)
- 共享进程的地址空间,无需复制
- 开销: 微秒级

创建速度: 线程 >> 进程

优点2: 切换开销小

bash 复制代码
进程切换:
1. 保存当前进程的寄存器状态
2. 切换页表(CR3寄存器)
3. 刷新TLB(因为地址空间变了)
4. 刷新CPU缓存(Cache失效)
5. 加载新进程的寄存器状态

线程切换:
1. 保存当前线程的寄存器状态  
2. 加载新线程的寄存器状态
(无需切换页表和TLB,因为共享地址空间!)

切换速度: 线程切换 > 进程切换

优点3: 占用资源少

bash 复制代码
进程:
- 独立的地址空间(页目录+页表)
- 独立的文件描述符表
- 独立的信号处理表
- ...

线程:
- 共享地址空间
- 共享文件描述符表
- 共享信号处理表
- 只需要私有的栈、寄存器、线程ID

资源占用: 线程 << 进程

优点4: 通信简单

bash 复制代码
进程间通信:
- 需要使用管道、共享内存、消息队列等
- 需要内核介入
- 效率低

线程间通信:
- 直接访问共享的全局变量
- 无需内核介入
- 效率高

通信效率: 线程 >> 进程

优点5: 充分利用多核CPU

bash 复制代码
单进程:
- 只能跑在一个CPU核心上
- 其他核心闲置,浪费

多线程:
- 不同线程可以并行在不同核心上
- 充分利用多核CPU的计算能力

并行能力: 多线程 > 单进程

优点6: 提升程序响应性

bash 复制代码
单线程程序:
- 执行耗时操作(如网络I/O)时,整个程序阻塞
- 用户体验差

多线程程序:
- 一个线程负责I/O,另一个线程处理用户交互
- 即使I/O阻塞,界面仍然响应

响应性: 多线程 > 单线程

3.2 线程的缺点

缺点1: 性能损失

bash 复制代码
场景: 计算密集型任务,线程数 > CPU核心数

问题:
- 多个线程竞争CPU,频繁切换
- 切换带来额外的调度开销
- 可用计算资源不变,开销却增加了

结论: 线程不是越多越好!

缺点2: 健壮性降低

bash 复制代码
进程模型:
- 进程A崩溃,不影响进程B
- 系统整体稳定性高

线程模型:
- 一个线程崩溃(如野指针、除零),整个进程终止
- 所有线程都会受影响
- 需要更谨慎的编程

健壮性: 进程 > 线程

缺点3: 缺乏访问控制

bash 复制代码
进程:
- 每个进程有独立的地址空间
- 一个进程无法直接访问另一个进程的数据
- 安全性高

线程:
- 同一进程内的所有线程共享地址空间
- 任何线程都能访问其他线程的栈(虽然不推荐)
- 线程调用某些系统调用会影响整个进程

访问控制: 进程 > 线程

缺点4: 编程难度高

bash 复制代码
单线程:
- 逻辑简单,顺序执行
- 容易调试

多线程:
- 需要考虑同步问题(竞态条件、死锁)
- 需要考虑数据共享问题
- 调试困难(bug不易重现)
- 时间相关的bug很难定位

开发成本: 多线程 >> 单线程

3.3 线程异常

bash 复制代码
★★★ 重要特性 ★★★

单个线程如果出现异常(除零、野指针等),整个进程都会崩溃!

原因:
1. 线程是进程的执行分支
2. 线程出异常 = 进程出异常
3. 触发信号机制,终止进程
4. 进程终止 → 该进程内的所有线程随即退出

示例:
进程有3个线程:
- 线程A、线程B正常运行
- 线程C访问了空指针 → SIGSEGV信号
- 整个进程收到SIGSEGV → 终止
- 线程A、B、C全部退出

3.4 线程的用途

bash 复制代码
适用场景1: CPU密集型程序
- 科学计算、图像处理、视频编码等
- 利用多核CPU并行计算
- 线程数 ≈ CPU核心数

适用场景2: I/O密集型程序
- Web服务器、数据库、网络爬虫
- 一个线程等待I/O时,其他线程继续执行
- 提升整体吞吐量

适用场景3: 提升用户体验
- GUI程序:界面线程 + 后台任务线程
- 下载工具:下载线程 + 界面线程
- 游戏:渲染线程 + 逻辑线程 + 音频线程

四、进程vs线程深入对比

4.1 资源分配与调度

bash 复制代码
进程:
- 资源分配的基本单位
- 拥有独立的地址空间、文件描述符表等

线程:
- 调度的基本单位
- 共享进程资源,只拥有少量私有资源

形象比喻:

bash 复制代码
把操作系统比作一个公司:

进程 = 部门
- 公司给每个部门分配资源(办公室、设备、预算)
- 部门之间相对独立

线程 = 部门内的员工
- 员工共享部门资源(办公室、设备)
- 员工有自己的工位(线程栈)
- 公司的调度对象是员工(线程),而不是部门(进程)

4.2 线程私有的资源

虽然线程共享进程的大部分资源,但线程也有自己的私有数据:

bash 复制代码
1. 线程ID (tid)
   - 在进程内唯一标识线程
   - pthread_self()获取

2. 寄存器组
   - PC(程序计数器):下一条指令的地址
   - SP(栈指针):线程栈的栈顶
   - 通用寄存器:eax、ebx等

3. 线程栈
   - 每个线程有独立的栈空间
   - 存储局部变量、函数调用信息
   - 主线程栈在进程栈区
   - 其他线程栈在共享区(后续文章详解)

4. errno变量
   - 错误码
   - 每个线程独立的errno

5. 信号屏蔽字
   - 每个线程可以独立屏蔽某些信号

6. 调度优先级
   - 可以为每个线程设置不同的优先级

4.3 线程共享的进程资源

bash 复制代码
1. 地址空间
   - 代码段(Text Segment):程序指令
   - 数据段(Data Segment):全局变量、静态变量
   - 堆(Heap):malloc分配的内存
   - 共享库(Shared Libraries)

2. 文件描述符表
   - 所有线程共享打开的文件
   - 一个线程打开文件,其他线程都能访问

3. 信号处理方式
   - SIG_IGN(忽略)
   - SIG_DFL(默认)
   - 自定义信号处理函数

4. 当前工作目录
   - 所有线程的cwd相同
   - 一个线程chdir,影响所有线程

5. 用户ID和组ID
   - uid、gid、euid、egid

6. 进程ID
   - 所有线程的pid相同

用图示理解:

bash 复制代码
进程地址空间:
┌─────────────────────────────────┐
│       内核空间(1GB)              │
├─────────────────────────────────┤ ← 0xC0000000
│                                 │
│      栈区(主线程栈)              │ ← 向下增长
│         ↓                       │
│                                 │
│ ─────────────────────────────── │
│                                 │
│      共享区(线程2栈)             │ ← 线程栈在这里!
│      共享区(线程3栈)             │
│      共享库、mmap区域            │
│                                 │
│         ↑                       │
│      堆区                        │ ← 向上增长
│                                 │
├─────────────────────────────────┤
│   .bss段(未初始化全局变量)       │
├─────────────────────────────────┤
│   .data段(已初始化全局变量)      │
├─────────────────────────────────┤
│   .rodata段(只读数据)            │
├─────────────────────────────────┤
│   .text段(代码段)                │
├─────────────────────────────────┤ ← 0x08048000
│       保留区                     │
└─────────────────────────────────┘ ← 0x00000000

线程1(主线程):
- 栈在栈区(高地址)
- 共享.text、.data、堆

线程2、3(子线程):  
- 栈在共享区(中间地址)
- 共享.text、.data、堆

4.4 理解"单线程进程"

bash 复制代码
问题: 我们之前学的进程,有线程吗?

答案: 有! 所有进程至少有一个线程

单线程进程:
- 只有main函数这一个执行流
- 这个执行流就是主线程
- 主线程使用进程栈区的栈

多线程进程:
- 有main函数(主线程)
- 还有其他执行流(子线程)
- 子线程使用共享区的栈

五、本篇总结

📌 核心知识回顾

  1. 线程的本质

    • 线程是进程内部的控制序列
    • 线程在进程地址空间内运行
    • Linux中线程实现为轻量级进程(LWP)
  2. 虚拟地址空间

    • 解决物理内存碎片、地址冲突、保护问题
    • 每个进程有独立的虚拟地址空间(32位系统0~4GB)
    • 通过页表建立虚拟地址到物理地址的映射
  3. 分页机制

    • 物理内存按4KB划分为页框(Page Frame)
    • 虚拟地址空间按4KB划分为页(Page)
    • struct page描述每个物理页框
  4. 两级页表

    • 页目录表(1024项) + 二级页表(每个1024项)
    • 按需分配,节省内存
    • CR3寄存器指向页目录基址
  5. 地址转换

    • MMU硬件完成虚拟地址到物理地址的转换
    • TLB缓存加速地址转换
    • 缺页异常:硬缺页(从磁盘加载)、软缺页(建立映射)、无效缺页(段错误)
  6. 线程优缺点

    • 优点:创建快、切换快、通信简单、占用资源少
    • 缺点:健壮性差、编程难度高、一个线程崩溃影响整个进程
  7. 进程vs线程

    • 进程是资源分配的基本单位
    • 线程是调度的基本单位
    • 线程共享进程的地址空间、文件、信号处理等
    • 线程私有栈、寄存器、线程ID、errno

六、承上启下

本篇我们深入理解了线程的本质和虚拟地址空间的分页机制,为后续学习打下了坚实的理论基础。

下一篇预告:

在第二篇中,我们将学习Linux线程控制的核心API:

bash 复制代码
✓ pthread_create - 创建线程
✓ pthread_exit - 线程退出
✓ pthread_join - 等待线程
✓ pthread_cancel - 取消线程  
✓ pthread_detach - 线程分离
✓ pthread_self - 获取线程ID

每个API都会配合完整的代码示例,让你真正掌握多线程编程!


💬 互动环节

学完本篇,你应该能够回答:

  • 为什么需要虚拟内存?
  • 两级页表相比单级页表有什么优势?
  • TLB的作用是什么?
  • 线程相比进程有哪些优点?
  • 一个线程崩溃会影响其他线程吗?

如果这些问题你都能回答,说明你已经掌握了本篇的核心内容!

👍 如果本文对你有帮助,请点赞、收藏、分享,让更多同学受益!

💭 有疑问欢迎评论区讨论,我会及时回复!

相关推荐
你才是臭弟弟2 小时前
Docker 容器使用运行镜像(Windows)
运维·docker·容器
难得的我们2 小时前
C++中的状态模式
开发语言·c++·算法
eWidget2 小时前
Shell循环进阶:break/continue,循环嵌套与优化技巧
运维·开发语言·ffmpeg·运维开发
LetsonH2 小时前
调节 Ubuntu 的 Swap 大小
linux·运维·ubuntu
啊阿狸不会拉杆2 小时前
《计算机操作系统》第十章 - 多处理机操作系统
c++·算法·计算机组成原理·os·计算机操作系统
txinyu的博客2 小时前
用户态与内核态
linux·运维·服务器
半路_出家ren2 小时前
5.RSA和AES加密(python)
服务器·网络·python·https·aes·rsa·加密算法
爱喝水的鱼丶2 小时前
SAP-ABAP:从SAP中暴露REST API:完整实操SICF接口开发指南
运维·开发语言·api·sap·abap·rest·接口开发
鸠摩智首席音效师2 小时前
如何在 Docker 容器下运行 cronjob ?
运维·docker·容器