121|0

197

帖子

0

TA的资源

一粒金砂(高级)

楼主
 

《Linux内核深度解析》-03-内存管理 [复制链接]

      在LInux系统中,内存管理是一个非常重要的环节,烧友不慎就还导致系统崩溃,所以内存管理是系统稳定运行的基础,也是非常重要的一环。在 Linux 系统的庞大架构里,内存管理无疑是一块关键基石。它肩负着保障系统稳定运行、实现资源高效利用以及提升应用程序性能等多重重任,犹如一位幕后英雄,默默支撑着整个系统的运转。当我们同时开启多个应用程序,比如一边听音乐、一边浏览网页,还后台运行着文件下载任务,此时内存管理就要像一位精明的管家,合理分配内存资源,让每个程序都能顺畅运行,互不干扰。这背后靠的就是内存管理对进程内存空间的精细划分与调度,确保每个进程都有专属的 “内存领地”,避免数据混乱与冲突。再者,对于资源有限的嵌入式设备,如智能手环、智能家居控制器等,高效的内存管理更是决定设备性能优劣的关键。通过优化内存使用,系统能够快速响应操作指令,避免卡顿,为用户带来流畅体验。毫不夸张地说,深入探究 Linux 内核内存管理机制,是解锁系统潜能、优化应用性能的必经之路

 

1 物理地址

        在 Linux 内核的底层世界里,物理内存有着一套严谨且精妙的组织架构。内核通常以页框(Page Frame)作为管理物理内存的基本单位,这就好比将一片广阔的土地划分成规整的小块,每一块都能独立管理与分配。一个页框通常对应着固定大小的内存空间,常见的页框大小为 4KB,当然,在不同架构和配置下,也可能出现 8KB、16KB 甚至更大的页框规格。

为了进一步适配不同硬件特性与系统需求,内核又将物理内存划分成多个区域,也就是常说的区(Zone)。其中,ZONE_DMA 区专门用于满足那些需要直接内存访问(DMA)的设备,这类设备往往对内存访问有特殊要求,只能在特定低地址、物理连续的内存范围内操作,一般涵盖内存起始的 16MB 空间。ZONE_NORMAL 区则是内核与普通进程频繁交互的 “主战场”,涵盖 16MB 到 896MB 的内存段,这片区域的内存能够被内核直接线性映射,访问起来高效便捷。而对于 32 位系统中物理内存超过 896MB 的部分,会被纳入 ZONE_HIGHMEM 区,由于这部分内存无法直接被内核线性映射,需要借助一些特殊的映射机制来访问,像是动态映射技术,不过这也使得访问速度相对较慢。再往宏观层面看,在 NUMA(非一致内存访问)架构系统里,内存还会依据节点(Node)来划分。每个节点关联着一定数量的 CPU 核心以及对应的本地内存,这种架构设计充分考虑到了多处理器系统中,不同 CPU 访问不同内存区域的速度差异,旨在优化整体内存访问性能,让数据获取与处理更加高效。

       当系统需要分配物理内存时,针对不同规模的内存需求,内核有着不同的应对策略。对于大块内存的申请,伙伴系统(Buddy System)就会挺身而出。伙伴系统的核心原理犹如一场精妙的拼图游戏,它按照 2 的幂次大小对内存块进行分块管理,从最小的 1 个页框(4KB)开始,逐步扩展到 2 个、4 个、8 个…… 直至 2 的若干次幂个页框的连续内存块。这些大小各异的内存块如同不同规格的拼图碎片,被有序组织在各个链表之中。当有内存分配请求到来,系统就会在相应链表中寻找合适大小的 “拼图碎片”。要是找不到恰好匹配的,它还会尝试将更大的内存块进行拆分,直到满足需求;而当内存被释放回系统时,伙伴系统又会施展 “合并魔法”,将相邻且大小相同的空闲内存块重新组合,回归到更大规格的内存块链表中,时刻保持内存的规整与高效利用,避免碎片化问题的加剧。

