-
struct page : 描述的是一个物理页, 内存管理的最小单元, 只描述的是物理页面本身, 程序和系统运行,存在数据的换入换出, 因此page中的数据和page无本质的联系. 数据结构定义在 :
include/linux/mm_types.h
主要内容:
(1) 页面状态(page-flags.h);
(2) 页的类型:
a. page cache/anonymous page;
b. page_pool used by netstak(DMA address);
c. slab, slob and slub;
d. slab kmem_cache (高速缓存);
e. compound page(复合页, 多个物理页组成的大页);
f. Page table page;
g. ZONE_DEVICE pages;
(3) 页面引用计数(page_ref.h);
(4) memory cgroup;
(5) 页面虚拟地址(asm/page.h);
-
struct zone : 由于硬件的限制, 内核并不能堆所有的内存页都一视同仁, 有些页位于内存中的特定物理地址上, 用于特定用途, 内核将具有相同特性的页分组, 形成不同的zone.
硬件缺陷或者局限性,导致的寻址问题:
(1) 一些硬件只能在特定地址执行DMA ;
(2) 一些体系结构下物理内存比虚拟内存大得多, 一些内存就不能永久映射到内存;
ZONE_DMA : 包含的页执行DMA 操作;
ZONE_DMA32: 32位 DMA 操作;
ZONE_NORMAL: 正常映射的页;
ZONE_HIGHMEM: 高端内存, 不能永久映射到内核地址空间的页;
数据结构定义在 :
include/linux/mmzone.h
各个zone的初始化:
kernel/mm/page_alloc.c
-
获得页 include/linux/gfp.h:
(1) alloc_pages() // 获取2的幂函数个连续的物理页;
(2) page_address() // 物理地址转换为虚拟地址
(3) __get_free_pages() // 获取第一个页的虚拟地址(和alloc_pages()作用相同);
(4) alloc_page() / __get_free_page() //获取一页
(5) get_zeroed_page() // 获取填充为0 的页(和__get_free_pages()类似);
(6) 以上都有对应的释放函数;
-
获取以字节为单位的内核内存 kmalloc()
分配的内存是物理上连续的.
分配器标志:
定义在 :
linux/gfp.h
linux/slab.h
(1) 行为修饰符
分配器是否可以睡眠, 是否可以阻塞, 是否可以执行磁盘IO , 文件系统IO等.
(2) 区修饰符
分配器从 DMA/DMA32/HIGHMEM/NORMAL zone上分配
(3) 类型修饰符
综合了以上行为/区修饰符, 定义的一种简易的修饰符, 也即常用的标识符.
GFP_KERNEL / GFP_ATOMIC等
vmalloc()
定义:
linux/vmalloc.h
mm/vmalloc.c
和kmalloc()类似, 申请内存在虚拟地址上是连续的,在物理地址上不连续.
可通过非连续的物理地址, 修改页表, 确保在虚拟地址上连续.
一般,只有硬件设备需要得到物理地址连续的内存, 软件使用的是连续的虚拟地址即可.
内核编程中, 一般使用kmalloc(), 而不是vmalloc(), 处于性能考虑:
(1) malloc申请的内存物理地址不连续, 为了虚拟地址上连续, 需要将物理地址上不连续的物理地址转换为虚拟地址上连续的;
(2) 需重新建立页表;
(3) 比直接内存映射更多的TLB 抖动;
一般, 在不得已下使用vmalloc(), 需要获取大块内存时, 比如动态装载内核模块
-
slab 背景: 内核中数据结构的分配和释放是最常用的操作之一, 为了便于数据的频繁分配和回收, 使用到了空闲链表, 事先分配好的,可供使用的数据块结构, 需要使用时候, 从链表获取, 不用时, 放回链表,不释放, 从某种程度上讲, 实现了高速缓存的目的. 使用过程中, 面临着问题:
(1) 不能全局控制, 当内存不足时, 内核无法通知每个空闲链表 , 并让他释放链表, 回收内存空间;
(2) 空闲链表也只是内存空间中的一些活跃的内存数据, 也无法特别识别他们;
综上, slab分配器诞生了, 设计时应当遵循如下基本原则: (1) 频繁使用的数据结构也会频繁分配和释放, 缓存他们; (2) 缓存会连续的放, 释放后会放回缓存, 所以即使频繁分配和释放, 也不会导致内存碎片; (3) 缓存能提高性能, 释放的内存对象会立即投入下一次分配, 频繁的分配和释放, 不会导致性能衰减; (4) 如果分配器明确内存对象的大小,页大小和高速缓存的大小等信息, 可以作出更加明智的决策; (5) 如果部分缓存专属于单个处理器, 分配和释放就可以不用加SMP锁; (6)如果分配器和NUMA相关, 可以从相同的内存节点进行分配; (7) 对存放的对象进行着色, 防止多个内存对象映射到 相同的高速缓存行; -
slab设计 slab将内存对象分为不同的高速缓存组, 每个组作为不同类型的用途, 相同使用类型的申请者 可在对应类型的高速缓存组中申请和释放高速缓存对象. 高速缓存被划分为 slab, 每个slab由一个或多个物理页(成员对象)组成,一般是一个页, 每个高速缓存由多个slab组成. slab三种状态:
(1)满: 没有空闲对象;
(2)部分满: 有部分空闲对象;
(3) 空: 空闲对象均可使用
使用时,一般顺序是: 部分满 , 空, 新创建 struct slab {} 定义: mm/slab.c slab分配器分配slab, 使用kmem_getpages()/kmem_freepages()分配/释放,调用的低级内存分配函数__get_free_pages(). 高速缓存及其内存的slab复杂的管理,完全依赖于内部机制, 尽量避免频繁的分配和释放: (1) 页分配: 当高速缓存中既没有满页咩有空的时候, 调用页分配函数; (2) 页释放 a. 内存紧缺时, 系统释放内存; b. 高速缓存显式撤销时; slab分配器的外部使用接口 : 在kenrel中的各个子系统模块中, 可以看到高速缓存的相关接使用, 比如 fork.c模块, f2fs等: (1) 高速缓存创建/销毁
kmem_cache_create()/kmem_cache_destory()
(2) 从高速缓存中获取/释放内存对象:
kmem_cache_alloc() / kmem_cache_free()
因此, 我们要频繁的使用相同类型的内存对象时, 可以考虑使用 slab高速缓存. -
内核栈 程序执行周期中, 必然会陷入内核, 新的线程创建时, 同时也会创建线程的内核栈, 实现上是通过slab 分配thread_info_cache, thread_info_cache是kmem_cache_create创建. 大小是THREAD_SIZE, 一般是2 page size. 每个内核栈大小, 依赖于体系结构和编译选项. 在kernel2.6版本之前, 内核栈采用的是单页, 主要原因是: (1) 可以减少内存消耗; (2) 随着机器的运行时间的增加, 难以获取到未分配, 连续的页, 物理内存的碎片, 新进程分配虚拟内存的压力也增大; 无论内核页是1页还是2页 , 在编写程序时, 函数的变量使用, 都应该注意节省栈空间, alloca()应该谨慎使用(alloca()可以申请栈空间, 自动释放).
内核对栈的管理有限, 如果栈溢出, 最好的结果是悄无声息的覆盖别的堆栈空间, 最坏的情况是直接宕机. 在kernel4.9版本以前, 栈溢出, 最受考验的是 thread_info, kernel4.9之后, thread_info直接放在task_struct中了.
https://baijiahao.baidu.com/s?id=1712620414089250801&wfr=spider&for=pc
-
高端内存映射 高端内存不能永久映射到内核地址空间, alloc_pages()的__GFP_HIGHMEM标志不可能有虚拟地址对应. (1)永久映射/解除映射 kmap() / kunmap() –> 接口可休眠, 因此只能用在进程上下文中; –> 可映射高端/低端内存, 如果是低端内存, 直接返回虚拟地址即可, 如果是高端内存, 会建立永久映射, 并返回地址. (2)临时映射 kmap_atomic()/ kunmap_atomic() // asm/kmap_types.h –> 当必须 一个映射而当前上下文不能 休眠时, 内核提供了临时映射, 有一组保留的映射, 用于新创建的临时映射, 内核可以原子的高端内存中的一个页映射到一个保留的映射中, 函数不会阻塞, 因此可以用在中断上下文和其他不能重新调度的地方. 实现上也禁止了内核抢占. https://zhuanlan.zhihu.com/p/74218758 -
SMP数据访问(percpu) 对于给定的处理器, 数据访问是唯一的, 如果存在任务抢占或者重新调度, get_cpu()/put_cpu()已经是禁止/激活内核抢占, 此时的数据访问是安全的, 不用在另外去禁止抢占或者激活抢占. linux/percpu.h
mm/slab.c
asm/percpu.h
linux/percpu-defs.h
-
percpu : 新的每个cpu接口 简化了上述操作行为, 简化了创建和操作每个cpu上的数据 -
编译时percpu数据 DEFINE_PER_CPU(type, name) / DECLARE_PER_CPU(type, name)
定义或者释放一个type类型的name变量
get_cpu_var() / put_cpu_var() 可操作变量, 且可以禁止或激活抢占
.data.percpu用于保存变量定义
-
运行时percpu数据 alloc_percpu() / free_percpu() 用于动态申请释放cpu数据, get_cpu_var() / put_cpu_var() 可操作变量, 且可以禁止或激活抢占 -
使用cpu数据的原因和好处 (1) 减少了数据访问锁, percpu本身就能保证当前cpu只访问自己的唯一数据; (2) 大大减少缓存失效, 失效发生在 多个cpu 的缓存同步上, 多cpu间的较多缓存同步, 必将影响性能, 而percpu的数据访问, 可以保证只使用自己的数据, 并且有cache对齐, 将多cpu缓存影响降到最低 (3)唯一需要保证的禁止抢占, 比上锁开销要小; -
分配函数的选择 可以根据实际的需求, 各个分配函数的特性来选择分配函数.
(1) 物理页连续
kmalloc() 及其flag也对应不同的应用场景
(2) 高端内存分配
alloc_pages() --> 得到struct page 结构指针(高端内存不一定映射, 不一定有虚拟地址)
kmap() --> 得到真正的指针
(3) 虚拟地址连续的页
vmalloc() 有一定的性能损失
(4) 创建和撤销 大的数据结构
slab高速缓存
|