趣谈linux操作系统--Linux内存管理笔记

  • 内存管理包含: 物理内存管理; 虚拟内存管理; 两者的映射

  • 除了内存管理模块, 其他都使用虚拟地址(包括内核)

  • 虚拟内存空间包含: 内核空间(高地址); 用户空间(低地址)

  • 用户空间从低到高布局为: 代码段; 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 级页表

    由于x86_64处理器硬件限制。x86_64处理器地址线只有48条,故而导致硬件要求传入的地址48位到63位地址必须相同。 4K页面下, 48位线性地址分为5段,位宽度分别是9、、9、9、12。映射的方法为页表查找。

    在这里插入图片描述

    在这里插入图片描述

//struct mm_struct 主要内容
unsigned long mmap_base;  /* base of mmap area */
//虚拟地址空间中用于内存映射的起始地址从高地址到低地址增长
unsigned long total_vm;    /* Total pages mapped */总共映射的页的数目
unsigned long locked_vm;  /* Pages that have PG_mlocked set */被锁定不能换出
unsigned long pinned_vm;  /* Refcount permanently increased */不能换出,也不能移动。
unsigned long data_vm;    /* VM_WRITE & ~VM_SHARED & ~VM_STACK */数据的页的数目
unsigned long exec_vm;    /* VM_EXEC & ~VM_WRITE & ~VM_STACK */可执行文件的页的数目
unsigned long stack_vm;    /* VM_STACK */栈所占的页的数目
unsigned long start_code, end_code,//可执行代码的开始和结束位置
 start_data, end_data;//已初始化数据的开始位置和结束位置。
unsigned long start_brk, brk, start_stack;
//start_brk堆的起始位置,brk 是堆当前的结束位置,
//start_stack 是栈的起始位置,栈的结束位置在寄存器(在esp寄存器里)的栈顶指针中。
unsigned long arg_start, arg_end, env_start, env_end;
//arg_start 和 arg_end 是参数列表的位置, 
//env_start 和 env_end 是环境变量的位置。它们都位于栈中最高地址的地方。

struct vm_area_struct *mmap;    /* list of VMAs */
struct rb_root mm_rb;

在这里插入图片描述

 //vm_area_struct
struct vm_area_struct {
  /* The first cache line has the info for VMA tree walking. */
  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;
  struct rb_node vm_rb;
  struct mm_struct *vm_mm;  /* The address space we belong to. */
  struct list_head anon_vma_chain; /* Serialized by mmap_sem &
            * page_table_lock */
  struct anon_vma *anon_vma;  /* Serialized by page_table_lock */ //映射到物理内存的时候称为匿名映射,
  /* Function pointers to deal with this struct. */
  const struct vm_operations_struct *vm_ops;
  struct file * vm_file;    /* File we map to (can be NULL). */ //映射到文件
  void * vm_private_data;    /* was vm_pte (shared mem) */
} __randomize_layout;
建立内存映射

static int load_elf_binary(struct linux_binprm *bprm)
{
......
  setup_new_exec(bprm);
......
  retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
         executable_stack);
......
  error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
        elf_prot, elf_flags, total_size);
......
  retval = set_brk(elf_bss, elf_brk, bss_prot);
......
  elf_entry = load_elf_interp(&loc->interp_elf_ex,
              interpreter,
              &interp_map_addr,
              load_bias, interp_elf_phdata);
......
  current->mm->end_code = end_code;
  current->mm->start_code = start_code;
  current->mm->start_data = start_data;
  current->mm->end_data = end_data;
  current->mm->start_stack = bprm->p;
......
}
  • load_elf_binary 会完成以下的事情:
    • 调用 setup_new_exec,设置内存映射区 mmap_base;
    • 调用 setup_arg_pages,设置栈的 vm_area_struct,这里面设置了 mm->arg_start 是指向栈底的,current->mm->start_stack 就是栈底;
    • elf_map 会将 ELF 文件中的代码部分映射到内存中来;
    • set_brk 设置了堆的 vm_area_struct,这里面设置了 current->mm->start_brk = current->mm->brk,也即堆里面还是空的;
    • load_elf_interp 将依赖的 so 映射到内存中的内存映射区域。
      在这里插入图片描述
32 位的内核态的布局

在这里插入图片描述在这里插入图片描述

所谓的直接映射区,就是这一块空间是连续的,和物理内存是非常简单的映射关系,其实就是虚拟内存地址减去 3G,就得到物理内存的位置

64 位的内存布局

在这里插入图片描述在这里插入图片描述- 内存管理信息在 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(内核代码, 也采用直接映射)

在这里插入图片描述
如果有多个 CPU,那就有多个节点。每个节点用 struct pglist_data 表示,放在一个数组里面。每个节点分为多个区域,每个区域用 struct zone 表示,也放在一个数组里面。每个区域分为多个页。为了方便分配,空闲页放在 struct free_area 里面,使用伙伴系统进行管理和分配,每一页用 struct page 表示。

