初探Linux内核中的内存管理

Linux内核设计与实现之内存管理的读书笔记

初探Linux内核管理

  1. 内核本身不像用户空间那样奢侈的使用内存;
    • 内核不支持简单快捷的内存分配机制, 用户空间支持? 这种简单快捷的内存分配机制是什么呢?
    • 内核不能睡眠;
    • 内核空间和用户空间分配内存是不一样的, 差一点在哪里呢?
  2. 内核是如何管理内存?
    • 内核把物理页作为内存管理的基本单位;
      • 因为内存管理单元通常以页为单位进行处理;
      • 从内存管理单元的角度来看, 页是最小的单位;
    • 什么是内存管理单元(MMU) -- 就是把虚拟地址转换为物理地址的硬件; 那什么是虚拟地址呢?
      • 体系结构不同, 页的大小也可能不同; 体系结构不同代表的含义可能是芯片的架构不一样, 32位机, 64位机;
      • 虚拟内存和物理内存有什么区别?
  3. 高端内存并不永久的映射到内核地址空间上, 什么是高端内存?
  4. 内核用page数据结构来描述当前时刻在相关物理内存中存放的东西;
    • page结构体的目的在于描述物理内存本身, 而不是描述包含其中的数据;
    • 内核用page结构管理所有的页, 内核需要知道每一个页是否空闲, 如果已经被分配, 内核需要知道谁拥有这个页;
      • 拥有者可能是用户空间进程, 动态分配的内核数据, 高速缓存等;
    • 每一个物理页都对应一个page结构体
  5. 区的概念?
    • 内核不能对所有的页一视同仁, 一些页位于特定的物理内存上, 用于特定的任务;
      • 内核会把页分成区; 内核使用区对相似的页进行分组;
    • 内核必须处理两种情况(由硬件缺陷引起的)
      • 一些硬件只能使用某些特定的内存来执行DMA(直接内存访问);
      • 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多, 有一些内存就不能永久的映射到内核空间上;
    • 四种不同的分区
      • ZONE_DMA
      • ZONE_DMA32
      • ZONE_NORMAL
      • ZONE_HIGHEM -- 高端内存中的页不能永久的映射到内核空间中, 32位体系结构中有用;
        • 其他体系结构上, 所有内存都被直接映射;
    • 区的实际分布和使用和体系结构相关
    • Linux把系统的页划分为区, 形成不同的内存池, 根据不同的用途进行分配;
    • 区的划分没有任何物理意义, 只是内核为了管理页而采取的一种逻辑上的分组;
  6. 64位机上的zone结构体有一个自旋锁, 它防止该结构体被并发访问;
  7. 内核提供了一种请求内存的底层机制, 并提供了对它的访问接口;
    • alloc_pages函数分配2^order个连续的物理页, 返回的指针指向第一个页的page结构体;
    • page_address把页转换为逻辑地址, 返回当前页的逻辑地址;
    • __get_free_pages直接返回的是所请求的第一个页的物理地址, 应该封装了alloc_pages和page_address函数;
    • 如果分配的页是给用户空间的, 调用get_zeroed_page来分配全为0的页;
    • 释放页, 只能释放属于自己的页;
      • __free_pages, free_pages, free_page
      • 内核是完全相信自己的;
    • 如果以字节为单位分配内存, 直接调用内核函数kmalloc就可以了, 返回的是连续的物理地址;
      • kfree释放由kmalloc分配出来的内存块;
      • 注意的是调用kfree(NULL)是安全的;
  8. 分配器标志-gfp_mask标志
    • 行为修饰符 -- 内核应该如何分配所需的内存
    • 区修饰符 -- 表示从哪里分配内存
    • 类型 -- 指定所需的行为和区描述符以完成特殊类型的处理;
    • 内核最常用的标志是GFP_KERNEL, 这种分配会引起睡眠, 它使用的是普通优先级(在没有锁被持有等情况下使用);
  9. vmalloc也是以字节为单位分配内存, 但它与kmalloc不同的是, 分配的内存虚拟地址是连续的, 物理地址不一定是连续的;
    • 和malloc类似的, malloc返回的页在进程的虚拟地址空间内是连续的, 但不能保证在物理RAM中也是连续的;
    • 只有硬件设备需要得到物理空间连续的地址;
    • 对内存而言, 所有的内存看起来都是逻辑上连续的;
    • 出于性能考虑, 内核很多代码都是直接调用kmalloc来获得内存, 而不是调用vmalloc;
    • vmalloc为了把物理上不连续的页转换为虚拟空间中连续的页, 必须建立专门的页表;
    • 更糟糕的是vmalloc获得页需要一个一个地进行映射;
      • 导致TLB(一种硬缓冲区)抖动;
    • 什么时候用vmalloc, 获得大内存块时, 模块动态地插入到内核中, 把模块装载到由vmalloc分配的内存上;
    • vmalloc函数可能会睡眠, 不能在中断上下文进行调用;
  10. slab 层(slab分配器, )
    • slab层是为了便于数据的频繁分配和回收;
    • 空闲链表包含可用的, 已经分配好的数据结构体块, 代码直接从空闲链表中获得一个数据块, 不用重新分配, 不用了再放回空闲链表;
    • 空闲链表相当于对象高速缓存 -- 快速存储频繁使用的对象类型;
      • 空闲链表不能全局控制;
    • slab分配器扮演了通用数据结构缓存层的角色;
    • SMP(对称多处理结构)锁
    • slab层把不同的对象划分为高速缓存组, 每个缓存组存放不同类型的对象;
      • 一个高速缓存用于存放进程描述符, 一个高速缓存存放索引节点对象;
      • kmalloc接口建立在slab层上, 使用了一组通用的高速缓存;
        • 高速缓存又被划分为slab, 这里的slab是由一个或多个连续的页组成, 一般slab仅仅是由一页组成, 但每个高速缓存可以由多个slab组成;
        • slab有三种状态: 满, 部分满或空, --- 最终目的是为了减少内存碎片;
    • inode结点是磁盘索引节点在内存中的体现, 会频繁的创建和释放, 用slab来管理inode节点;
      • 每个slab包含尽可能多的inode对象, 创建一个对象, 从部分满或者空的slab中返回一个指向已分配但未使用的结构的指针。
      • 内核用完一个对象后, slab分配器就把该对象标记为空闲;
    • 高速缓存 >> slab >> 具体对象
      • 高速缓存是较大的内存空间, 里面包含了几个slab;
      • 而一个slab中又包含好几个同一类的对象;
      • 每个高速缓存都使用kmem_cache结构来表示, 包含三个链表:
        • slabs_full
        • slabs_partial
        • slabs_empty
      • slab分配器可以创建新的slab, 通过__get_free_pages低级内核页分配器进行;
        • __get_free_pages为高速缓存分配足够多的内存, 分配的页为2的幂次方;
        • NUMA系统上较好的性能(非一致的内存访问);
    • slab的管理是在每个高速缓存的基础上, 并提供内核一个简单的接口;
      • 通过接口可以创建和撤销新的高速缓存, 并在高速缓存内分配和释放对象;
      • 高速缓存及其内部slab的复杂管理完全通过slab层的内部机制来处理;
      • 当创建一个高速缓存后, slab层所起得作用就像一个专用的分配器, 可以为具体的对象类型进行分配;
    • 要理解slab层的具体实现;
    • slab分配器的接口
      • 新建一个高速缓存 -- kmem_cache_create;
      • 对齐越严格, 浪费的内存越多
      • 撤销一个高速缓存 -- kmem_cache_destroy, 可能睡眠;
        • 调用kmem_cache_destroy过程中(调用之后), 不再访问这个高速缓存;
        • 调用者必须确保这种同步;
      • 从缓存中分配: kmem_cache_alloc, 从给定的高速缓存中返回对象的指针;
        • 如果高速缓存的所有slab中都没有空闲的对象, 那么slab层必须通过kmem_getpages来获取新的页;
      • 释放一个对象 -- kmem_cache_free;
      • 进程描述符是内核的核心组成部分;
      • slab层负责内存紧缺情况下所有底层的对齐, 着色, 分配, 和回收等;
      • 如果要频繁的创建很多相同类型的对象, 应该考虑使用slab高速缓存, 不要自己去实现空闲链表;
  11. 在栈上的静态分配
    • 用户空间的内存分配在栈上进行静态分配;
      • 用户空间能够非常奢侈的负担起非常大的栈, 而且栈空间还可以动态增长;
    • 内核栈小而固定, 给每个进程分配一个固定大小的小栈后, 不但可以减少内存的消耗, 而且内核也无需负担太重的栈管理任务;
      • 每个进程都有两页的内核栈, 32位机一页为4K, 64位机一页为8K, 所以内核栈的大小分别为8KB和16KB;
    • 随着机器运行时间的增加, 寻找两个未分配的, 连续的页变得越来越困难, 内存渐渐变成碎片, 因此给一个新进程分配一个虚拟内存(VM)的压力也在增大;
    • 中断栈: 中断栈为每个进程提供一个用于中断处理程序的栈;
      • 中断处理程序不用再和被中断的进程共享一个内核栈, 它可以使用自己的栈;
    • 内核栈可以是一页, 也可以是两页, 所以栈的大小为4~16KB;
    • 再具体的函数中, 让所有局部变量(即所谓的自动变量)所占空间之和不要超过几百字节;
    • 在栈上进行大量的静态分配是很危险的(分配大型数组或者大型结构体);
  12. 高端内存的映射
    • 高端内存中的页不能永久地映射到内核地址空间上;
    • 通过alloc_pages函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址;
    • x86体系结构上, 896MB以上的物理内存大都是高端内存, 它并不会永久地或自动地映射到内核地址空间;
    • 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上;
    • 在x86上, 高端内存中的页被映射到3G~4G;
    • 什么是永久映射?
      • kmap -- 映射一个给定的page结构到内核地址空间, 高端内存和低端内存中都能使用;
        • 函数返回该页的虚拟地址;
        • 可以永久地映射高端内存;
      • kunmap解除映射;
    • 临时映射
      • 临时映射可以用到不能睡眠的地方(中断处理程序);
        • kmap_atomic -- 建立一个临时映射;
        • kunmap_atomic -- 取消映射;
  13. 每个CPU的分配
    1. SMP(对称多处理器)的现代操作系统使用每个CPU上的数据, 对于给定的处理器其数据是唯一的
      • 一般来说, 每个CPU的数据存放着一个数组中, 数组的每一项对应着系统上一个存在的处理器;
      • 按处理器号, 确定这个数组的当前元素;
      • my_percpu[NR_CPUS];
      • get_cpu -- 获得当前处理器, 并禁止内核抢占;
      • put_cpu -- 激活内核抢占;
    2. 新的每个CPU接口
      • percpu 简化了创建和操作每个CPU的数据;
        • 更适合大型对称多处理器计算机的要求;
      • 编译时定义每个CPU数据
        • DEFINE_PER_CPU(type, name); -- 为每个处理器创建一个类型为type, 名字为name的变量实例;
        • DECLARE_PER_CPU -- 声明变量;
        • get_cpu_var(增加该处理器上的 name变量的值)和put_cpu_var(完成: 重新激活内核的抢占);
        • per_cpu(name, cpu)++, 增加指定处理器上的name变量的值;
          • 不会禁止内核抢占, 也不会提供任何形式的锁保护;
      • 编译时每个CPU数据的例子并不能在模块内使用, 因为链接程序实际上将他们创建在一个唯一的可执行段中(.data.percpu)
  14. 分配函数的选择
    • 如果需要连续的物理页, 就可以使用某个低级页分配器或者kmalloc;
    • 要创建和撤销很多大的数据结构, 那么考虑建立slab高速缓存;
    • 虚拟文件系统(VFS) -- 负责管理文件系统且为用户空间程序提供一致性接口的内核子系统;
posted @ 2018-09-06 15:12  coding-for-self  阅读(239)  评论(0编辑  收藏  举报