与之相对,对于那些频繁出现的小内存分配需求,slub 分配器则扮演着关键角色。它像是一位精明的 “内存管家”,在系统初始化阶段,就预先从伙伴系统中获取一批内存页框,然后将这些页框精细划分成一个个小的内存对象,再把这些对象串联成缓存链表。当进程或内核模块需要小内存时,无需再大费周章地向伙伴系统申请,直接从 slub 分配器的缓存链表中快速获取即可,用完之后归还到链表,以供后续复用。这一过程极大地减少了小内存分配时的碎片化风险,同时借助缓存机制,显著提升了内存分配与回收的速度,确保系统在面对海量小内存需求时,依然能够保持流畅运行。

 

虚拟地址空间

 

一)用户态虚拟地址空间构成

在 Linux 系统的进程运行天地里,用户态虚拟地址空间犹如一片专属 “自留地”,为进程的数据与代码存储提供了多样的 “分区”。这片空间起始于地址 0,大小依据处理器架构而定,在 32 位系统中,通常是 3GB 的范围。

代码段(Text Segment),作为这片空间的 “智慧中枢”,承载着程序执行的关键 ——CPU 执行的机器指令。它如同一位严谨的指挥官,只读不写,确保指令的稳定性与安全性,防止程序运行时误操作对代码的篡改,让程序沿着既定的逻辑轨道顺畅前行。

紧挨着代码段的是数据段(Data Segment),这里存放着程序中已初始化且初值不为 0 的全局变量和静态变量,犹如一个装满宝藏的仓库,为程序运行提供各类初始数据,随时待命供程序取用。与之相邻的 BSS 段(Block Started by Symbol),则专门收纳那些未初始化以及初始值为 0 的全局变量和静态变量。在程序加载之际,操作系统会贴心地将这片区域初始化为零或空值,既节省了目标文件的存储空间,又确保变量有初始的 “栖息之所”。

再往高地址方向探索,便是充满活力的堆(Heap)区域,它像是一片可拓展的 “建筑工地”,用于存放进程运行时动态分配的内存。当程序调用 malloc ()、new () 等函数时,如同建筑工人在此添砖加瓦,新分配的内存单元不断在堆上 “拔地而起”;而 free ()、delete () 函数的调用,则如同拆除废弃建筑,释放的内存回归堆中,等待下次被重新利用。堆向高地址扩展,不过由于其内存分配与回收的随机性,就像建筑工地的布局常变,容易产生内存碎片问题。

与堆相反,栈(Stack)区域则是从高地址向低地址 “生长”,它如同一个高效的 “数据管家”,负责存储函数内部声明的非静态局部变量、函数参数以及函数返回地址等关键信息。函数调用时,栈帧如同一个个收纳盒,层层堆叠,保存着当前函数的上下文;函数返回时,栈帧又依次弹出,恢复之前的状态。栈内存由编译器自动分配释放,其操作迅速高效,确保函数调用的流畅进行,不过一旦使用不当,比如递归过深或局部变量占用空间过大,就可能引发栈溢出的风险,如同管家的收纳盒堆满溢出现象。

最后,还有内存映射区(mmap),这是一片神奇的 “连接地带”,通过 mmap () 系统调用,它能将磁盘文件的内容直接映射到内存之中,让程序可以像操作内存一样便捷地读写文件内容,极大提升文件操作效率;同时,动态链接库在加载时也会入驻这片区域,为程序运行提供丰富的功能拓展,如同为程序开启一扇通往外部资源的大门。

(二)内核态虚拟地址空间剖析

相较于用户态虚拟地址空间,内核态虚拟地址空间更像是一片 “管控中枢”,掌控着系统的核心资源与关键操作。在 32 位系统中,它占据着虚拟地址空间的 3GB 到 4GB 这 1GB 范围,虽然看似不大,却有着极高的 “权限” 与精妙的布局。

