当前位置: 首页 > news >正文

Linux内核机制——内存管理

        Linux与RTOS的一大区别在于是否具备虚拟内存地址到物理地址映射转换的能力。这个能力依赖于CPU硬件架构设计中的MMU(内存管理单元)。MMU会自动将进程申请访问的虚拟地址转换为实际的物理内存地址,并检查访问权限。下面将宏观介绍一下Linux内存管理大体涉及的机制。

1、虚拟内存申请

        MMU是进行虚拟地址到物理地址的转换,那么在此之前就需要了解Linux的用户态和内核态分别是依靠什么进行虚拟内存管理的分配的。

        Linux的内存管理可以分为用户态和内核态两方面来介绍。 用户态的内存申请接口使用malloc,其底层使用brk或者mmap系统调用(根据申请内存大小的不同,有所 区别。小内存使用brk,大内存使用mmap)。

对比项brkmmap
功能扩展进程的堆(Heap)空间,通过调整 break 指针(堆的结束地址)来分配内存。在进程虚拟地址空间中创建新的内存映射区域,可映射文件、设备或分配匿名内存。
操作对象堆空间(属于进程数据段的一部分)。任意空闲的虚拟地址区域(通常在堆和栈之间,或指定地址范围)。
内存区域类型连续的堆内存,地址空间在逻辑上是堆的延伸。可分配单个或多个不连续的内存区域,每个区域通过独立的映射描述符管理。
分配方式线性扩展堆空间,break 指针向高地址移动,分配的内存属于堆的一部分。在虚拟地址空间中查找空闲区域,通过映射(文件 / 匿名)创建新的内存区域,地址可指定或由系统选择。
释放方式只能通过降低 break 指针释放内存,且释放的内存必须是堆顶的连续区域(无法释放中间块),可能导致内存碎片。使用 munmap 释放任意已映射的区域,可独立释放多个不连续的内存块,无碎片问题。
内存映射类型无文件映射,仅用于匿名内存分配(堆内存)。支持 文件映射(如 open 打开的文件)或 匿名映射MAP_ANONYMOUS 标志)。
物理内存分配与 mmap 相同,均为 延迟分配:虚拟内存分配后,物理内存仅在第一次访问时通过缺页中断分配。同上。
适用场景- 小内存分配(通常小于 1MB)
- 频繁分配 / 释放小块内存(如堆上的动态数据)
- 大内存分配(通常大于 1MB)
- 文件映射(如动态链接库、内存映射文件)
- 共享内存(MAP_SHARED
- 栈扩展、设备映射等
碎片问题容易产生 内部碎片(堆空间释放不连续时,中间空闲块无法利用)。无碎片问题,每个映射区域独立管理,释放后地址空间可被其他映射复用。
分配粒度依赖系统实现,通常以 字节 为单位,但实际受限于页大小(如 4KB),不足一页时按页分配。必须按 页大小PAGE_SIZE)对齐,分配的最小单位为一个页(4KB、8KB 等,取决于架构)。
系统调用参数brk(const void *addr):通过指定 break 指针目标地址来扩展 / 收缩堆。mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
需指定映射大小、权限、标志、文件描述符(可选)、偏移量等。
返回值成功时返回新的 break 地址,失败返回 -1成功时返回映射区域的起始虚拟地址,失败返回 MAP_FAILED(void *)-1)。
典型使用malloc 底层对小内存分配的实现(通过 brk 扩展堆)。malloc 底层对大内存分配的实现(通过 mmap 分配匿名内存)、shmget(共享内存)、动态链接库加载(dlopen)等。
内核实现复杂度简单,仅需维护 break 指针和堆空间状态。复杂,需管理虚拟内存区域(vm_area_struct)、页表映射、文件锁(若映射文件)等。
内存访问局部性堆内存通常集中在低地址,局部性较好(适合小数据频繁访问)。映射区域位置灵活,可能分布在高地址(如匿名映射)或跨地址空间,局部性取决于具体场景。

        而内核态的内存申请使用vmalloc或kmalloc,内核对象(小内存申请)申请使用kmalloc或者专门的对象创建函数,其底层调用的slab分配器。 (slab分配器就是从伙伴系统申请页之后,进一步根据不同的对象进行细粒度的内存分配。)slab会为每个不同大小的内核对象设置不同的页进行分配。 抽象示意图如下:

                          ┌───────────────┐│   内核空间    │├───────────────┤│  伙伴系统     │ (分配整页内存)├───────────────┤│   Slab 分配器  │├────┬───────────┤│    │           │┌───▼───┐   ┌───▼───┐   ┌───▼───┐│ Cache A │   │ Cache B │   │ Cache C │  (每个缓存对应一种对象类型)├────┬────┤   ├────┬────┤   ├────┬────┤│ Slab  │   │ Slab  │   │ Slab  │├────┼────┤   ├────┼────┤   ├────┼────┤│ 对象1 │   │ 对象1 │   │ 对象1 │  (每个 slab 包含多个同类型对象)│ 对象2 │   │ 对象2 │   │ 对象3 ││ ...   │   │ ...   │   │ ...   │└────┴────┘   └────┴────┘   └────┴────┘▲│ (空闲对象通过链表管理,分配/释放直接操作链表)┌───────────┼───────────┐│  分配:从空闲链表取对象  ││  释放:将对象放回链表  │└───────────────────────┘

        而vmalloc适用于大内存分配,其底层依靠伙伴系统进行物理内存页的分配。   kmalloc和vmalloc的区别在于, vmalloc是虚拟内存连续,但是物理内存不连续。 而kmalloc是物理内存和虚拟内存都连续。  因此对于一些硬件的内存申请,比如DMA等,就需要连续的物理内存空间使用kmalloc进行分配。  而对于一些内核模块加载等大内存需要的场景使用vmalloc。

