适合零基础入门,也可用于期末、考研复盘,全文整合实操代码、命令实验与底层原理,知识点完整无遗漏。
前言
当初学操作系统线程和虚拟内存分页时,很容易混淆进程与线程、搞不懂 32 位地址分段、CR3 寄存器作用这些高频考点。本文结合代码实操、服务器命令验证,把两大块内容完整梳理,逻辑循序渐进,没有晦涩的跳跃知识点,同时预留截图插入位置,方便自己复习查阅。
第一部分 Linux 线程:轻量级进程 LWP
1 进程、线程基础概念
1.1 什么是进程
我们把正在运行的程序称作进程,它由内核维护的管理结构、程序代码、全局数据共同构成,操作系统是以进程为单位分配硬件资源。 当系统创建新进程时,会一次性分配全套独立资源:专属虚拟内存空间、独立页目录、私有的文件描述符表、单独的信号处理规则,还有专属工作目录与权限掩码。 进程切换的成本很高,切换时需要替换页目录,刷新 CPU 内 TLB 缓存,消耗不少算力。
进程是运行起来的程序,内核数据结构+代码和数据=进程--->承担分配相同资源的的基本实体
1.2 什么是线程
线程是进程内部独立的执行分支,一个进程最少有一条主线程,也能创建多条子线程。平时直接运行的程序,本质就是单线程进程。 这里是 Linux 和其他系统最大的区别:内核没有专门存放线程信息的 TCB 结构体,Linux 里线程就是轻量级进程 LWP。不管是进程还是线程,内核都用task_struct统一管理,CPU 调度程序不会区分二者,只依靠 LWP 识别不同执行流。
线程是进程内部的执行分支
必须学习具体的操作通信的线程实现
线程资源分为共享、私有两类
- 同进程下所有线程共享资源 整个进程的虚拟地址空间(代码段、全局数据、堆、动态链接库)、页目录和页表、打开的文件描述符、信号处理函数、文件系统配置(工作目录、umask)全部共享。
- 每条线程独有的资源 独立栈空间、运行时寄存器上下文、错误标记 errno、调度相关数据,以及独属于自己的内核标识 LWP。
1.3 进程与线程简单对比
| 对比维度 | 普通进程 | Linux 线程(LWP) |
|---|---|---|
| 资源隔离 | 全部资源独立 | 绝大多数资源共享 |
| 切换时修改 CR3 | 需要修改 | 不用修改 |
| 唯一标识 | PID | LWP,同进程 PID 完全相同 |
原理
Linux的线程原理
2 创建线程的 pthread_create 函数详解
2.1 头文件与完整函数原型
pthread_create
参数
pthread_t *restrict thread输出型参数,传入pthread_t变量地址,函数执行成功后,会把新建线程的 TID(线程 ID,TCB 标识)写入该变量。restrict是 C 关键字,标记该指针是访问对应内存的唯一途径,编译器优化用,写代码时直接传普通指针即可。const pthread_attr_t *restrict attr线程属性结构体,用来设置栈大小、分离属性、调度优先级等; 传nullptr/NULL代表使用系统默认线程属性。void *(*start_routine)(void *)— 线程入口函数(重点)函数指针,规定线程主函数固定格式:- 参数:
void*通用指针 - 返回值:
void*你之前代码报错根源就是线程函数返回值写成void,不匹配该签名。
- 参数:
void *restrict arg传给线程入口函数的唯一参数,无参数时填nullptr; 可传递数字、结构体指针等任意数据,在线程函数内强转类型使用。
返回值
- 返回
0:线程创建成功 - 返回非 0 数字:创建失败,对应错误码(如内存不足、属性非法)
我们发现这两个死循环居然同时在运行,也就是说明这是两个执行流,我们不妨可以进一步验证
我们让这两个循环都打印自己的pid,发现pid都是一致的,说明这两个执行流属于同一个进程
查看线程(LWP 轻量级进程)
ps -aLPID LWP TTY TIME CMD 2664911 2664911 pts/0 00:00:00 testThread 2664911 2664912 pts/0 00:00:00 testThread字段解释:
- PID = 进程 ID:两行 PID 完全一致
2664911,代表两个线程隶属于同一个进程; - LWP = 轻量级进程 ID(内核线程 ID)
- LWP=2664911:主线程 main(主线程 LWP 号和进程 PID 相等)
- LWP=2664912:
pthread_create创建的子线程 Linux 内核不区分进程与用户态线程,全部统一用 task_struct 管理,用户线程在内核层面就是轻量级进程 LWP。
区分执行流严格意义上来说,并不是依靠pid而是lwp
那么如何杀掉这个呢---->直接选择杀掉进程kill -9 pid
虚拟地址空间第四讲--页表,虚拟地址 物理地址
1 物理内存基础管理规则
1.1 物理内存最小单元:4KB 页框
内核会把整块物理内存切割成大小统一的页框,单块页框固定 4KB,换算公式4KB=2^12。内存和磁盘交换数据时,读写的最小单位同样是 4KB。
1.2 页框管理结构体 struct page
机器上每一块物理页框,都会对应一个 struct page 结构体。结构体内部 flags 采用位图存储页面状态,一位标记一种状态,包含是否空闲、是否脏页、是否锁定、文件缓存、交换分区标记等。使用位图能极大节省内核内存开销。
1.3 全局数组 mem [] 统一管理所有页框
系统全部物理页框由全局数组 mem [] 统一索引。32 位 4GB 内存设备,总页框数量:4GB / 4KB = 1048576。
物理地址与数组下标换算公式
- 物理地址转数组下标:
index = 物理地址 / 4096- 数组下标转回页框起始地址:
页基址 = index * 4096同一页框里任意物理地址,最终都会映射到同一个 struct page,页面状态统一。
整体逻辑流程
- 物理内存被切分为固定大小页框(4KB 标准物理块),每个页框状态、用途不同,操作系统必须为每一块单独描述、管理。
- 使用
struct page结构体作为页框描述符,记录单个物理页框所有信息。 - 所有物理页框的
struct page统一存放在全局数组mem[]中集中管理。 - 数组下标和物理页框物理地址可以快速互相换算,实现地址与描述符一一映射。
2 32 位程序虚拟地址空间布局
32 位系统完整虚拟地址空间共 4GB,分为用户空间、内核空间两大区域:
- 用户空间(0 ~ 0xC0000000,合计 3GB) 地址由低到高依次存放:程序代码段、初始化全局数据、未初始化数据、堆(地址向上增长)、共享库映射区域、栈(地址向下增长)。
- 内核空间(0xC0000000 ~ 4GB,合计 1GB) 所有进程共用同一份内核空间,存放内核代码、硬件驱动、全局页表、内核栈等核心资源。
3 32 位二级分页:虚拟地址 10+10+12 分段规则
32 位虚拟地址一共 32 个 bit,硬件 MMU 会自动拆分三段:
1)蓝色段:高 10 位,页目录索引
- 长度:10bit,取值范围 \(0 \sim 2^{10}-1=1023\),共 1024 个下标;
- 对应结构:页目录(每个进程独立一张,固定 4KB 物理内存);
- 作用:以 CR3 存放的页目录物理地址为基址,用这 10 位找到对应页目录项;
- 页目录项内容:保存一张页表的物理起始地址。
2)绿色段:中间 10 位,页表索引
- 长度:10bit,同样 0~1023,单张页表 1024 个页表项;
- 对应结构:页表(按需分配,进程没用到的虚拟区间不会创建页表);
- 作用:拿到页表物理地址后,用这 10 位找到页表项 PTE;
- 页表项核心:存放物理页框的起始地址,同时附带权限位(读 / 写 / 执行)、存在位、脏位。
3)红色段:低 12 位,页内偏移
- 长度:12bit,\(2^{12}=4096\),正好对应 4KB 一页;
- 关键特性:不参与页表查找;
- 作用:找到物理页框首地址后,直接加上这 12 位偏移,定位页内具体的字节;
- 举例:物理页框起始地址 0x1000,偏移 0x0FF → 最终物理地址 0x10FF。
每张页表同样包含 1024 个页表项 (PTE),单张页表 4KB; 一个页目录项对应一张独立页表,图里画出 3 张页表代表不同虚拟区间。
一条虚拟地址完整翻译流程
- CPU 执行代码,输出 32 位虚拟地址,送入 MMU;
- MMU 拆分虚拟地址:高 10 位、中间 10 位、低 12 位;
- 从 CR3 寄存器读取当前进程页目录物理首地址;
- 高 10 位索引页目录,取出对应页表的物理地址;
- 中间 10 位索引页表项,拿到物理页框首地址;
- 物理页框地址 + 低 12 位页内偏移 = 最终物理内存地址;
- 硬件访问物理内存;
- 若页表项 “存在位 = 0”:代表该虚拟地址对应的页面在磁盘交换区,触发缺页异常,内核加载页面到物理内存后重试。
页表大小:4MB+4kb
单个进程不可能拥有全部内存,当前进程页表总数远远小于4MB
懒加载,写实拷贝,缺页中断--->页表少-->页表会更少
写时拷贝会讲整个4kb进行拷贝----->局部性原理(磁盘上的数据可以提前加载到内存)
任何一个页框的地址,用多少位就够了
页框的起始地址低12位都是0---->用20比特位表示页框的起始地址,对地址进行相应的右移12位,左移12位来完成--->20位表示页表的地址剩下来12位--->标志位,
以上工作由硬件完成mmu,因为效率高
4 CR3 寄存器核心知识点
4.1 CR3 寄存器存储内容
CR3 是 CPU 专属硬件寄存器,里面存放的是当前进程页目录的物理起始地址,绝对不能存放虚拟地址。 原因很容易理解:翻译虚拟地址需要依赖页目录,如果 CR3 存虚拟地址,想要读取页目录又要翻译地址,会形成无限循环。
4.2 CR3 和进程、线程切换的关联
- 硬件层面:整颗 CPU 只存在一个 CR3 寄存器;
- 内核层面:每个进程的 task_struct 结构体,都会保存自身页目录的物理基地址;
- 进程切换完整流程: ① 保存旧进程:把当前 CR3 的值存入旧进程的 task_struct; ② 加载新进程:读取新进程存储的页目录物理地址,写入 CR3; ③ CR3 更新完成后,MMU 地址翻译规则同步切换,实现进程地址空间隔离。
- 线程切换特殊点:同一个进程所有线程共用一套页目录,切换线程不用修改 CR3,这也是线程切换开销远小于进程的关键原因。
5 MMU 完成虚实地址转换完整流程
- CPU 执行指令,输出 32 位虚拟地址,送入内存管理单元 MMU;
- MMU 读取 CR3 寄存器,拿到当前进程页目录的物理基地址;
- 截取虚拟地址高 10 位作为索引,查询页目录,得到对应页表物理地址;
- 截取虚拟地址中间 10 位作为索引,读取页表内部 PTE 页表项;
- 判断 PTE 有效标记位:
- 标记有效:提取物理页框基地址,拼接低 12 位偏移,访问物理内存;
- 标记无效:触发缺页异常,内核分配物理页、从磁盘加载对应数据、更新页表项,之后重新执行出错指令。
拥有更多的虚拟地址--->拥有更多的内存,只要我们将虚拟地址进行划分,本质就是划分物理内存
页表的本质:是进程看到内存资源的窗口,拥有更多的虚拟地址,拥有更多的内存,划分区域,划分虚拟地址,在编译阶段进行划分.
linux在物理上,存在真正意义上的线程吗??--->不存在,没有TCB,只存在轻量级进程
进程拥有独立性大部分资源独立
线程拥有共享性,大部分资源共享
总结
- CR3 寄存器只存放页目录物理地址;切换进程需要修改 CR3,同进程切换线程无需修改;
- PID 区分不同进程,LWP 区分同进程内多条线程;多线程程序所有线程 PID 完全相同;
- clone 依靠 flags 标识控制资源共享,CLONE_VM 是进程和线程的分界线,fork 是 clone 的特殊实现;
- 32 位二级分页虚拟地址分段固定 10+10+12,低 12 位对应 4KB 页框偏移;
- struct page 管理单块物理页框,全局 mem [] 数组索引整机所有物理内存;
- Linux 没有独立线程内核结构,线程就是轻量级进程 LWP,进程、线程统一由 task_struct 调度;
- pthread 库只是用户层封装,底层创建线程依赖 clone 系统调用。