Linux内核设计与实现之内存管理的读书笔记
初探Linux内核管理
- 内核本身不像用户空间那样奢侈的使用内存;
- 内核不支持简单快捷的内存分配机制, 用户空间支持? 这种简单快捷的内存分配机制是什么呢?
- 内核不能睡眠;
- 内核空间和用户空间分配内存是不一样的, 差一点在哪里呢?
- 内核是如何管理内存?
- 内核把物理页作为内存管理的基本单位;
- 因为内存管理单元通常以页为单位进行处理;
- 从内存管理单元的角度来看, 页是最小的单位;
- 什么是内存管理单元(MMU) -- 就是把虚拟地址转换为物理地址的硬件; 那什么是虚拟地址呢?
- 体系结构不同, 页的大小也可能不同; 体系结构不同代表的含义可能是芯片的架构不一样, 32位机, 64位机;
- 虚拟内存和物理内存有什么区别?
- 高端内存并不永久的映射到内核地址空间上, 什么是高端内存?
- 内核用page数据结构来描述当前时刻在相关物理内存中存放的东西;
- page结构体的目的在于描述物理内存本身, 而不是描述包含其中的数据;
- 内核用page结构管理所有的页, 内核需要知道每一个页是否空闲, 如果已经被分配, 内核需要知道谁拥有这个页;
- 拥有者可能是用户空间进程, 动态分配的内核数据, 高速缓存等;
- 每一个物理页都对应一个page结构体
- 区的概念?
- 内核不能对所有的页一视同仁, 一些页位于特定的物理内存上, 用于特定的任务;
- 内核会把页分成区; 内核使用区对相似的页进行分组;
- 内核必须处理两种情况(由硬件缺陷引起的)
- 一些硬件只能使用某些特定的内存来执行DMA(直接内存访问);
- 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多, 有一些内存就不能永久的映射到内核空间上;
- 四种不同的分区
- ZONE_DMA
- ZONE_DMA32
- ZONE_NORMAL
- ZONE_HIGHEM -- 高端内存中的页不能永久的映射到内核空间中, 32位体系结构中有用;
- 区的实际分布和使用和体系结构相关
- Linux把系统的页划分为区, 形成不同的内存池, 根据不同的用途进行分配;
- 区的划分没有任何物理意义, 只是内核为了管理页而采取的一种逻辑上的分组;
- 64位机上的zone结构体有一个自旋锁, 它防止该结构体被并发访问;
- 内核提供了一种请求内存的底层机制, 并提供了对它的访问接口;
- 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)是安全的;
- 分配器标志-gfp_mask标志
- 行为修饰符 -- 内核应该如何分配所需的内存
- 区修饰符 -- 表示从哪里分配内存
- 类型 -- 指定所需的行为和区描述符以完成特殊类型的处理;
- 内核最常用的标志是GFP_KERNEL, 这种分配会引起睡眠, 它使用的是普通优先级(在没有锁被持有等情况下使用);
- vmalloc也是以字节为单位分配内存, 但它与kmalloc不同的是, 分配的内存虚拟地址是连续的, 物理地址不一定是连续的;
- 和malloc类似的, malloc返回的页在进程的虚拟地址空间内是连续的, 但不能保证在物理RAM中也是连续的;
- 只有硬件设备需要得到物理空间连续的地址;
- 对内存而言, 所有的内存看起来都是逻辑上连续的;
- 出于性能考虑, 内核很多代码都是直接调用kmalloc来获得内存, 而不是调用vmalloc;
- vmalloc为了把物理上不连续的页转换为虚拟空间中连续的页, 必须建立专门的页表;
- 更糟糕的是vmalloc获得页需要一个一个地进行映射;
- 什么时候用vmalloc, 获得大内存块时, 模块动态地插入到内核中, 把模块装载到由vmalloc分配的内存上;
- vmalloc函数可能会睡眠, 不能在中断上下文进行调用;
- 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高速缓存, 不要自己去实现空闲链表;
- 在栈上的静态分配
- 用户空间的内存分配在栈上进行静态分配;
- 用户空间能够非常奢侈的负担起非常大的栈, 而且栈空间还可以动态增长;
- 内核栈小而固定, 给每个进程分配一个固定大小的小栈后, 不但可以减少内存的消耗, 而且内核也无需负担太重的栈管理任务;
- 每个进程都有两页的内核栈, 32位机一页为4K, 64位机一页为8K, 所以内核栈的大小分别为8KB和16KB;
- 随着机器运行时间的增加, 寻找两个未分配的, 连续的页变得越来越困难, 内存渐渐变成碎片, 因此给一个新进程分配一个虚拟内存(VM)的压力也在增大;
- 中断栈: 中断栈为每个进程提供一个用于中断处理程序的栈;
- 中断处理程序不用再和被中断的进程共享一个内核栈, 它可以使用自己的栈;
- 内核栈可以是一页, 也可以是两页, 所以栈的大小为4~16KB;
- 再具体的函数中, 让所有局部变量(即所谓的自动变量)所占空间之和不要超过几百字节;
- 在栈上进行大量的静态分配是很危险的(分配大型数组或者大型结构体);
- 高端内存的映射
- 高端内存中的页不能永久地映射到内核地址空间上;
- 通过alloc_pages函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址;
- x86体系结构上, 896MB以上的物理内存大都是高端内存, 它并不会永久地或自动地映射到内核地址空间;
- 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上;
- 在x86上, 高端内存中的页被映射到3G~4G;
- 什么是永久映射?
- kmap -- 映射一个给定的page结构到内核地址空间, 高端内存和低端内存中都能使用;
- 函数返回该页的虚拟地址;
- 可以永久地映射高端内存;
- kunmap解除映射;
- 临时映射
- 临时映射可以用到不能睡眠的地方(中断处理程序);
- kmap_atomic -- 建立一个临时映射;
- kunmap_atomic -- 取消映射;
- 每个CPU的分配
- SMP(对称多处理器)的现代操作系统使用每个CPU上的数据, 对于给定的处理器其数据是唯一的
- 一般来说, 每个CPU的数据存放着一个数组中, 数组的每一项对应着系统上一个存在的处理器;
- 按处理器号, 确定这个数组的当前元素;
- my_percpu[NR_CPUS];
- get_cpu -- 获得当前处理器, 并禁止内核抢占;
- put_cpu -- 激活内核抢占;
- 新的每个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)
- 分配函数的选择
- 如果需要连续的物理页, 就可以使用某个低级页分配器或者kmalloc;
- 要创建和撤销很多大的数据结构, 那么考虑建立slab高速缓存;
- 虚拟文件系统(VFS) -- 负责管理文件系统且为用户空间程序提供一致性接口的内核子系统;
posted @
2018-09-06 15:12
coding-for-self
阅读(
239)
评论()
编辑
收藏
举报