对比项kmallocvmalloc
分配的内存连续性物理地址和虚拟地址都是连续的虚拟地址连续,但物理地址不连续
适用场景适用于分配较小的、需要物理连续内存的场景,例如 DMA 操作、内核数据结构等。适用于分配较大的内存块,对物理内存连续性要求不高的情况,如临时缓冲区。
分配大小限制通常用于分配小于一页(一般为 4KB 或 8KB)到几页大小的内存。分配大内存时效率较低且可能失败。可以分配比kmalloc大得多的内存,理论上可分配接近系统可用内存总量的大小。
性能速度快,因为物理地址连续,内存访问效率高,且分配和释放操作简单。性能相对较低,由于物理地址不连续,需要通过页表映射来访问,会增加额外的开销。
分配和释放函数使用kmalloc分配,kfree释放。使用vmalloc分配,vfree释放。
分配失败处理分配失败时通常返回NULL,调用者需要检查返回值并进行相应处理。同样,分配失败返回NULL,调用者要检查并处理错误情况。
分配上下文可在进程上下文和中断上下文中使用,但在中断上下文中使用时需要使用GFP_ATOMIC标志以避免睡眠。一般在进程上下文中使用,因为分配过程可能会睡眠。
内存管理机制基于 slab 分配器或伙伴系统,slab 分配器负责管理小内存块,伙伴系统管理大内存块。基于页表机制,通过修改页表来建立虚拟地址到物理地址的映射。

2、虚拟地址到物理地址的转换

       以上分配机制(各种malloc)都处理的虚拟内存空间。  在linux中是依靠MMU实现了虚拟地址到物理地址空间的转换,以及访问权限的检查(用户态和内核态实现的基础)。用户态程序如果想要访问内核态的数据则只能通过系统调用,否则权限检查不通过,直接抛出page_fault。   

         在用户空间中(低3GB空间),进程通过malloc申请到虚拟内存后,此时并不会立刻调用伙伴系统分配具体的物理页, 而是在等到进程实际对虚拟内存进行访问之后,  MMU检查到此虚拟地址没有对应的物理地址,此时抛出缺页异常, 由内核处理该异常,使用伙伴系统分配对应大小的物理内存,并更新页表项建立映射。

2.1、页表结构

        在32位的cpu中(32位地址总线),一般使用二级页表,即页目录和页表来存储整个页表项的映射关系。因为一级页表每个进程需要完整开辟4MB的空间,这个空间消耗是不可接收的。 而二级页表只用存储对应页表和页目录,还未分配的页表不用开辟物理空间,从而大大节约了物理内存。   其中如果通过申请的虚拟地址得到对应的页目录索引和页表项? 在32位cpu架构中,前10位用来存储页目录的索引, 中间10位用来存储页表项的索引,最后12位用来存储基于这一页的偏移地址,从而根据对应的物理页帧和其中的偏移地址得到真实数据。

部分位数范围(从高位到低位)位数名称功能描述
页目录索引31 位~22 位(最高 10 位)10 位Page Directory Index用于定位 页目录表(Page Directory Table) 中的条目,每个条目指向一个页表的基址。
页表项索引21 位~12 位(中间 10 位)10 位Page Table Index在页目录项指向的页表中,定位 页表项(Page Table Entry),每个条目指向一个物理页的基址(帧号)。
页内偏移11 位~0 位(最低 12 位)12 位Page Offset在物理页内的字节偏移量(页大小为 \(2^{12} = 4KB\)),与物理页基址组合得到最终物理地址。

2.2、页表交换机制

        在虚拟地址到物理地址转换的过程中,除了未分配物理页的pagefault,还有一种情况是该物理页被移出物理内存了,被移到了磁盘上。 这种情况主要是因为所有进程是共享一个物理内存的,因此会造成物理页帧不够存放的情况。此时就需要通过一些策略(比如LRU,最久未使用)把一些页帧swap(交换)出去,从而给需要使用的物理页帧腾出空间。

        在MMU中,还设计有一个机制加快虚拟页到物理页的转换,就是TLB结构。这个结构缓存了最近使用的虚拟页到物理页的转换条目, 在访问虚拟地址时,虚拟地址进MMU后先和TLB中的转换项进行比较,如果命中那么直接返回物理地址。(缓存就会带来数据一致性的问题,要注意对应的解决方案)

          ┌───────────┐│    CPU    │└─────┬─────┘│  发送虚拟地址▼┌───────────┐│    MMU    │├─────┬─────┤│     │     ││ ┌───▼───┐ ││ │  TLB  │ ││ └───┬───┘ ││     │     ││  ┌──▼──┐  ││  │页表 │  ││  └──┬──┘  ││     │     │└─────┴─────┘│  返回物理地址▼┌───────────┐│  内存    │