- 物理内存组织方式
- 每个物理页由 struct page 表示
- 物理页连续, page 放入一个数组中, 称为平坦内存模型
- 多个 CPU 通过总线访问内存, 称为 SMP 对称多处理器(采用平坦内存模型, 总线成为瓶颈)
- 每个 CPU 都有本地内存, 访问内存不用总线, 称为 NUMA 非一致内存访问
- 本地内存称为 NUMA 节点, 本地内存不足可以向其他节点申请
- NUMA 采用非连续内存模型,页号不连续
- 另外若内存支持热插拔,则采用稀疏内存模型

  • 节点

    • 用 pglist_data 表示 NUMA 节点,多个节点信息保存在 node_data 数组中
    • pglist_data 包括 id,page 数组,起始页号, 总页数, 可用页数
    • 节点分为多个区域 zone, 包括 DMA; 直接映射区; 高端内存区; 可移动区(避免内存碎片)
  • 区域 zone

    • 用 zone 表示; 包含第一个页页号; 区域总页数; 区域实际页数; 被伙伴系统管理的页数; 用 per_cpu_pageset 区分冷热页(热页, 被 CPU 缓存的页)
    • 用 struct page 表示, 有多种使用模式, 因此 page 结构体多由 union 组成
    • 使用一整个页: 1) 直接和虚拟地址映射(匿名页); 2) 与文件关联再与虚拟地址映射(内存映射文件)
      • page 记录: 标记用于内存映射; 指向该页的页表数; 换出页的链表; 复合页, 用于合成大页;
    • 分配小块内存:
      • Linux 采用 slab allocator 技术; 申请一整页, 分为多个小块存储池, 用队列维护其状态(较复杂)
      • slub allocator 更简单
      • slob allocator 用于嵌入式
      • page 记录: 第一个 slab 对象; 空闲列表; 待释放列表
  • 页分配

    • 分配较大内存(页级别), 使用伙伴系统
    • Linux 把空闲页分组为 11 个页块链表, 链表管理大小不同的页块(页大小 2^i * 4KB)
    • 分配大页剩下的内存, 插入对应空闲链表
    • alloc_pages->alloc_pages_current 用 gfp 指定在哪个 zone 分配

    小内存分配, 例如分配 task_struct 对象
    在这里插入图片描述

里面就有三个变量:size 是包含这个指针的大小,object_size 是纯对象的大小,offset 就是把下一个空闲对象的指针存放在这一项里的偏移量。

在这里插入图片描述

在分配缓存块的时候,要分两种路径,fast path 和 slow path,也就是快速通道和普通通道。其中 kmem_cache_cpu 就是快速通道,kmem_cache_node 是普通通道。每次分配的时候,要先从 kmem_cache_cpu 进行分配。如果 kmem_cache_cpu 里面没有空闲的块,那就到 kmem_cache_node 中进行分配;如果还是没有空闲的块,才去伙伴系统分配新的页。

//kmem_cache_cpu 里面是如何存放缓存块
struct kmem_cache_cpu {
  void **freelist;  /* Pointer to next available object */
  unsigned long tid;  /* Globally unique transaction id */
  struct page *page;  /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
  struct page *partial;  /* Partially allocated frozen slabs */
#endif
......
};
  • 会调用 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 设置申请大块内存
  • 页面换出
    • 触发换出:
      1. 分配内存时发现没有空闲; 调用 get_page_from_freelist->node_reclaim->__node_reclaim->shrink_node
      1. 内存管理主动换出, 由内核线程 kswapd 实现
    • kswapd 在内存不紧张时休眠, 在内存紧张时检测内存 调用 balance_pgdat->kswapd_shrink_node->shrink_node
    • 页面都挂在 lru 链表中, 页面有两种类型: 匿名页; 文件内存映射页
    • 每一类有两个列表: active 和 inactive 列表
    • 要换出时, 从 inactive 列表中找到最不活跃的页换出
    • 更新列表, shrink_list 先缩减 active 列表, 再缩减不活跃列表
    • 缩减不活跃列表时对页面进行回收:
      • 匿名页回收: 分配 swap, 将内存也写入文件系统
      • 文件内存映射页: 将内存中的文件修改写入文件中
        在这里插入图片描述物理内存分 NUMA 节点,分别进行管理;每个 NUMA 节点分成多个内存区域;每个内存区域分成多个物理页面;伙伴系统将多个连续的页面作为一个大的内存块分配给上层;kswapd 负责物理页面的换入换出;Slub Allocator 将从伙伴系统申请的大内存块切成小块,分配给其他系统。

用户态内存映射

  • 申请小块内存用 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_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)
  • 避免每次都需要经过页表(存再内存中)访问内存

    • TLB 缓存部分页表项的副本

    在这里插入图片描述

    • 用户态内存映射函数 mmap,包括用它来做匿名映射和文件映射。
    • 用户态的页表结构,存储位置在 mm_struct 中。
    • 在用户态访问没有映射的内存会引发缺页异常,分配物理页表、补齐页表。如果是匿名映射则分配物理内存;如果是 swap,则将 swap 文件读入;如果是文件映射,则将文件读入。

    内核态内存映射

    在这里插入图片描述在这里插入图片描述- 涉及三块内容:

    • 内存映射函数 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 完成映射
  • 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 会被换出

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值