"Linux内核-内存管理"

  "内存管理"

Posted by Xu on July 17, 2018

内存管理

页框管理

页描述符

  • 内核必须记录每个页框当前的状态。内核必须能区分哪些页框属于进程的页,哪些页框包含内核代码或内核数据等等信息。
  • 所以我们使用page 的页描述符来记录每个页框的这些信息
  • 所有的页描述符都存放在mem_map数组中,每个描述符的长度为32字节
  • 一些与页描述符相关的宏
    • virt_to_page(addr):产生线性地址addr对应的页描述符地址
    • pfn_to_page(pfn):产生于页框号对应的页描述符

page的结构体结构如下所示

struct page{
    unsigned long           flags;//页状态标志位,包含很多状态
    atomic_t                _count;//引用计数,当该值为-1时,说明空闲
    atomic_t                _mapcount;//
    unsigned                private;
    struct address_space    *mapping;//指向和该页相关的地址空间对象
    pgoff_t                 index;
    struct list_head        lru;
    void                    *virtual;//页的虚拟地址
};

  • 由于硬件的限制,内核不能对所有的页一视同仁,有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。由于这种限制,所以内核将页划分为不同的区。
  • linux必须处理如下两种由于硬件存在缺陷而引起的寻址问题:
    • 一些硬件只能用某些特定的内存地址执行DMA
    • 一些体系结构的内存的物理地址范围比虚拟寻址范围大的多,这样就有一些物理地址永远不能的映射到内核空间
  • 根据这些限制,linux内核主要使用四种区:
    • ZONE_DMA:该区包含的页可以执行DMA操作,包含所有低于16MB的内存页框
    • ZONE_NORMAL:包含能正常映射的页框,16MB~896MB
    • ZONE_HIGHMEN:高端内存,其中的页并不能永久的映射到内核地址空间。

页的获取

内核提供了一种请求内存的底层机制,并提供了几个接口:

page_allocate

  1. 最核心的函数:

         struct page *alloc_page(gfp_t gfp_mask,unsigned int order);
    
    • 请求2^order个连续的页框,返回一个指针,执行第一个页框的页描述符page地址
  2. 将给定的页描述符转换为其逻辑地址:

         void *page_address(struct page* page);
    
  3. 直接返回第一个页的逻辑地址

         unsigned long _get_free_page(gfp_t gfp_mask,int order );
    
  4. 如果只需要一页即可,用下面两个封装好的函数:

         struct page * alloc_page(gfp_t gfp_mask);
         unsigned long _get_free_page(gfp_t gfp_mask);
    
  5. 获得填充为0的页

         unsigned long __get_zeroed_page(unsigned int gfp_mask);
    

页的释放

page_release.png

slab层

  • 分配和释放数据结构是所有内核最普遍的操作之一,为了防止数据结构不断的内存分配和初始化的过程,我们可以使用一个空闲链表来实现
  • 该链表上的结构体都是已经分配好的数据结构
  • 当代码需要该数据结构时,直接获取即可,不需要分配内存后填充数据
  • 当代码不需要该数据结构时,返回空闲链表即可,而不是释放它
  • 所以空闲链表相当于对象高速缓存-快速存储频繁使用的对象类型

所以为了对以上各数据结构的空闲链表进行统一的管理,我们引入slab分配器,来扮演通用数据结构缓存层的角色

  • slab层的设计原则:
    • 频繁使用的数据结构也会频繁分配和释放,因此应该缓存它们
    • 频繁的分配和释放会导致大量的碎片,所以slab缓存的对象链表要连续的存放,已释放的数据结构又会放回到空闲链表,不会导致碎片
    • 回收的对象可以立即投入下一次分配
    • 对存放的对象进行“着色,防止多个对象映射到相同的高速缓存行(cache line)”

slab层设计

  • slab层把不同的对象划分为所谓的高速缓存组,其中每个缓存组存放不同类型的对象
  • 每种对象类型对应一个高速缓存,如一个高速缓存用于存放进程描述符(task_struct结构),一个高速缓存用于存放索引节点对象(struct inode)
  • 这些高速缓存又被划分为slab,slab由一个或多个物理上连续的页组成。每个slab包含一些对象成员
  • 每个slab的所能存放的对象个数一致,每个slab处于三种状态之一:
    • 部分满
    • 当内核中某个部分需要一个新的对象时,先从部分满的slab中分配对象,如果没有,再从空的slab中进行分配,如果空slab也不存在了,说明无法满足分配请求
  • 每个高速缓存(一个高速缓存存放一类对象)都使用kmem_cache结构来表示,该结构包含三个链表:
    • slabs_full
    • slabs_partial
    • slabs_empty
  • 高速缓存和slab之间的关系:
    • cache_slab
  • slab的数据结构

          struct slab{
              struct list_head list;//位于高速缓存中的哪条链表上
              unsigned long colouroff;//slab着色的偏移量
              void *s_mem;//在slab中的第一个对象
              unsigned int inuse;//slab已经分配的对象数
              kmem_bufctl_t free;//第一个空闲对象
          }
    
  • slab描述符:要么在slab之外另行分配,要么放在slab自身开始的地方(足够小时)

slab着色与cpu硬件高速缓存

  • 同一硬件高速缓存行可以映射RAM中多个不同的块,相同大小的对象倾向于存放在高速缓存内相同的偏移量处。
  • 在不同slab内具有相同偏移量的对象最终很可能映射到同一高速缓存行中。而使用slab分配器的对象通常是频繁使用的小对象
  • 高速缓存的硬件可能因此而花费内存周期在同一高速缓存行与RAM内存单元之间来来往往的传送两个对象。

如下例:假设cache行为32Bytes,CPU包含512个cache行(缓存大小16K)。

  • 假设对象A,B均为32B,且A的地址从0开始,B的地址从16K开始,则根据组相联或直接相联映射方式(全相联方式很少使用),A,B对象很可能映射到cache的第0行
  • 此时,如果CPU交替的访问A,B各50次,每一次访问cache第0行都失效,从而需要从内存传送数据。
  • slab着色就是为解决该问题产生的,不同的颜色代表了不同的起始对象偏移量,对于B对象,如果将其位置偏移向右偏移32B,则其可能会被映射到cache的第1行上,这样交替的访问A,B各50次,只需要2次内存访问即可。

这里的偏移量就代表了slab着色中的一种颜色,不同的颜色代表了不同的偏移量,尽量使得不同的对象的对应到不同的硬件高速缓存行上,以最大限度的提高效率。实际的情况比上面的例子要复杂得多,slab的着色还要考虑内存对齐等因素,以及slab内未用字节的大小,只有当未用字节数足够大时,着色才起作用。