其中,直接映射区是内核态空间的一大 “基石”,占据了前 896MB 的区域。它如同一条坚实的 “纽带”,将内核空间与物理内存中的 ZONE_DMA 和 ZONE_NORMAL 区域紧密相连,建立起一种直接、固定的映射关系。这种映射方式简单而高效,内核只需通过简单的数学运算,将虚拟地址减去特定偏移量,就能精准定位到对应的物理地址,如同在地图上凭借坐标快速找到目的地,使得内核可以迅速访问关键的物理内存资源,满足日常运行需求。

而对于高端内存的管理,内核态引入了动态映射区。由于内核需要掌控所有物理内存,但受限于 1GB 的虚拟地址空间,当物理内存超过 896MB 时,直接映射已无法满足全部需求。此时,动态映射区就像一位 “灵活的调度员” 挺身而出,它位于内核态虚拟地址空间的高端部分,通过一系列复杂而巧妙的页表操作,能够按需将虚拟地址映射到物理内存中的高端内存区域(ZONE_HIGHMEM)。无论是临时的数据缓存,还是内核模块加载到高端内存,动态映射区都能灵活调配,确保内核在面对多样的内存需求时,始终游刃有余,维持系统的稳定高效运行。

内存映射:虚拟与物理的 “桥梁” 

 

(一)分页机制:精准映射的实现

在 Linux 内核的内存管理 “蓝图” 里,分页机制宛如一位神奇的 “空间架构师”,将虚拟内存和物理内存这两大 “空间领域”,精心划分成一个个固定大小的 “单元积木”—— 也就是页。常见的页大小为 4KB,这一规格如同标准的积木尺寸,在不同硬件架构与系统配置下,虽可能有所调整,如变为 8KB、16KB 等,但都遵循着统一、规整的划分原则。

如此一来,虚拟内存中的每一页,都能依据一套精密的 “映射蓝图”—— 页表,精准找到与之对应的物理内存页。页表,就像是一座连接虚拟与物理世界的 “桥梁”,它的每一项记录(页表项)都承载着关键信息,如同桥梁上的 “指示牌”,清晰指引着虚拟页通往物理页的路径。以 32 位系统为例,虚拟地址通常被划分为两部分:高 20 位作为虚拟页面号(VPN),宛如街区编号,用于在页表中精准定位到对应的页表项;低 12 位则是页内偏移量,如同房间号,能在找到的物理页帧内迅速定位到具体的数据存储位置。

这种分页模式带来的优势显著非凡。一方面,它极大地削减了内存管理的复杂度,内核无需再对每一个字节的内存 “斤斤计较”,只需聚焦于这些固定大小的页,如同城市规划师只需着眼于一个个规整的街区,管理负担大幅减轻。另一方面,内存的分配与回收效率得到飞跃提升。当进程申请内存时,内核能迅速从空闲页链表中挑选出若干连续或离散的页进行分配;进程结束后,回收这些页也如同清理闲置街区般轻松,有效规避了内存碎片的滋生,确保内存空间始终井然有序。

更为关键的是,分页机制为内存的保护与共享筑牢了坚实根基。每个进程配备的独立页表,恰似一把把专属 “钥匙”,赋予进程访问自身内存页的权限,严禁越界访问,有力保障了系统的安全性,防止进程间的内存数据 “相互串门”、引发混乱。同时,借助巧妙的页表设置,多个进程能够共享同一段物理内存,就像不同的租客可以共享公寓的公共区域,实现了内存资源的高效复用,极大提升了系统的整体性能,让有限的物理内存发挥出更大的价值。

(二)MMU 与 TLB:加速访问的 “利器”

在内存管理的 “高速通道” 上,内存管理单元(MMU)无疑是一位核心 “调度员”,肩负着将虚拟地址迅速转换为物理地址的重任,保障 CPU 能精准、高效地访问内存数据。当 CPU 发出内存访问指令,携带的虚拟地址信息就如同一份 “快递收件地址”,MMU 会依据进程专属的页表,快速解读这份地址,将其转换为对应的物理地址,如同快递员依据地址簿找到收件人的实际住址,整个过程精准且迅速,确保数据能及时送达。

