【Linux】浅析Linux内存管理机制

本文主要讲解Linux内存管理机制中的一个核心子模块:内核内存分配与回收机制。通过了解这个机制,对内核内存分配失败时,应该怎么看日志、分析原因有一定帮助。

目录

1、基础知识

[1.1 虚拟内存](#1.1 虚拟内存)

[1.2 页与页表](#1.2 页与页表)

[1.3 伙伴系统](#1.3 伙伴系统)

[1.4 slab 分配器](#1.4 slab 分配器)

2、常见内存问题

[2.1 Out Of Memory](#2.1 Out Of Memory)

[2.2 Segment Fault](#2.2 Segment Fault)

[2.2.1 段错误成因](#2.2.1 段错误成因)

[2.2.2 段错误定位](#2.2.2 段错误定位)

1、基础知识

该部分仅作简单介绍,详细的内存管理机制可结合内核源码mm目录下的代码分析。

5.10版本的瑞芯微 RK3588/Rock5b BSP内核下载链接https://github.com/redroid-rockchip/kernel-rock5b/tree/linux-5.10-gen-rkr3.4

1.1 虚拟内存

虚拟内存是现代通用操作系统(如Linux、Windows)采用的一种内存管理技术。它为每个进程提供了一个巨大、连续且私有的地址空间,即"虚拟地址空间"。实际上,程序的代码和数据存储在物理内存(RAM)和磁盘上。当CPU执行指令需要访问内存时,会通过内存管理单元(MMU)来将虚拟地址"翻译"成真实的物理地址。好处:

  • 内存隔离与保护:每个进程都有自己独立的虚拟地址空间,一个进程无法直接访问另一个进程的内存,从而保证了进程和操作系统自身的安全性和稳定性。(核心作用)
  • 扩展地址空间:虚拟内存允许程序使用的内存量超过实际可用的物理内存。操作系统可以将暂时不用的内存数据交换到硬盘上(这个区域通常称为交换空间(Swap Space)或页面文件),在需要时再加载回物理内存。
  • 简化内存管理:为每个进程提供了一个从0开始的、连续的地址空间,这极大地简化了程序的链接、加载和内存分配过程。

RAM(Random Access Memory,随机存取存储器)是一类易失性存储器:

  • 作用:高速读写(GB/s 级),作为"临时运算工作台",保障系统实时性和高性能
  • 分类:stm32f407 的 SRAM 为 192KB,RK3588 的板载 RAM(4/8/16GB),手机的运行内存(8/12/16GB),电脑内存(8/16/24/32GB)。嵌入式平台多为板载 LPDDR 颗粒,PC 为内存条。

磁盘:非易失性存储,断电后数据长期保留:

  • 作用:长期数据仓库,实现数据持久化留存。
  • 分类:分为机械硬盘(HDD)、固态硬盘(SSD),嵌入式多为 eMMC(Embedded MultiMediaCard 嵌入式多媒体卡)、SD卡、NVMe SSD(基于PCIe 接口的高速固态存储)

1.2 页与页表

为了便于管理,虚拟和物理内存都被划分为固定大小的块,通常是4KB,这个块被称为页(笔者在"Socket编程"博客中也有提到,CPU 读取内存时,并不是一个字节一个字节地读,而是以 "块" 为单位进行读取,这样可以提高内存读取效率)。

操作系统为每个进程维护一个页表(Page Table)。页表就像一个翻译词典,记录了哪个虚拟页对应哪个物理页(在RAM中),或者记录了该页被临时存放在硬盘的哪个位置。当进程访问一个虚拟地址时,MMU(CPU中的内存管理单元,是个硬件)会查询页表,找到对应的物理地址,然后访问数据。

1.3 伙伴系统

伙伴系统是Linux内核中管理物理页框的基础机制,核心目标是解决物理内存分配中的外部碎片问题,并高效提供连续的页框块(物理内存最小分配单位是页框,通常 4KB)。它是内核内存分配的底层支撑(slab 分配器、vmalloc 等均依赖它)

设计思想 :物理内存被划分为固定大小的页框,所有空闲页框按块大小 分组管理,块大小必须是 2 的幂次(如 1 个页框、2 个页框、4 个页框、8 个页框)。两个伙伴块需满足:大小相同、物理地址连续、来自同一父块(即两个块是同一更大块拆分的结果),释放时可合并为一个更大的块

内部碎片:分配给进程 / 内核的内存块 大于实际需求,导致块内未被使用的空间被 "浪费",这部分空闲空间仅属于该块,无法被其他请求复用。

外部碎片:内存中存在多个 分散的空闲块,总空闲内存大小足够满足某个连续内存请求,但由于这些空闲块不连续,无法合并成一个足够大的连续块,导致请求失败。

1.4 slab 分配器

打破伙伴系统 "2^k 页框块" 的固定粒度限制,通过 "按小对象尺寸定制缓存池 + 大块页框拆分 + 对象复用",让分配粒度精准匹配小内存需求,从根源消除 "分配块远大于实际需求" 导致的内部碎片。

Slab 分配器的小内存申请在单个缓存池内,优先复用 "部分满的 slab 块",再用 "空闲 slab 块",最后才向伙伴系统申请新页框创建新 slab 块------ 不同大小的小内存对应不同的 "专属缓存池",缓存池之间互不干扰。

  • 每个 Slab 缓存池 = 一个专属货架
  • 每个 slab 块 = 货架上的一个盒子(从伙伴系统申请的 4KB 页框,拆成 16 个 256B 格子)
  • 小对象 = 要存放的 "商品"(如256B 传感器配置数据)

2、常见内存问题

2.1 Out Of Memory

OOM 错误是 Linux 系统(尤其是嵌入式平台)因物理内存 + 交换空间耗尽,内核无法满足新的内存分配请求时触发的致命错误,最终会通过 OOM Killer 杀死 "得分最高" 的进程释放内存(极端情况可能导致系统崩溃)。

OOM 发生时,内核会在 dmesg 中输出详细日志,包括被杀死的进程、内存使用状态、OOM 得分等,这是最关键的定位依据:

bash 复制代码
# 查看 OOM 相关日志(嵌入式终端执行)
dmesg | grep -i oom
# 或查看完整内核日志,按时间排序
dmesg | tail -200

|--------|----------------------------------------------------------------------------------------------|
| dmesg | 输出Linux内核环形缓冲区中的所有日志内容 |
| 管道符 | | 将前一个命令的标准输出作为后一个命令的标准输入,实现 命令串联处理 |
| grep | 文本过滤工具,逐行扫描输入内容,匹配指定字符串并输出匹配行 -i 忽略大小写,匹配 OOM/oom/Oom 等所有大小写组合 oom 匹配关键词,内核触发 OOM 时日志中必含该关键词 |
| tail | 读取文件 / 输入的末尾行,默认显示最后 10 行,-200 显示最后200行 |

2.2 Segment Fault

Segment Fault(段错误)是嵌入式 Linux 中用户态程序最常见的崩溃类型,本质是程序访问了内核不允许的内存地址(如空指针、内存越界、野指针等)

2.2.1 段错误成因

段错误由内核触发(SIGSEGV 信号,信号编号 11),本质是内存访问违规

类别 具体场景
非法指针操作 空指针解引用(char *p = NULL; *p = 1;)、野指针
内存越界 数组越界(char buf[10]; buf[10] = 'a';)、字符串拷贝越界(strcpy无长度限制)
栈溢出 ISR 中定义大局部变量、递归调用函数
内存释放异常 重复 free、free 非堆内存(栈内存 / 常量区)、释放后复用该内存
权限 / 硬件问题 用户态访问内核态内存、非法外设寄存器地址
多线程 / 进程问题 线程访问已销毁的内存、共享内存未同步导致野指针

NULL 指针:

C/C++ 预定义的宏,表示指针指向内存地址 0(地址 0 是操作系统保留的、不可读写的受保护内存区域,系统不允许用户程序操作这个地址)

野指针:

解引用野指针时,程序大概率崩溃,或篡改随机内存的数据。

  • 1、定义指针变量时未赋值,编译器会给它分配一个随机的内存地址(栈上的垃圾值),此时指针直接变成野指针。
  • 2、用 free()(C)/ delete(C++)释放指针指向的内存后,指针本身的地址值不会变(仍指向已释放的内存),此时指针变为野指针。
  • 3、指针超出数组 / 缓冲区的合法范围后,指向的是数组外的随机内存,变成野指针。

规避野指针的方法

  • 1、指针定义时立即初始化(要么指向有效内存,要么置 NULL)
  • 2、释放内存后,立即将指针置 NULL(切断野指针):free(p); p = NULL;
  • 3、C++ 中优先使用智能指针(自动管理生命周期)

非法外设寄存器地址导致段错误成因:

地址错误:硬编码的外设物理地址与芯片手册不符(如写错寄存器偏移量)

未映射:外设物理地址(如0x10000000)未通过mmap映射到用户态虚拟地址,直接访问物理地址

权限不足:映射时未指定正确权限(如只读地址强行写)

2.2.2 段错误定位

段错误会被内核记录到dmesg,执行命令:

bash 复制代码
dmesg | grep -i segfault  # 过滤段错误日志
# 或查看最新50行上下文
dmesg -T | tail -50
相关推荐
猫豆~2 小时前
Shell脚本部署——8day
linux·云计算
誰能久伴不乏2 小时前
深入理解 `poll` 函数:详细解析与实际应用
linux·服务器·c语言·c++·unix
倔强的石头1062 小时前
Linux 进程深度解析(二):进程状态、fork 创建与特殊进程(僵尸 与 孤儿)
linux·运维·服务器
小李小李无与伦比3 小时前
使用Simiki,部署.md文档
linux·运维·服务器
做人不要太理性3 小时前
【Linux系统】ELF 文件格式的硬核揭秘
java·linux·服务器
怀旧,3 小时前
【Linux系统编程】12. 基础IO(下)
linux·运维·服务器
Winter_Sun灬3 小时前
CentOS 7 编译安卓 arm64-v8a 版 OpenSSL 动态库(.so)
android·linux·centos
ベadvance courageouslyミ3 小时前
系统编程之进程
linux·进程·pcb结构体
松涛和鸣3 小时前
29、Linux进程核心概念与编程实战:fork/getpid全解析
linux·运维·服务器·网络·数据结构·哈希算法