接着上周的请页机制,今天写一下内存分配与回收。 在Linux中,CPU访问的不是物理内存而是虚拟内存。因此对于内存页面的管理,通常是先在虚拟内存空间中分配一个区间,然后才根据需要,为此区间分配相应的物理页面,并建立映射。 进程的执行和加载从操作系统的角度看,一个进程最关键的特征是拥有独立的虚拟地址空间。 当我们说一个进程在执行的时候,是在说如下步骤
![]() 当我们说一个程序在装载时在说什么?在上述步骤后,执行文件的指令和数据加载进内存,但并没有真正的装入物理内存,只是通过ELF文件头部信息建立起可执行文件与虚拟地址空间的映射关系而已,真正加载过程将在发生缺页异常处理时才进行,装载执行过程如下:
物理内存的管理和分配上周说的请页机制可以为进程请求物理内存,那么物理内存在内核中究竟是如何管理和分配的。 首先,我们看一下内核空间的划分。在X86-32体系架构上,内核空间的地址范围是PAGE_0FFSET(3GB) 到4GB。 内核空间的第一部分试图将系统的所有物理内存线性地映射到虚拟地址空间中,但最多只能映射 high_memory (默认为896M)大小的物理内存。大于high_memory 的物理内存将映射到内核空间的后部分。如图所示。![]() 按照这样的映射规则,0M到 high_memory 的物理内存称为低端内存,大于high_memory 的物理内存称为高端内存。从图中可以看出,内核采用了三种机制将高端内存映射到内核空间:永久内核映射、固定映射、vmalloc机制。 那么, 内核虚拟地址和物理地址如何进行转换? 内核为线性映射的内存区提供物理地址和虚拟地址的转换函数:
物理内存管理机制基于物理内存在内核空间中的映射原理,物理内存的管理方式也有 所不同。内核中物理内存的管理机制主要有以下四种:
伙伴算法Linux伙伴算法负责大内存的分配,它把所有的空闲页面分为10个块链表,每个链表中的一个块含有2的幂次个页面,这种块称为“页块”。 伙伴系统采用的数据结构是一个叫 free_area 的数组struct free_area_struct { 数组 free_area 包含三个域:next, prev, map。指针next和prev用于将物理结构页面struct page链接成一个双向链表,其中的数字表示内存块的起始页面号。而map域指向一个位图。申请过程:比如,我要分配4(2^2)页(16k)的内存空间,算法会先从 free_area[2] 中查看nr_free 是否为空,如果有空闲块,则从中分配,如果没有空闲块,就从它的上一级free_area[3] (每块32K)中分配出16K,并将多余的内存(16K)加入到free_area[2] 中去。如果free_area[3] 也没有空闲,则从更上一级申请空间,依次递推,直到free_area[max_order] ,如果顶级都没有空间,那么就报告分配失败。释放就是申请的逆过程。 满足以下条件的块称为伙伴
per-CPU页框高速缓存内核经常请求和释放单个页框。为了提升系统性能,每个内存管理区定义了一个“每CPU”页框高速缓存。所有“每CPU”高速缓存包含一些预先分配的页框,它们被用于满足本地CPU发出的单一内存请求。 在内存管理区中,分配单页使用per-cpu机制,分配多页使用伙伴算法。 slab缓存使用伙伴算法,每次分配至少一个页面,而当请求分配的内存为几十个字节的时候,怎么处理呢?Linux引入了slab机制。 伙伴算法的迁移机制很好的解决了外部碎片。 当我们申请几十个字节的时候,内核也是给我们分配一个页,这样在每个页中就形成了很大的浪费。称之为内部碎片。 内核中引入了slab机制去尽力的减少这种内部碎片。 slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构(例如: task_struct , file_struct 等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。slab分配器并不丢弃已经分配的对象,而是释放并把它们保存在内存中。slab分配对象时,会使用最近释放的对象的内存块,因此其驻留在cpu高速缓存中的概率会大大提高。每种对象的高速缓存是由若干个slab组成,每个slab是由若干个页框组成的。虽然slab分配器可以分配比单个页框更小的内存块,但它所需的所有内存都是通过伙伴算法分配的。 vmalloc机制非连续内存处于3G到4G之间的内核空间的高端内存区,也就是内核空间。 如图所示 ![]() 我们知道物理上连续的映射对内核是最好的,但不是总能成功。在分配一大块内存时,可能无法找到连续的内存块。内核使用 vmalloc() 接口函数,来分配在虚拟内存中连续但在物理内存中不一定连续的内存。vmalloc() 函数的原型是void * vmalloc(unsigned long size) 函数首先把size参数取整为页面大小(4KB)的一个倍数,也就是按照页的大小对齐,然后进行有效性检查,如果有合适大小的内存,就调用 get_vm_area() 获得一个内存区的结构,最后调用vmalloc_area_pages() 真正的进行非连续内存的分配,该函数实际上是建立了非连续内存区到物理页面的映射。简单说一下 vmalloc() 和kmalloc() 的区别首先vmalloc()与 kmalloc()都可用于内核空间分配内存。 kmalloc() 分配的内存处于3GB~high_memory 之间,这段内核空间与物理内存的映射一一对应,而vmalloc() 分配的内存在VMALLOC_START~VMALLOC_END 之间,这段非连续内存区映射到物理内存也可能是非连续的。如图所示 ![]() 总的来说就是 vmalloc() 分配的物理地址无需连续,而kmalloc() 确保页在物理上是连续。用户进程发出内存分配请求,到内核最终分配物理内存,这中间内核要做大量的工作,这里介绍了vmalloc()和kmalloc(),但它们最终都要调用伙伴算法,通过get_free_page()内核函数获得物理内存。如图所示: ![]() 当用户程序通过调用系统调用申请内存时,首先陷入内核,建立虚拟地址空间的映射,获得一块虚拟内存区VMA。当进程对这块虚存区进行访问时,如果物理内存尚未分配,那么此时发生一个缺页异常, 通过 get_free_pages 申请一个或多个物理页面,并将此物理内存和虚拟内存的映射关系写入页表。内存分配与回收先介绍到这里,这里面的水很深,我目前理解的很肤浅,后续有机会多写几篇来介绍内存管理相关的知识。 ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:暴走的耗子19,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ---------------------------------------------------------------------------------------------------------------------- |