为了进一步加快地址转换的速度,提升系统运行效率,MMU 内部还配备了一个强大的 “缓存助手”—— 转换后备缓冲器(TLB),也就是常说的快表。TLB 宛如一个 “高速快递分拣站”,专门缓存近期频繁使用的页表项,这些页表项如同提前分拣好、等待派送的快递包裹。当 CPU 发起内存访问时,MMU 首先会在 TLB 中 “查找包裹”,若幸运命中,就能直接获取物理地址,省去了访问内存中庞大页表的时间,如同快递员直接从分拣站取走包裹派送,效率极高;若不幸 TLB 未命中,才会 “前往” 内存中的页表进行完整的地址转换查找,不过此时找到的页表项也会被 “顺手” 存入 TLB,为后续的访问加速。

在实际运行场景中,TLB 的命中率对于系统性能起着关键作用。由于程序运行通常具有良好的局部性原理,即在一段时间内,CPU 往往频繁访问相邻或相关的内存区域,这使得 TLB 有很大几率缓存到这些热点区域的页表项,命中率常常能达到 90% 以上。像日常办公软件,在文档编辑过程中,CPU 频繁读写文档内容、样式数据所在的内存页,这些页表项大概率会驻留在 TLB 中,使得操作响应迅速;又如图形渲染场景,渲染数据、纹理内存页在一段时间内被密集访问,TLB 的高效缓存也能显著减少地址转换延迟,让渲染流程更加流畅,为用户带来顺滑的使用体验。

内存分配与回收

 

(一)伙伴系统算法:大块内存的高效分配

伙伴系统算法作为 Linux 内核管理大块内存的得力 “干将”,其核心原理基于一种巧妙的分组策略。它把物理内存中的空闲页框,按照 2 的幂次大小进行分组,如同将积木按照不同规格整齐归类。从最小的 1 个页框(4KB)开始,逐步形成包含 2 个、4 个、8 个…… 直至 2 的若干次幂个连续页框的组,每组构成一个链表,这些链表就像是不同规格积木的收纳盒,有序存放着相应大小的内存块资源。

当系统收到内存分配请求时,比如某个进程需要 128KB(即 32 个 4KB 页框)的连续内存空间,伙伴系统首先会在对应 128KB 大小的链表中寻找空闲块。若该链表中有可用资源,直接取出交付给进程使用;要是此链表为空,它便会向更大一级的链表(如 256KB 链表)“求助”。一旦找到合适的大内存块,就将其一分为二,一半分配给进程,另一半则插入到 128KB 链表中备用,确保后续同规格内存需求能快速满足。

而在内存回收阶段,伙伴系统的 “合并魔法” 就开始施展。当进程释放一块内存时,系统会检查其 “伙伴” 内存块状态,这里的 “伙伴” 需满足三个严苛条件:一是大小相同;二是物理地址连续;三是起始地址符合特定对齐规则,即起始物理地址是对应内存块大小的整数倍。只有同时满足这些条件,两块内存才能合并成更大的内存块,回归到上一级链表之中,就像两个相同规格的积木重新拼接成更大组件,放回对应的收纳盒,时刻保持内存的规整性,有效避免外部碎片的产生。

举个例子,假设系统初始有一块 1MB(256 个 4KB 页框)的连续空闲内存,以 2 的幂次划分为多个子块并链接管理。当依次有进程 A、B、C 分别申请 64KB、128KB、32KB 内存时,系统会精准分配,并按需拆分更大内存块,剩余部分妥善归位到相应链表。待进程结束释放内存,伙伴系统又会依据 “伙伴” 规则迅速合并可用内存,让内存布局始终处于高效有序状态。不过,这种以 2 的幂次分配内存的方式,偶尔也会产生一些内部碎片。例如进程申请 70KB 内存,系统只能分配 128KB 内存块,就会有 58KB 空间暂时闲置,造成一定的内存浪费,这也是在享受伙伴系统高效管理带来便利时,需要权衡考量的小 “代价”。

