Linux内核:内存管理系统
前言
26嵌入式软开选手,秋招被问这块八股被问麻了,啥也回答不出来>…<,不过知道自己这块薄弱那就了解补充一下嘛(以后有时间了持续深入研究一下爱)。这篇博文主要是记录自己在网上看博客和书籍的一个记录。
在这里也推荐一下
书籍: 《Linux内核设计与实现》 《深入理解Linux进程与内存》 《深入理解Linux虚拟内存管理》
博客:一步一图带你深入理解Linux虚拟内存
1. Linux物理内存管理
Linux的物理内存管理算法有memblock / SLAB / Buddy
-
1.1 memblock算法
是内核启动初期的内存管理算法,这个目前也没有人问过,先标记后面再看

-
1.2 物理内存管理数据结构
-
页:Page
Linux是用页来管理所有内存的,一页的大小是4Kb。在start_kernel后会调用paging_init来初始化所有的物理页内存
整个的内存管理器的核心是一个全局二维数组,通过数组来管理内存

这个Page是描述物理页而非虚拟页的,嗯到今天这个struct Page已经很复杂了,我在这里放个之前的Linux内核页的结构,先了解一下思想

-
区:Zone
- 区的出现原因
- 对于有些DMA外设,只能访问特定内存地址
- 一些体系结构的物理内存寻址范围比虚拟内存寻址范围大很多
- 最终划分为:ZONE_DMA / ZONE_DMA32 / ZONE_NORMAL
- struct Zone结构体
核心就是free_area,这个是伙伴系统管理内存的核心,也就是说每个Zone都会有自己的伙伴管理系统
struct zone { unsigned long watermark[NR_WMARK]; unsigned long nr_reserved_highatomic; long lowmem_reserve[MAX_NR_ZONES]; #ifdef CONFIG_NUMA int node; #endif /*...*/ /**伙伴内存管理算法的核心*/ struct free_area free_area[MAX_ORDER]; /* zone flags, see below */ unsigned long flags; /*....*/ } - 区的出现原因
-
-
1.3 伙伴内存管理(内核态)
伙伴内存管理是通过一个多级空闲页面链表区管理空闲内存的

对于free_list,还会细分为UNMOVEABLE / MOVEABLE / RELCLAIMABLE(这些不同暂时先不研究)

-
1.4 SLAB内存管理(内核态)
SLAB是为了进一步更加细分的一页的内存kmem_cache: 内存池(高速缓存) slab: 内存池从系统申请内存的基本单位 object: 内存池提供的内存的单位
-
1.5 物理内存分配使用的API
- alloc_pages
#define alloc_pages(gfp_mask, order) \ alloc_pages_node(numa_node_id(), gfp_mask, order)最终的调用是__alloc_pages_nodemask函数,最终会从Zone里面分配内存
- kmalloc
kmalloc保证分配的页的物理地址是连续的
kmalloc底层会调用SLAB相关API分配内存void *__kmalloc(size_t size, gfp_t flags) { struct kmem_cache *s; void *ret; if (unlikely(size > KMALLOC_MAX_CACHE_SIZE)) /*大内存还是伙伴系统*/ return kmalloc_large(size, flags); s = kmalloc_slab(size, flags); /*小内存用SLAB*/ if (unlikely(ZERO_OR_NULL_PTR(s))) return s; ret = slab_alloc(s, flags, _RET_IP_); trace_kmalloc(_RET_IP_, ret, size, s->size, flags); kasan_kmalloc(s, ret, size, flags); return ret; } - vmalloc
vmalloc则不保证页物理地址是连续的
2. Linux虚拟内存管理
虚拟内存了解先从几个基本的数据结构入手
-
2.1 虚拟内存相关的结构体:struct mm_struct
struct mm_struct { struct { struct vm_area_struct *mmap; /* 内存管理的核心 */ struct rb_root mm_rb; /*红黑树 快速查询与遍历*/ } } /*对于每个区的描述*/ struct vm_area_struct { unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next, *vm_prev; /* Second cache line starts here. */ struct mm_struct *vm_mm; /* The address space we belong to. */ }具体而言可以看这张图片。进程的每个区域(data段/BSS段/代码段)都会有对应的vm_area_struct进行描述

-
2.2 页表与页表项
每个进程都有自己的页表,主要是来管理虚拟地址和物理地址之前的映射关系

页表中的每一项,就成为页表项(PTE)
页表项记录了物理内存页的起始地址和权限

而每一个虚拟地址都有自己的格式,通过[算法] + [页表] 就算出真实物理地址,这里用一个简单的转换例子为例:

- CR3寄存器的值怎么来的
在通过fork调用的时候,对于每个进程都会分配一个PGD,只有有了它才能进行物理地址到虚拟地址的转换pgd_t * pgd_alloc(struct mm_struct *mm) { pgd_t *ret, *init; ret = (pgd_t *)__get_free_page(GFP_KERNEL | __GFP_ZERO); init = pgd_offset(&init_mm, 0UL); if (ret) { #ifdef CONFIG_ALPHA_LARGE_VMALLOC memcpy (ret + USER_PTRS_PER_PGD, init + USER_PTRS_PER_PGD, (PTRS_PER_PGD - USER_PTRS_PER_PGD - 1)*sizeof(pgd_t)); #else pgd_val(ret[PTRS_PER_PGD-2]) = pgd_val(init[PTRS_PER_PGD-2]); #endif /* The last PGD entry is the VPTB self-map. */ pgd_val(ret[PTRS_PER_PGD-1]) = pte_val(mk_pte(virt_to_page(ret), PAGE_KERNEL)); } return ret; } - 多级页表(单级页表占用内存太大了)
原理是一样的,只是无论是虚拟地址本身的格式还是整个MMU的转换都变得更复杂了

- CR3寄存器的值怎么来的
-
2.3 缺页异常(倒在了这里QAQ)
- 2.3.1 触发流程:
- MMU查页表,无有效映射,CPU立即触发Page Fault(同步异常)。
- CPU硬件自动切换到内核态,跳入异常处理入口(如x86的IDT 14号,ARM的Data Abort向量)。
- 汇编入口保存现场,跳转到C实现(如do_page_fault)。
- 内核判断异常原因(按需分配、COW、swap、权限、非法等)。
- 合法则分配/调页/更新页表,非法则发信号终止进程。
- 正常时返回用户态继续执行。
- 2.3.2 触发原因:
主要是对应的页表项不存在,或者权限对不上- 访问非法地址(零地址) --页表项不存在
- malloc之后,具体访问内存时会触发 --页表项不存在
- 可执行文件的按需加载(程序不会一次性都加载到内存,访问到未加载代码触发缺页异常)
- 虚拟页被“换出"磁盘:暂时不用的虚拟页会被置换出磁盘
- COW技术
- 尝试写入不可写的内存区域
- 2.3.3 具体操作: do_page_falut函数(此时进入内核态)
- 判断虚拟地址的合法性
如果虚拟地址合法才能找到对应的vm_area_struct,不合法就要杀死进程了 - 检查权限
看虚拟地址flags和vm_area_struct的权限是否匹配 - 分配物理页
核心是: 根据页表项的具体状态(不存在/被换出/其他)进行操作
最终do_anonymous_page函数来分配物理页
do_anonymous_page底层会调用alloc_pages实际分配物理页
static int __kprobes __do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr, unsigned int flags, struct task_struct *tsk) { struct vm_area_struct *vma; int fault; vma = find_vma(mm, addr); // step1:会检查虚拟地址的合法性 fault = VM_FAULT_BADMAP; if (unlikely(!vma)) goto out; if (unlikely(vma->vm_start > addr)) goto check_stack; /* * Ok, we have a good vm_area for this * memory access, so we can handle it. */ good_area: if (access_error(fsr, vma)) { // step2: 检查权限 fault = VM_FAULT_BADACCESS; goto out; } // step3: 进一步处理 return handle_mm_fault(vma, addr & PAGE_MASK, flags); check_stack: /* Don't allow expansion below FIRST_USER_ADDRESS */ if (vma->vm_flags & VM_GROWSDOWN && addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr)) goto good_area; out: return fault; } ``` - 判断虚拟地址的合法性
- 2.3.1 触发流程:
-
mmap
mmap需要做的就是分配一段新的vm_area_struct / insert_vm_struct
mmap调用结束后,如果真的要用内存,就会触发缺页异常了然后分配物理页(所以调整vm_area_struct就是防止真的用的时候地址不合法啦)- 映射文件
而且如果映射的是文件的话,会调用file_operation的mmap操作vma->vm_file = get_file(file); error = call_mmap(file, vma); static inline int call_mmap(struct file *file, struct vm_area_struct *vma) { return file->f_op->mmap(file, vma); } - 匿名映射
- malloc分配大内存的时候就用匿名映射
- 匿名映射 + MAP_SHARED标志 可以实现父子进程共享数据
只是分配,真正用的时候会触发缺页异常
- 映射文件
-
brk系统调用
brk调用可以用来修改vm_area_struct的起始地址和终止地址
只由当真正使用的时候,才会触发缺页中断分配物理内存
硬件是如何联动
- TLB
TLB本质就是一块缓存(SRAM),缓存当前虚拟地址和页表项的映射关系 - Cache
Cache里面存储的是物理地址的缓存而非虚拟地址,也是一块SRAM - MMU
MMU就是用来处理虚拟地址和物理地址之间的映射的 - 整个的流程
- CPU访问虚拟地址,虚拟地址发送给MMU (用户态)
- MMU先查Cache缓存 (硬件自动完成)
- Cache缓存查不到就查TLB (硬件自动完成)
- TLB里面没有,MMU多级页表查询了 (硬件自动完成)
- MMU检查页表项的有效性
- 没找到对应页表项: 触发缺页异常(触发就进入内核态)
- 发现权限不匹配(写一个只读页): 触发缺页异常
- MMU把物理地址发送给内存控制器,内存控制器进行读写 (硬件完成)


更多推荐



所有评论(0)