前言

  26嵌入式软开选手,秋招被问这块八股被问麻了,啥也回答不出来>…<,不过知道自己这块薄弱那就了解补充一下嘛(以后有时间了持续深入研究一下爱)。这篇博文主要是记录自己在网上看博客和书籍的一个记录。
在这里也推荐一下
书籍: 《Linux内核设计与实现》 《深入理解Linux进程与内存》 《深入理解Linux虚拟内存管理》
博客:一步一图带你深入理解Linux虚拟内存

1. Linux物理内存管理

Linux的物理内存管理算法有memblock / SLAB / Buddy

  • 1.1 memblock算法
    是内核启动初期的内存管理算法,这个目前也没有人问过,先标记后面再看
    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的转换都变得更复杂了
      在这里插入图片描述
  • 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;
          }
      
          ```
      
  • 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把物理地址发送给内存控制器,内存控制器进行读写 (硬件完成)
      在这里插入图片描述

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