(二)Slab 分配器:小对象的贴心管家

在 Linux 内核的日常运作里,频繁会有对小内存块的分配需求,像进程描述符(task_struct)、文件描述符等这些内核常用的数据结构,往往只需占用几十字节到几百字节的空间。此时,slab 分配器就宛如一位贴心管家登场,专为管理此类小对象而生。

slab 分配器的精妙之处在于,它为不同类型的内核对象量身定制了专属的内存缓存。内核初始化阶段,就依据各类对象的常见使用频率与大小,提前从伙伴系统 “批发” 来大块内存,再将这些大块内存精细切分成一个个与对象大小适配的小块,同时配备相应的管理数据结构,共同构成一个个高速缓存组。这些缓存组如同一个个分类明晰的 “小宝箱”,针对不同内核对象,有着不同规格的 “内存格子”,随时准备为内核分配小内存块。

以常见的内核对象为例,对于文件描述符,slab 分配器会创建专门的高速缓存。这个缓存中,每个 slab 通常由一个或多个连续物理页组成,再进一步将其划分成多个文件描述符大小的对象。内核需要新的文件描述符时,无需大费周章向伙伴系统申请大块内存,直接从对应的文件描述符高速缓存中快速取出一个空闲对象即可,整个过程如同从装满文件描述符 “模板” 的小宝箱里顺手拿出一个,迅速且便捷;用完之后,归还到缓存,等待下次复用,避免频繁的内存申请与释放操作带来的性能开销。

从操作细节看,创建 slab 缓存时,内核通过 kmem_cache_create () 函数,依据对象大小、对齐要求等参数,构建起 kmem_cache 结构体,它如同宝箱的 “总管家”,掌控着缓存内对象的分配、回收等诸多事宜。分配对象时,首选 kmem_cache_cpu 结构体管理的快速通道,这里缓存着当前 CPU 本地的空闲对象,通过 freelist 指针迅速定位获取;若本地无空闲,则向 kmem_cache_node 结构体管理的共享缓存 “求助”;实在不够,才向伙伴系统申请新内存扩充缓存。释放对象时,对象会依据其状态,在 slabs_full、slabs_partial、slabs_empty 这三个链表间灵活转移,确保内存高效利用。

   对比 kmalloc () 和 vmalloc () 等内存分配函数,slab 分配器的优势尽显。kmalloc () 虽能分配连续物理内存,但对于频繁小对象分配,易产生内存碎片;vmalloc () 虽可分配大块虚拟内存,但物理内存可能不连续,访问效率相对较低。而 slab 分配器基于对象类型精准管理内存,不仅减少碎片,还利用缓存机制加速分配,成为内核小对象内存管理的不二之选,保障内核在复杂任务处理中始终高效运行。

 

      至此,我们一同深入探究了 Linux 内核内存管理的诸多关键层面,从物理内存的底层架构搭建,到虚拟地址空间的精巧划分;从内存映射的精准桥梁构建,再到内存分配与回收的动态智慧施展,乃至嵌入式驱动开发场景中的实战应用,每一处细节都彰显着 Linux 内核内存管理的精妙与强大。

它不仅是系统稳定运行的根基,更是开发者挖掘系统潜能、优化应用性能的核心工具。深入理解这些机制,无论是对于系统编程高手雕琢大型软件架构,还是嵌入式开发者优化硬件资源利用,都有着不可估量的指引作用,能助力大家在面对复杂系统挑战时,游刃有余地设计、调试与优化。

       Linux 内核内存管理领域仍在持续演进,新技术、新优化不断涌现。希望各位读者以此为起点,保持探索热情,深入研读内核源码,紧跟内核开发社区动态,不断实践积累,去解锁更多前沿技术奥秘,为 Linux 系统性能提升、创新应用落地添砖加瓦,在开源世界里留下属于自己的智慧印记。

 

点赞 关注

回复
举报
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/8 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表