跳转至

03 内存管理

一 虚拟内存

进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU) 的映射关系,来转换变成物理地址。

Pasted image 20250720021047

1.1 内存分段

  • 四个段代码分段、数据分段、栈段、堆段
  • 分段机制下的虚拟地址由两部分组成,段选择因子段号)和段内偏移量
  • 段表:段的基地址界限、特权等级 问题:
  • 内存碎片(外部内存碎片)
  • 内存交换效率低

1.2 内存分段

  • Linux:4KB
  • 页表:存储在内存里的,CPU的内存管理单元MMU 负责将虚拟内存地址转换成物理地址
  • 缺页异常
  • 内存碎片:无外部,有内部内存碎片
  • 页面置换:最近未被使用LRU,一次一个或几个页,内存交换的效率就相对比较高。
  • 分段机制下的虚拟地址由两部分:页号+页内偏移量
  • 页表:页表由虚拟页号对应物理页号,含物理页每页所在物理内存的基地址
  • 简单分页的问题:32位--每个进程都有一个4MB内存存页表,那100个进程就是巨大的开销
  • 多级页表: 100 多万个「页表项」的单级页表再分页
    • 二级页表虚拟地址由:一级页表+二级页表+页内偏移组成
    • 为什么不会更大?程序一般不会使用大量空间,需要时才会创建对应的二级页表。
    • 64位已经发展到4级目录了
  • TLB快表(页表缓存):程序是有局部性,把最常访问的几个页表项存储到访问速度更快的硬件(CPU)。
    • 是一种Cache,装在CPU里,和MMU交互
    • 命中率很高,CPU在寻址时,会先查TLB

1.3 段页式内存管理

  • 地址结构:段号、段内页号和页内位移
  • 内存:每一个程序一张段表,每个段又建立一张页表

1.4 Linux内存布局

  • 早期IntelCPU:(段+偏移)逻辑地址-段式内存管理->线性(虚拟)地址-页式内存管理->物理地址
  • Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间屏蔽了处理器中的逻辑地址概念
  • Linux分内核空间和用户空间
    • 进程在用户态时,只能访问用户空间内存;进入内核态后,才可以访问内核空间的内存;
    • 内核地址:每个进程都各自有独立的虚拟内存。但是每个虚拟内存中的内核地址,关联的都是相同的物理内存。切换到内核态后,就可以很方便地访问内核空间内存
    • 用户空间内存:从下到上
      • 保留区:非合法地址,比如无效指针的NULL指向这块
      • 代码段:二进制可执行代码+常量
      • 数据段:已初始化的静态变量+全局变量
      • BSS段:未初始化的~
      • 堆段:动态分配的内存,动态分配
      • 文件映射区(保留区):动态库共享内存,动态分配
      • 栈段

内存满了,发生什么(缺页中断过程)?

  • 内存分配过程:缺页中断,进程从用户态切换到内核态,内核的缺页中断函数会进行处理。缺页中断函数会做如下处理:
    • 查看有无空闲内存:有则直接分配物理内存,建立内部映射关系
    • 回收内存工作:没有则触发回收
      • 后台内存回收(异步)
      • 直接内存回收:如果后台跟不上进程内存申请的速度,会同步并阻塞进程执行。
  • 如果回收后,仍然无法满足。则触发OOM机制(out of memory)
    • OOM:据算法选择一个占用物理内存较高的进程杀死,还不够久继续杀,直到释放足够的内存位置。
  • 内存紧张时,会自动进行回收。
    • 文件页:内核缓存磁盘数据文件数据。(干净页)大部分都可以释放了,(脏页)要用再重新读取。被应用修改过还没写入磁盘的,那就写了再释放。
    • 匿名页:没有文件这样的载体。但是会用Swap机制写入磁盘等待再次读取,换入换出。
    • 页的回收倾向可以手动调控。
    • 页的回收算法LRU:两个链表:活跃和不活跃两个链表,优先回收不活跃的。

预读失效和缓存污染问题

  • 文件缓存:读取的文件会被存储在Page Cache中, 加速访问,大小有限。
  • 同样是LRU管理文件页

预读失效? - 提前加载进来的页,并没有被访问,白读了。 - LRU前列却没读,还要等待淘汰地址,Cache命中率大大降低。、 - 解决:只有真正读的时候,再放到lru活跃链表里。

缓存污染: - 数据访问一次,就活跃头部了,之前的热点数据全部淘汰。后续不再读那就是缓存污染。

Linux LRU机制总结

活跃和不活跃链表: - 真正读了第二次,就到活跃头部。 - 活跃末尾被淘汰,降级为不活跃头部 - 不活跃降级才是淘汰。

虚拟内存管理

PCB:task_struct - pid - file_struct *files - mm_struct

每次创建会携带一个mm_struct(COW) - 共享:变成线程了。是否共享地址空间几乎是Linux进程和线程之间的本质区别。

内核线程和用户态线程的本质区别:就是内核线程没有相关的内存描述符 mm_struct,所以内核线程之间调度是不涉及地址空间切换的。

mm_struct内容: - task_size:定义用户态地址空间和内核态地址空间分界线。 - 进程虚拟内存空间:data、text、brk、stack的起始、args和env的起始、mmap的起始,以及根据权限区分的VMA区域,还有rbtree,mmap分配VMA存储到一颗RBTREE上 - 内核内存空间: - 直接映射区:直接映射到物理地址(不过还是走的页表)。存进程相关描述符,task_struct、mm_struct、vm_area_struct之类的。以及内核栈(容量小,固定) - 8M空洞 - vmalloc映射区:分配页 - 永久映射区:允许建立与物理高端内存的长期映射关系 - 固定映射区:有些模块需要使用虚拟内存并映射到指定的物理地址上,使用该地址的模块在初始化的时候,将这些固定分配的虚拟地址映射到指定的物理地址上去。 - 临时映射区:缓存页