内存管理
- 内存管理包含: 物理内存管理; 虚拟内存管理; 两者的映射
- 除了内存管理模块, 其他都使用虚拟地址(包括内核)
- 虚拟内存空间包含: 内核空间(高地址); 用户空间(低地址)
- 用户空间从低到高布局为: 代码段; DATA 段; BSS 段(未初始化静态变量); 堆段; 内存映射段; 栈地址空间段
- 多个进程看到的用户空间是独立的
- 内核空间: 多个进程看到同一内核空间, 但内核栈每个进程不一样
- 内核代码也仅能访问内核空间
- 内核也有内核代码段, DATA 段, 和 BSS 段; 位于内核空间低地址
- 内核代码也是 ELF 格式
- 虚拟内存地址到物理内存地址的映射
- 分段
- 虚拟地址 = 段选择子(段寄存器) + 段内偏移量
- 段选择子 = 段号(段表索引) + 标识位
- 段表 = 物理基地址 + 段界限(偏移量范围) + 特权等级
- Linux 分段实现
- 段表称为段描述符表, 放在全局标识符表中
- Linux 将段基地址都初始化为 0, 不用于地址映射
- Linux 分段功能主要用于权限检查
- Linux 通过分页实现映射
- 物理内存被换分为大小固定(4KB)的页, 物理页可在内存与硬盘间换出/换入
- 页表 = 虚拟页号 + 物理页号; 用于定位页
- 虚拟地址 = 虚拟页号 + 页内偏移
- 若采用单页表, 32位系统中一个页表将有 1M 页表项, 占用 4MB(每项 4B)
- Linux 32位系统采用两级页表: 页表目录(1K项, 10bit) + 页表(1K项, 10bit)(页大小(4KB, 12bit))
- 映射 4GB 内存理论需要 1K 个页表目录项 + 1K*1K=1M 页表项, 将占用 4KB+4MB 空间
- 因为完整的页表目录可以满足所有地址的查询, 因此页表只需在对应地址有内存分配时才生成;
- 64 为系统采用 4 级页表
内存空间布局和管理
- 内存管理信息在 task_struct 的 mm_struct 中
- task_size 指定用户态虚拟地址大小
- 32 位系统:3G 用户态, 1G 内核态
- 64 位系统(只利用 48 bit 地址): 128T 用户态; 128T 内核态
- 用户态地址空间布局和管理
- mm_struct 中有映射页的统计信息(总页数, 锁定页数, 数据/代码/栈映射页数等)以及各区域地址
- 有 vm_area_struct 描述各个区域(代码/数据/栈等)的属性(包含起始/终止地址, 可做的操作等), 通过链表和红黑树管理
- 在 load_elf_bianry 时做 vm_area_struct 与各区域的映射, 并将 elf 映射到内存, 将依赖 so 添加到内存映射
- 在函数调用时会修改栈顶指针; malloc 分配内存时会修改对应的区域信息(调用 brk 堆; 或调用 mmap 内存映射)
- brk 判断是否需要分配新页, 并做对应操作; 需要分配新页时需要判断能否与其他 vm_area_struct 合并
- 内核地址空间布局和管理
- 所有进程看到的内核虚拟地址空间是同一个
- 32 位系统, 前 896MB 为直接映射区(虚拟地址 - 3G = 物理地址)
- 直接映射区也需要建立页表, 通过虚拟地址访问(除了内存管理模块)
- 直接映射区组成: 1MB 启动时占用; 然后是内核代码/全局变量/BSS等,即 内核 ELF文件内容; 进程 task_struct 即内核栈也在其中
- 896MB 也称为高端内存(指物理内存)
- 剩余虚拟空间组成: 8MB 空余; 内核动态映射空间(动态分配内存, 映射放在内核页表中); 持久内存映射(储存物理页信息); 固定内存映射; 临时内存映射(例如为进程映射文件时使用)
- 64 位系统: 8T 空余; 64T 直接映射区域; 32T(动态映射); 1T(物理页描述结构 struct page); 512MB(内核代码, 也采用直接映射)
物理内存管理
1.物理内存组织方式
1.1 平坦内存模型
- 页和物理地址都是连续的,依次对应,映射算法简单
- 较早的x86都是如此访问内存的,所有cpu统一走总线访问内存,并且距离相同,称为SM对称多处理器;缺点是总线称为了性能瓶颈
1.2 非连续内存模型
- 后期出现NUMA非一致内存访问,就是每个CPU都有自己本地内存,CPU和内存一起称为NUMA节点,访问时不需要经过总线,速度快,但是本地内存不足时需要较长的时间去其他的节点申请内存
- 节点数据结构
- node_id 节点id
- node_mem_map 页表
- node_start_pfn 节点起始页号
- node_spanned_pages 节点中包含不连续的物理内存地址的页面数
- node_present_pages 是真正可用的物理页面的数目
- zone 区域是指内存适用于哪种操作(针对物理内存
- ZONE_DMA 是指可用于作 DMA(Direct Memory Access,直接内存存取)的内存
- ZONE_NORMAL 是直接映射区,从物理内存到虚拟内存的内核区域,通过加上一个常量直接映射。
- ZONE_HIGHMEM 是高端内存区,就是上一节讲的,对于 32 位系统来说超过 896M 的地方,对于 64 位没必要有的一段区域
- ZONE_MOVABLE 是可移动区域,通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片。
- 页(物理内存基本单位,一个物理页面根据使用模式的不同分为不同的页
- 映射虚拟地址空间(匿名页)或者关联一个文件然后与虚拟地址空间建立映射(内存映射文件
- 分配小块内存,需要用slub allocator分配器管理内存块的状态
1.3 稀疏型(适用于内存热插拔
2.页的分配方式
伙伴系统(buddy system)用于分配页级别的内存块。
把所有的空闲页分组为 11 个页块链表,每个块链表分别包含很多个大小的页块,有 1、2、4、8、16、32、64、128、256、512 和 1024 个连续页的页块。最大可以申请 1024 个连续页,对应 4MB 大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。
当向内核请求分配 (2(i-1),2i]数目的页块时,按照 2^i 页块请求处理。如果对应的页块链表中没有空闲页块,那我们就在更大的页块链表中去找。当分配的页块中有多余的页时,伙伴系统会根据多余的页块大小插入到对应的空闲页块链表中。
例如,要请求一个 128 个页的页块时,先检查 128 个页的页块链表是否有空闲块。如果没有,则查 256 个页的页块链表;如果有空闲块的话,则将 256 个页的页块分成两份,一份使用,一份插入 128 个页的页块链表中。
3.小内存分配方式
- 小内存分配, 例如分配 task_struct 对象
- 会调用 kmem_cache_alloc_node 函数, 从 task_struct 缓存区域 task_struct_cachep(在系统初始化时, 由 kmem_cache_create 创建) 分配一块内存
- 使用 task_struct 完毕后, 调用 kmem_cache_free 回收到缓存池中
- struct kmem_cache 用于表示缓存区信息, 缓存区即分配连续几个页的大块内存, 再切成小内存
- 小内存即缓存区的每一项, 都由对象和指向下一项空闲小内存的指针组成(随机插入/删除+快速查找空闲)
- struct kmem_cache 中三个 kmem_cache_order_objects 表示不同的需要分配的内存块大小的阶数和对象数
- 分配缓存的小内存块由两个路径 fast path 和 slow path , 分别对应 struct kmem_cache 中的 kmem_cache_cpu 和 kmem_cache_node
- 分配时先从 kmem_cache_cpu 分配, 若其无空闲, 再从 kmem_cache_node 分配, 还没有就从伙伴系统申请新内存块
- struct kmem_cache_cpu 中
- page 指向大内存块的第一个页
- freelist 指向大内存块中第一个空闲项
- partial 指向另一个大内存块的第一个页, 但该内存块有部分已分配出去, 当 page 满后, 在 partial 中找
- struct kmem_cache_node
- 也有 partial, 是一个链表, 存放部分空闲的多个大内存块, 若 kmem_cacche_cpu 中的 partial 也无空闲, 则在这找
- 分配过程
- kmem_cache_alloc_node->slab_alloc_node
- 快速通道, 取出 kmem_cache_cpu 的 freelist , 若有空闲直接返回
- 普通通道, 若 freelist 无空闲, 调用
__slab_alloc
-
__slab_alloc
会重新查看 freelist, 若还不满足, 查看 kmem_cache_cpu 的 partial - 若 partial 不为空, 用其替换 page, 并重新检查是否有空闲
- 若还是无空闲, 调用 new_slab_objects
- new_slab_objects 根据节点 id 找到对应 kmem_cache_node , 调用 get_partial_node
- 首先从 kmem_cache_node 的 partial 链表拿下一大块内存, 替换 kmem_cache_cpu 的 page, 再取一块替换 kmem_cache_cpu 的 partial
- 若 kmem_cache_node 也没有空闲, 则在 new_slab_objects 中调用 new_slab->allocate_slab->alloc_slab_page 根据某个 kmem_cache_order_objects 设置申请大块内存
4.页面换出
- 触发换出:
- 1) 分配内存时发现没有空闲; 调用 get_page_from_freelist->node_reclaim->__node_reclaim->shrink_node
- 2) 内存管理主动换出, 由内核线程 kswapd 实现
- kswapd 在内存不紧张时休眠, 在内存紧张时检测内存 调用 balance_pgdat->kswapd_shrink_node->shrink_node
- 页面都挂在 lru 链表中, 页面有两种类型: 匿名页; 文件内存映射页
- 每一类有两个列表: active 和 inactive 列表
- 要换出时, 从 inactive 列表中找到最不活跃的页换出
- 更新列表, shrink_list 先缩减 active 列表, 再缩减不活跃列表
- 缩减不活跃列表时对页面进行回收:
- 匿名页回收: 分配 swap, 将内存也写入文件系统
- 文件内存映射页: 将内存中的文件修改写入文件中
用户态内存映射MMAP
- 申请小块内存用 brk; 申请大块内存或文件映射用 mmap
- mmap 映射文件, 由 fd 得到 struct file
- 调用 ...->do_mmap
- 调用 get_unmapped_area 找到一个可以进行映射的 vm_area_struct
- 调用 mmap_region 进行映射
- get_unmapped_area
- 匿名映射: 找到前一个 vm_area_struct
- 文件映射: 调用 file 中 file_operations 文件的相关操作, 最终也会调用到 get_unmapped_area
- mmap_region
- 通过 vm_area_struct 判断, 能否基于现有的块扩展(调用 vma_merge)
- 若不能, 调用 kmem_cache_alloc 在 slub 中得到一个 vm_area_struct 并进行设置
- 若是文件映射: 则调用 file_operations 的 mmap 将 vm_area_struct 的内存操作设置为文件系统对应操作(读写内存就是读写文件系统)
- 通过 vma_link 将 vm_area_struct 插入红黑树
- 若是文件映射, 调用 __vma_link_file 建立文件到内存的反映射
- 调用 ...->do_mmap
- 内存管理不直接分配内存, 在使用时才分配
- 用户态缺页异常, 触发缺页中断, 调用 do_page_default
- __do_page_fault 判断中断是否发生在内核
- 若发生在内核, 调用 vmalloc_fault, 使用内核页表进行映射
- 若不是, 找到对应 vm_area_struct 调用 handle_mm_fault
- 得到多级页表地址 pgd 等
- pgd 存在 task_struct.mm_struct.pgd 中
- 全局页目录项 pgd 在创建进程 task_struct 时创建并初始化, 会调用 pgd_ctor 拷贝内核页表到进程的页表
- 进程被调度运行时, 通过 switch_mm_irqs_off->load_new_mm_cr3 切换内存上下文
- cr3 是 cpu 寄存器, 存储进程 pgd 的物理地址(load_new_mm_cr3 加载时通过直接内存映射进行转换)
- cpu 访问进程虚拟内存时, 从 cr3 得到 pgd 页表, 最后得到进程访问的物理地址
- 进程地址转换发生在用户态, 缺页时才进入内核态(调用__handle_mm_fault)
- __handle_mm_fault 调用 pud_alloc, pmd_alloc, handle_pte_fault 分配页表项
- 若不存在 pte
- 匿名页: 调用 do_anonymous_page 分配物理页 ①
- 文件映射: 调用 do_fault ②
- 若存在 pte, 调用 do_swap_page 换入内存 ③
- ① 为匿名页分配内存
- 调用 pte_alloc 分配 pte 页表项
- 调用 ...->__alloc_pages_nodemask 分配物理页
- mk_pte 页表项指向物理页; set_pte_at 插入页表项
- ② 为文件映射分配内存 __do_fault
- 以 ext4 为例, 调用 ext4_file_fault->filemap_fault
- 文件映射一般有物理页作为缓存 find_get_page 找缓存页
- 若有缓存页, 调用函数预读数据到内存
- 若无缓存页, 调用 page_cache_read 分配一个, 加入 lru 队列, 调用 readpage 读数据: 调用 kmap_atomic 将物理内存映射到内核临时映射空间, 由内核读取文件, 再调用 kunmap_atomic 解映射
- ③ do_swap_page
- 先检查对应 swap 有没有缓存页
- 没有, 读入 swap 文件(也是调用 readpage)
- 调用 mk_pte; set_pet_at; swap_free(清理 swap)
- 若不存在 pte
- 避免每次都需要经过页表(存再内存中)访问内存
- TLB 缓存部分页表项的副本
brk vs mmap
小内存块是分配在堆顶,会将堆顶指针往上推,并且小内存块在高位小内存块释放前,实际不会释放,需要等高位释放后在一起释放,此时系统会将相邻的堆最高位的内存块合并,在合并后的内存块大小超过限定值,则执行内存紧缩,将堆最高位指针回退。
大内存则是分配在空闲空间,而不是紧邻堆顶。
https://www.cnblogs.com/diegodu/p/9230280.html
内核态内存映射
- 涉及三块内容:
- 内存映射函数 vmalloc, kmap_atomic
- 内核态页表存放位置和工作流程
- 内核态缺页异常处理
- 内核态页表, 系统初始化时就创建
- swapper_pg_dir 指向内核顶级页目录 pgd
- xxx_ident/kernel/fixmap_pgt 分别是直接映射/内核代码/固定映射的 xxx 级页表目录
- 创建内核态页表
- swapper_pg_dir 指向 init_top_pgt, 是 ELF 文件的全局变量, 因此再内存管理初始化之间就存在
- init_top_pgt 先初始化了三项
- 第一项指向 level3_ident_pgt (内核代码段的某个虚拟地址) 减去 __START_KERNEL_MAP (内核代码起始虚拟地址) 得到实际物理地址
- 第二项也是指向 level3_ident_pgt
- 第三项指向 level3_kernel_pgt 内核代码区
- 初始化各页表项, 指向下一集目录
- 页表覆盖范围较小, 内核代码 512MB, 直接映射区 1GB
- 内核态也定义 mm_struct 指向 swapper_pg_dir
- 初始化内核态页表, start_kernel→ setup_arch
- load_cr3(swapper_pg_dir) 并刷新 TLB
- 调用 init_mem_mapping→kernel_physical_mapping_init, 用 __va 将物理地址映射到虚拟地址, 再创建映射页表项
- CPU 在保护模式下访问虚拟地址都必须通过 cr3, 系统只能照做
- 在 load_cr3 之前, 通过 early_top_pgt 完成映射
- swapper_pg_dir 指向内核顶级页目录 pgd
- vmalloc 和 kmap_atomic
- 内核的虚拟地址空间 vmalloc 区域用于映射
- kmap_atomic 临时映射
- 32 位, 调用 set_pte 通过内核页表临时映射
- 64 位, 调用 page_address→lowmem_page_address 进行映射
- 内核态缺页异常
- kmap_atomic 直接创建页表进行映射
- vmalloc 只分配内核虚拟地址, 访问时触发缺页中断, 调用 do_page_fault→vmalloc_fault 用于关联内核页表项
- kmem_cache 和 kmalloc 用于保存内核数据结构, 不会被换出; 而内核 vmalloc 会被换出