拓冰建站拓冰建站
首页 / 资讯中心 / 正文

【Linux】线程到底是什么?从轻量级进程、虚拟地址到页表与 MMU,一次理清线程底层模型

个人主页爱和冰阔乐专栏传送门《数据结构与算法》 、C学习方向C方向学习爱好者⭐人生格言得知坦然 失之淡然博主简介文章目录前言一、线程到底是什么1.1 从创建进程说起1.2 Linux 用进程模拟线程五个关键结论1.3 其他平台的实现方案1.4 从 CPU 视角看执行流1.5 用家庭比喻理解二、4KB 页框与物理内存管理2.1 为什么是 4KB2.2 struct page 结构page 数组管理内存申请物理内存三、虚拟地址和页表为什么会出现3.1 没有虚拟内存会怎样四、页表为什么不能设计成一张大表4.1 单张页表的问题4.2 32 位虚拟地址的三段拆分4.3 为什么是低 12 位4.4 页表的空间计算五、两级页表怎样完成地址转换5.1 多级页表的由来5.2 拆分成 1024 个小表5.3 页目录结构5.4 地址转换过程5.5 多级页表的代价六、CR3、MMU 与 TLB把转换链路串起来6.1 CR3 寄存器与 MMU6.2 查页表的两个阶段6.3 TLB 快表6.4 几个关键细节七、回到线程地址空间为什么代表资源总结前言如果只背“线程是进程内部的一个执行分支”后面遇到 LWP、页表、地址空间时很容易断开。这一篇从线程本身开始一直追到 4KB 页框、虚拟地址、两级页表、CR3、MMU 和 TLB。目的不是把内存管理单独讲一遍而是回答一个问题Linux 为什么能让多个执行流共享同一个进程的资源。一、线程到底是什么1.1 从创建进程说起创建进程时OS 会创建 PCB、地址空间、页表加载代码和数据构建映射关系识别信号打开对应文件加载动静态库等。这些都需要占据内存资源和 CPU 资源而内存资源属于硬件资源是有上限的。理解线程先聚焦在 Linux 上再推广到其他平台。进程访问大部分资源都是通过地址空间访问的。地址空间是进程看到资源的窗口。1.2 Linux 用进程模拟线程线程的核心思想创建一个进程让它共享同一个进程的地址空间那么所有进程就看到了同一份资源——即所有的task_struct指向同一个地址空间。将地址空间的资源分配给不同的task_struct就用进程模拟出了线程。初步理解进程看到的资源通过地址空间看到地址空间是资源的窗口。所谓的资源分配就是在划分地址空间本质是在划分虚拟地址的空间范围更本质地说是在划分页表。举个例子假设进程的代码在地址空间上的范围是[0-100]多线程可以将该范围划分为对应的线程个数所对应的范围让每个线程执行进程的一部分代码和数据。那么就可以将整个进程的资源划分成对应的子区域分配给每个线程。五个关键结论结论 1Linux 上线程的概念可以采用进程来模拟。结论 2对资源的划分本质是对地址空间虚拟地址范围的划分。合法的虚拟地址空间可以通过页表的转换找到对应的物理资源1 对 1 的关系。因此虚拟地址就是资源的代表——只要申请了虚拟地址相关的内存资源、代码资源等都属于你。结论 3代码区怎么划分我们写的代码归根结底由一个个函数组成每个函数都有入口地址语言层面才有回调、函数指针的概念。函数不只是有一个地址——函数由 10 行 C 语言代码编译后形成 100 行汇编代码函数本质是代码块代码块里每一行代码都有地址第一行代码地址叫做函数的入口地址。函数编址采用偏移量为 0 的虚拟地址平坦模式进行整体编址。所以函数就是虚拟地址空间的集合让不同线程执行不同代码块本质就是让线程执行 ELF 程序的不同函数。结论 4进程和线程不是一样的吗进程等于内核数据结构 代码和数据其中内核数据结构包括 PCB、虚拟地址空间、页表等结构而不只是task_struct。以前的进程内部只有一个线程的进程。单进程本质是 OS 内多执行流的特殊情况。结论 5Linux 的线程就是轻量级进程或者用轻量级进程模拟实现的。1.3 其他平台的实现方案问题其他平台比如 Windows也是这样吗不同平台对进程的实现大同小异但对线程的实现差别比较大。Windows 中存在进程的 PCB线程在内核中也需要管理先描述再组织线程的控制结构体为TCBThread Control Block。线程是进程内部的执行分支PCB 内部包含链表指针将所有 TCB 链入到 PCB 里。Windows 中 PCB 和 TCB 是同时存在的概念。这会更复杂——线程也需要调度切换时也需要上下文保存执行代码时也是从某个函数入口继续执行和进程的相似性很高这不就冗余了。Linux 程序员认为没有必要为线程单独设计结构只需要复用task_struct用进程来模拟线程。进程的内核代码全部复用代码更加健壮线程的调度结构和调度算法不需要变化。1.4 从 CPU 视角看执行流在 Linux 操作系统软件的视角下进程和线程都是执行流。在硬件 CPU 视角执行流 ≤ 进程。CPU 不区分是进程还是线程它只会主动触发时钟中断要求 OS 调度和被动接受OS 喂给 CPU 的内容。调度就执行调度算法切换就执行切换。CPU 认为执行流是 ≤ 进程的概念。站在硬件 CPU 角度上执行流叫做轻量级进程LWP。操作系统只提供思想不提供方案。Linux 提供实现思想的方案——操作系统定义线程是进程的一个执行流Linux 通过复用 PCB 实现Windows 通过 TCB PCB 实现。1.5 用家庭比喻理解社会分配资源的基本实体以家庭为基本单位。家庭有自己住的房子要有对应的医院、学校等资源。家庭内部从人员的角度看有父母、爷爷奶奶和小明。每个人做着不同的事情——小明在学习爷爷奶奶在养老父母在赚钱。每个人都有共同的目标把自己的事情做好了家庭就过上了好日子。家庭成员 线程调度的基本单位爷爷奶奶是线程在公园里散步公园就是 CPU小明是线程在学校学习学校就是 CPU父母是线程在公司上班公司就是 CPU家庭 进程操作系统 社会爷爷奶奶有自己的病历本小明有自己的作业本父母有自己的银行卡——每个人都有自己独立的资源。但家庭里的冰箱电视都是共享的。概念进程线程资源关系强调独占部分资源共享如通信时强调共享部分资源独占自己的入口函数总结线程是进程内部的一个执行分支线程在进程的地址空间上运行。二、4KB 页框与物理内存管理2.1 为什么是 4KB磁盘以4KB为单位进行数据块划分。可执行程序就是文件文件存储在磁盘上所以可执行程序天然以 4KB 为单位存储无论属性还是内容。物理内存的管理也不是按一个字节一个字节来而是按4KB进行管理。物理内存被 OS 划分为 4KB 的内存块。4KB 大小的数据块不仅是对磁盘的规定也影响物理内存——物理内存和磁盘进行数据交换以 4KB 为单位进行 IO。这个 4KB 数据块叫做页框或页帧。4KB 的划分不管是磁盘文件系统还是内存都是 OS 划分的和硬件厂商无关。2.2 struct page 结构假设一个可用的物理内存有 4GB 空间按一个页框 4KB 划分4GB / 4KB 1048576个页框。操作系统需要将其管理起来——知道哪些页正在使用哪些空闲。内核用struct page结构表示系统中的每个物理页出于节省内存的考虑使用了大量联合体union。/* include/linux/mm_types.h */structpage{unsignedlongflags;/* 原子标志有些情况下会异步更新 */union{struct{structlist_headlru;/* 换出页列表 */structaddress_space*mapping;/* 指向 address_space 或 anon_vma */pgoff_t index;/* 在映射内的偏移量 */unsignedlongprivate;/* 映射私有不透明数据 */};struct{/* slab, slob and slub */union{structlist_headslab_list;/* uses lru */struct{/* Partial pages */structpage*next;intpages;intpobjects;};};structkmem_cache*slab_cache;void*freelist;/* first free object */union{void*s_mem;/* slab: first object */unsignedlongcounters;/* SLUB */struct{/* SLUB */unsignedinuse:16;unsignedobjects:15;unsignedfrozen:1;};};};};union{atomic_t _mapcount;/* 页表项计数 */unsignedintpage_type;unsignedintactive;/* SLAB */intunits;/* SLOB */};#ifdefined(WANT_PAGE_VIRTUAL)void*virtual;/* 内核虚拟地址 */#endif};几个重要参数1.flags用来存放页的状态——页是不是脏的、是不是被锁定在内存中等。flag 的每一位单独表示一种状态至少可以同时表示 32 种不同状态。一些重要的比特位如PG_locked指定页是否锁定、PG_uptodate表示页的数据已从块设备读取且无错误。脏页内存里的数据已经被修改过但还没同步写回磁盘。干净页内存内容 磁盘内容页面回收时可以直接丢弃不需要写磁盘脏页内存改了磁盘还没更新回收前必须先把数据刷回磁盘匿名页堆没有对应磁盘文件脏了会写到 swap 交换分区页被锁定物理页正在被内核操作禁止别的进程 / 内核路径访问、修改。只要PG_locked置 1页面被锁住。缺页中断从磁盘读数据、脏页写回磁盘、写时拷贝 COW 处理时都会加锁。被锁定的页绝对不能被页面回收算法置换出去。2._mapcount表示页表中有多少项指向该页即这一页被引用了多少次。计数值为 -1 时说明内核没有引用这一页新分配中可以使用它。3.virtual页的虚拟地址。有些内存高端内存不永久映射到内核地址空间此时该域为 NULL需要时动态映射。page 数组管理内存早期内核使用struct page mem[1048576]管理内存对内存进行管理本质上就是对 4KB 空间增删改查转化为对数组的操作。只要知道每个 page 的下标就知道每个 page 的起始物理地址具体物理地址 起始物理地址 页内(4KB)偏移。所以不需要在 page 里保存 page 的起始地址。struct page数组在 OS 全局定义也占据空间。算一下struct page占 40 字节物理页为 4KB系统有 4GB 物理内存共有 1048576 个页消耗 40MB 内存——相对 4GB 而言很小。页的大小对内存利用和系统开销很重要页太大页内剩余较大不能利用的空间页内碎片页太小页太多导致页表太长频繁页转化加重系统开销。通常为 512B - 8KBWindows/Linux 系统的页框大小为4KB。申请物理内存在全局描述整个内存使用情况的数组中找到对应 page 是否被占用。没被占用且申请大小符合预期就将标志位设置为已占用然后修改内核数据结构对应关系。OS 申请内存都是申请 4KB 的。三、虚拟地址和页表为什么会出现3.1 没有虚拟内存会怎样思考一下如果没有虚拟内存和分页机制每个用户程序在物理内存上对应的空间必须是连续的因为每个程序的代码、数据长度都不一样按这种映射方式物理内存会被分割成各种离散的、大小不同的块。运行一段时间后有些程序退出它们占据的物理内存空间被回收导致物理内存以很多碎片的形式存在。还有一个问题程序如果直接使用物理地址加载到内存代码段的指针出错了导致野指针就可能从我的程序指向别人的程序把别人的代码修改了——无法实现进程独立性。怎么办我们希望操作系统提供给用户的空间必须是连续的但物理内存最好不要连续。此时虚拟内存和分页便出现了把物理内存按一个固定的长度页框进行分割有时叫做物理页。每个页框包含一个物理页page。一个页的大小等于页框的大小。区分一页和一个页框很重要页框是一个存储区域页是一个数据块可以存放在任何页框或磁盘中有了这种机制CPU 便并非直接访问物理内存地址而是通过虚拟地址空间间接访问物理内存地址。虚拟地址空间是操作系统为每一个正在执行的进程分配的一个逻辑地址在 32 位机上范围从0 ~ 4G-1。操作系统通过将虚拟地址空间和物理内存地址之间建立映射关系也就是页表让 CPU 间接访问物理内存地址。这张表记录的不是虚拟地址到物理地址的映射而是每一对页和页框的映射关系。核心思想将虚拟内存下的逻辑地址空间分为若干页将物理内存空间分为若干页框通过页表便能把连续的虚拟内存映射到若干个不连续的物理内存页。这样就解决了使用连续物理内存造成的碎片问题。四、页表为什么不能设计成一张大表4.1 单张页表的问题页表不能按“每个字节保存一条完整映射”来设计。32 位地址空间共有 2^32 个字节如果每条映射都保存 4 字节虚拟地址和 4 字节物理地址总量就是2^32 × 8 ≈ 32GB显然不可行。真正的页表按页建立映射所以才会引出 4KB 页和多级页表。为什么不能是单张虚拟地址在 32 位平台下是 32 位数字0000 0000 0000 0000 0000 0000 0000 0000。虚拟地址从 CPU 转换成物理地址需要查页表。页表不能是单表存储空间不够所以查页表不可能拿着虚拟地址整体去查。4.2 32 位虚拟地址的三段拆分在 32 位下系统会将虚拟地址逻辑上划分为 3 个区域区域比特位取值范围前 10 位页目录索引bit[31:22][0-1023]中间 10 位页表索引bit[21:12][0-1023]最后 12 位页内偏移bit[11:0][0-4095]虚拟地址 32 位拆分【10位页目录索引 | 10位页表索引 | 12位页内偏移】101012 32 位2^32 4GB这就是 4GB 的来源。虚拟地址以前我们当做线性地址是连续的但在系统层面使用虚拟地址不能整体映射因为页表不能是单张的。4.3 为什么是低 12 位页框大小是 4KB4KB 对应的字节取值范围是[0-4095]而2^12 4KB所以是 12——12 位可以充分覆盖一个页框的全部范围。为什么是低的12 位磁盘上对可执行程序编址是从全 0 到全 F。ELF 中认为每一个区域都是起始地址 偏移量。基于平坦模式所有数据段编址时起始偏移量都为 0。前面若干位前 20 位相同地址一定在一起因此低 12 位连续的地址一定属于同一个 4KB。⚠️重要区分两件事绝大多数同学混淆4GB进程的虚拟地址空间大小逻辑地址4MB 4KB页目录 全部页表这些页表结构本身占用的物理内存大小4.4 页表的空间计算页目录只有一个通过索引页目录结构可以找到对应的页表。页表有 1024 个每个页表保存 1024 个页框的起始地址。一个页表项 4 字节一个页表有 1024 个页表项 4096 4KB。一共有 1024 个页表打满就是 4KB × 1024 4MB再加页目录 4KB。这里算出的 4MB是把 1024 个二级页表全部建满时的上限。实际进程不会一开始就把整个 4GB 地址空间对应的页表全部创建出来下一节继续看多级页表为什么可以按需建立。五、两级页表怎样完成地址转换5.1 多级页表的由来页表中的每一个表项指向一个物理页的开始地址。在 32 位系统中虚拟内存最大空间是 4GB每个用户程序都拥有。要让 4GB 虚拟内存全部可用页表需要 4GB / 4KB 1048576 个表项假设每个表项 4 字节页表总大小1048576 × 4 4MB占用 4MB / 4KB 1024 个物理页。存在什么问题回想一下当初为什么使用页表——将进程划分为一个个页可以不连续存放在物理内存中。但此时页表就需要 1024 个连续的页框似乎和最初的目标背道而驰了。根据局部性原理进程在一段时间内只需要访问某几个页就可以正常运行了。没必要一次让所有物理页都常驻内存。解决大容量页表的最好方法把页表看成普通文件对它离散分配——对页表再分页形成多级页表的思想。5.2 拆分成 1024 个小表把这个单一页表拆分成 1024 个体积更小的映射表。1024每个表中的表项个数× 1024表的个数仍然可以覆盖 4GB 的物理内存空间每一个表就是真正的页表一共 1024 个。一个页表自身占用 4KB1024 个页表共占 4MB——总数和之前没差别关键一个应用程序不可能完全使用全部的 4GB 空间。一个用户程序的代码段、数据段、栈段一共 10MB 空间每个页表覆盖 4MB10MB 向上对齐取整4MB 的倍数 12MB只需3 个页表就够了。5.3 页目录结构每一个页框都被页表中的表项指向了那么这 1024 个页表也需要被管理。管理页表的表称之为页目录表形成二级页表所有页表的物理地址被页目录表项指向页目录的物理地址被CR3 寄存器指向保存当前正在执行任务的页目录地址所以操作系统在加载用户程序时不仅需要为程序内容分配物理内存还需要为保存程序的页目录和页表分配物理内存。5.4 地址转换过程以一个逻辑地址(0000000000, 0000000001, 11111111111)转换为物理地址为例在 32 位处理器中采用 4KB 页大小虚拟地址低 12 位为页偏移剩下高 20 位给页表分成两级每级 10 位1010CR3 寄存器读取页目录起始地址根据一级页号查页目录表找到下一级页表在物理内存中的存放位置根据二级页号查表找到最终想要访问的内存块号结合页内偏移量得到物理地址一个物理页的地址一定是 4KB 对齐的最后 12 位全部为 0所以只需要记录物理页地址的高 20 位即可。以上其实就是MMU内存管理单元的工作流程。MMU 是负责内存管理的重要硬件单元虚拟地址到物理地址的转换就是它承担的核心工作之一。5.5 多级页表的代价MMU 要先进行两次页表查询确定物理地址确认权限后将物理地址发送到总线。当页表变为 N 级时就变成了N 次检索 1 次读写。页表级数越多查询步骤越多CPU 等待时间越长效率越低。单级页表对连续内存要求高引入了多级页表。但多级页表是双刃剑——在减少连续存储要求和存储空间的同时降低了查询效率。虽然虚拟到物理地址转换使用硬件级别的 MMU 实现但页表存放在内存中CPU 需要访存——这是一种软硬件结合的做法。六、CR3、MMU 与 TLB把转换链路串起来6.1 CR3 寄存器与 MMU当前进程的页目录代表页表的起始地址。怎么找到当前 PCB 对应的页表起始地址CPU 里有一个寄存器CR3存放当前进程页目录的物理基地址。CR3 寄存器叫做当前进程的硬件上下文。进程切换了上下文切换了页表就切换了PCB 切换了地址空间也就切换了。在 CPU 内读寄存器拿到的都是虚拟地址。执行查找页框和页内偏移量访问具体字节不是软件完成的——页表结构是软件建立的但 CPU 内部集成了MMU内存管理单元负责将虚拟地址转换成物理地址。CPU 将虚拟地址交给 MMUMMU 拿着页表和虚拟地址进行转换由 MMU 硬件自动完成。从 CPU 进来的地址是虚拟地址从 CPU 出去就是物理地址直接连到地址总线。6.2 查页表的两个阶段第一阶段查找虚拟地址对应的页框CR3 给页目录物理地址虚拟地址拆索引第一次访问内存读页目录项拿到页表物理地址第二次访问内存读页表项得到物理页框拼接偏移第三次访问内存才读到真正程序要的数据第二阶段根据虚拟地址的低 12 位作为页内偏移访问具体字节。页表是软件建立的存放在内存中CPU 需要访存——这是一种软硬件结合的做法。6.3 TLB 快表有没有提升效率的办法计算机科学中所有问题都可以通过添加一个中间层来解决。MMU 引入了TLBTranslation Lookaside Buffer转译后备缓冲器是 CPU 内集成的一段存储空间。当 CPU 给 MMU 传新虚拟地址后MMU 先去问 TLB 有没有。有就直接拿到物理地址发到总线。TLB 容量较小难免发生Cache Miss这时 MMU 还有保底的页表。在页表中找到后MMU 除了把地址发到总线还把映射关系给 TLB 让它刷新缓存。6.4 几个关键细节细节 1页表里没有保存任何具体的虚拟和物理地址。页表用虚拟地址充当页目录与页表的数组下标和页框的页内偏移量。细节 2缺页中断、写时拷贝、内存申请等操作背后都可能要重新建立新的页表和映射关系。细节 3进程由一张页目录 n 张页表构建映射体系。虚拟地址是索引物理地址页框是目标虚拟地址低12位 页框地址 物理地址。细节 4编译的本质是对代码进行编址其实就是给每行代码分配资源。七、回到线程地址空间为什么代表资源进程的虚拟地址既可以给用户也可以给内核使用。mm_struct描述进程完整一整个虚拟地址空间vm_area_structVMA每一个 VMA 描述其中一块连续的虚拟地址区间执行流看到的资源的本质是在合法的情况下你拥有多少虚拟地址。虚拟地址是资源的代表。虚拟地址空间mm_struct vm_area_struct本质是进程资源的统计数据和整体数据。页表是一张从虚拟到物理的地图。概念本质资源划分地址空间的划分资源共享虚拟地址的共享线程资源划分划分地址空间获得合法虚拟地址范围本质是划分页表线程资源共享地址空间的共享本质是页表条目的共享一句话所谓的资源划分本质就是地址空间的划分。线程之间进行资源划分的本质是划分地址空间获得一定范围的合法虚拟地址资源共享的本质是对地址空间的共享本质就是对页表条目的共享。总结把这一篇压成一条线进程负责资源 ↓ 线程负责执行 ↓ Linux 用多个 task_struct 表示多个执行流 ↓ 这些执行流共享同一个地址空间 ↓ 页表把虚拟地址映射到物理页 ↓ MMU / TLB 完成地址转换理解到这里“线程共享进程资源”就不再是一句定义而是能落到地址空间和页表上的具体机制。资源分享【Linux】信号到底什么时候被处理sigaction、中断、用户态内核态与 SIGCHLD【Linux】信号产生后去了哪里从 Pending、Block 到 Core Dump讲清信号的保存【Linux】CtrlC 到底做了什么从键盘、kill 到 alarm一次讲清信号的产生
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门