3、伙伴系统介绍

        伙伴系统是物理内存的页分配管理算法。其机制是将物理内存管理成2的n次方。并由free_page[x]把空闲页链接起来。  x就标识当前空闲内存块中有2的x次方个页。在得到页申请的时候会选择一个合适大小的内存块分配出去。 如果当前没有对应大小的内存块, 则找更大的内存块,然后不断除2进行内存块拆分,就是伙伴的分离。 直到分离到所需大小的内存块为止。  当合并的时候会根据有无伙伴进行内存块合并。  比如2^x和2^x大小的内存块就会合并成2^(2x)大小的内存块。    

项目详情
基本原理将物理内存按 2 的 n 次方大小的页块进行管理,通过free_page[x]链表管理空闲页块,其中 x 表示空闲内存块包含 \(2^x\) 个页。
分配流程1. 收到页申请时,查找free_page[x]链表(x 满足 \(2^x\) 为所需内存块大小),若有合适大小的空闲内存块,直接分配。 2. 若当前没有对应大小的内存块,则查找更大的内存块,对其进行拆分(伙伴分离)。不断将大内存块拆分为两个 \(2^{x - 1}\) 大小的内存块,直到得到所需大小的内存块。
释放流程1. 释放内存块时,检查其伙伴块(具有相同大小且地址相邻的内存块)是否空闲。 2. 若伙伴块空闲,则将两者合并成一个 \(2^{x+1}\) 大小的内存块,继续检查新合并内存块的伙伴块是否空闲,重复合并过程,直到无法合并为止。
数据结构维护多个空闲链表free_page[x],每个链表存储相同大小( \(2^x\) 个页)的空闲内存块。
优点1. 有效减少外部碎片,因为合并机制可将相邻空闲块合并成大的连续块。 2. 分配和释放操作速度较快,基于链表和简单的拆分、合并逻辑。
缺点1. 存在内部碎片,分配的内存块大小只能是 2 的幂次方,可能导致分配的内存大于实际需求。 2. 对于非 2 的幂次方大小的内存请求,可能会造成一定的浪费。

4、多核/多CPU注意事项

        在多核 / 多 CPU(SMP 架构)环境下,内存管理会因多个核心同时访问共享数据结构(如伙伴系统的空闲页链表、slab 分配器的缓存元数据)而面临临界区访问冲突,可能导致数据不一致、双重分配等问题,同时 NUMA 架构下跨节点内存访问延迟和缓存一致性也需处理。为此需引入自旋锁、互斥量、原子操作、禁止内核抢占等同步机制,以确保共享资源的互斥访问和操作原子性,平衡正确性与并发性能。

相关文章:

  • Git LFS 学习笔记:原理、配置、实践与心路历程
  • 【ROS】TEB 规划器
  • 概率多假设跟踪(PMHT):多目标跟踪中的概率软关联与高效跟踪算法解析
  • 继承的了解与学习
  • 使用 vcpkg 构建支持 HTTPS 的 libcurl 并解决常见链接错误
  • 【时时三省】(C语言基础)用do...while语句实现循环
  • Wireshark 搜索组合速查表
  • linux服务器命令行获取nvidia显卡SN的方法
  • 通过 winsw 把相关服务配置windows自启动
  • package.json 里面出现 workspace:*,关于工作区的解释
  • 文献总结:NIPS2023——车路协同自动驾驶感知中的时间对齐(FFNet)
  • 时序逻辑电路——序列检测器
  • 如何提高单元测试的覆盖率
  • PC主板及CPU ID 信息、笔记本电脑唯一 MAC地址获取
  • 目标检测综述
  • 深度解析生成对抗网络:原理、应用与未来趋势
  • 三维点拟合平面ransac c++
  • MCP 协议:AI 世界的 “USB-C 接口”,开启智能交互新时代
  • 管家婆财贸ERP BB095.销售单草稿自动填充组合品明细
  • Python 的 pip 命令详解,涵盖常用操作和高级用法
  • 聚焦“共赢蓝色未来”,首届 “海洋命运共同体”上海论坛举行
  • 巴基斯坦航天员选拔工作正在进行,1人将以载荷专家身份参加联合飞行
  • 建行深圳市分行原副行长李华峰一审被判15年
  • 翁东华卸任文和友小龙虾公司董事,此前抢镜“甲亢哥”惹争议
  • 外交部:制裁在涉港问题上表现恶劣的美方人士是对等反制
  • 中国戏剧奖梅花奖终评启动在即,17场演出公益票将发售