第三章 分配内存
内存的管理总体上可以分为两大类:一是对物理内存的管理, 二是对虚拟内存的管理. 前者是用于特定的平台
构架上实际物理内存空间管理, 后者用于特定处理器体系架构上虚拟地址空间的管理.
3.1 物理内存的管理
物理内存定义方面, 内存节点, 内存区域和内存页. 对物理内存的管理总体又可分为两大部分:最底层实现的
是页面级内存管理, 然后是基于页面级管理之上的slab内存管理.
3.1.1 内存节点node
在计算机世界中, 有两种内存管理模型被广泛使用, 它们分别是:
1. UMA(一致内存访问)模型, 该模型的内存空间在物理上也许是不连续的, 但所有的内存空间对系统中的处
理器而言具有相同的访问我, 也即系统中所有的处理器对这些内存的访问具有相同的速度.
2. NUMA(非一致内存访问)模型, 使用这种模型的总是多处理器系统, 每个处理器有自己的本地内存, 处理器
之间通过总线连接起来以支持其它处理器本地内存访问, 处理器访问本地内存快于访问其它处理器的本地内存.
linux中以struct pglist_data数据结构来表示单个内存结点. 对于NUMA模型来说, 多个内存节点通过链表串
联起来, UMA模型因为只有一个内存节点, 因而不存在这样的链表.
??????我们系统中的双核a5访问是UMA吧, 而a5与arm9之间是NUMA
3.1.2 内存区域zone
内存区域属于单个内存节点中的概念, 考虑到系统的各个模块对分配的物理内存有不同的要求, 比如x86要求
DMA只能访问16mb以下的物理地址空间, 因此linux又将每个内存节点管理的物理内存划分成不同的内存区域,
struct zone数据结构表示每一个内存区域, 内存区域的类型用zone_type表示.
3.1.3 内存页
内存页是物理内存管理中的最小单位, 有时也叫页帧, linux为系统物理内存的每个页都创建一个struct page
对象, 系统用一个全局变量struct page *mem_map来存放所有物理页page对象的指针.
页大小取决于系统中的内存管理单元MMU, MMU用来将虚拟空间地址转化为物理空间地址.
3.2 页面分配器(page allocator)
linux系统中对物理内存进行分配的核心建立在页面级的伙伴系统之上, 在系统初始化期间, 伙伴系统负责对
物理内存页面进行跟踪.
每个物理页面都有一个struct page对象与之对应. 根据内存使用及内核虚拟地址空间限制等因素, 内存将物
理内存分为三个区: ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM, 因为mem_map中每个struct page对象与物理页
面间严格的一一对应关系, 使得在mem_map所引导的struct page实例中, 事实上也形成了三个区.
linux初始化期间, 会将虚拟地址空间的物理页面直接映射区作线性地址映射到ZONE_DMA和ZONE_NORMAL, 这意
味首如果页面分配器所分配的页面落在这两个zone中, 那么对应的内核虚拟地址到物理地址的映射的页目录表
项已经建立, 就是所谓的线性映射, 也就是虚拟地址和物理地址之间只有一个差值(PAGE_OFFSET0Xc0000000)
??????页面分配器所分配的内存怎么样才落在这两个zone中, 是kmalloc分配的都在这个区域吗?
而如果页面分配器所分配的页面落在ZONE_HIGHMEM中, 那么内核此时尚没有对该页面进行地址映射, 因此页面
分配器的调用者在这种情况需要做的是, 在内核虚拟地址空间的动态映射区或者固定映射区分配一个虚拟地址
, 然后映射到该物理页面上.
以上是页面分配器的大致工作原理, 接下来讨论分配器提供的接口, 无论是对UMA还是NUMA系统而言, 这些接
口是完全一致的. 内核分配函数的核心成员只有两个, alloc_pages和__get_free_pags, 而这两个接口最终会
用到alloc_pages_node所以两者实现原理完全一样, 只是__get_free_pages不能在高端内存区分配内存, 此外
两者返回的形式有所区别.
3.2.1 gfp_mask
gfp_mask不是页面分配器函数, 而是这些分配函数中一个重要的参数, 是用于控制分配行为的掩码, 并告诉内
核到哪个ZONE中分配物理内存页面.
通常上讲这些以"__"打头的GFP掩码只限于内存管理组件内部使用, 外部的接口以"GFP_"的形式出现.
GFP_ATOMIC: 内核模块中最常使用的掩码之一,用于原子分配, 告诉页分配器, 在分配内存页时, 绝对不能中
断当前进行或者把当前进程移出调度器, 必要情况下可以使用仅限紧急情况使用的保留内存页. 一般在中断处
理例程或者非进程上下文的代码中使用, 因为这两种情况下分配都必须保证当前进程不能睡眠.
如果没有在gfp_mask中明确指定__GFP_DMA/__GFP_HIGHMEM, 默认都是在ZONE_NORMAL中分配物理页, 如果ZONE
_NORMALk中现有空间页不足以满足当前的分配, 那么分配器会从ZONE_DMA域中查找空闲而, 而不会到ZONE_HIG
HMEM中查找.
分配域的优先次序是:
__GFP_HIGHMEM, 先在ZONE_HIGHMEM域中找空间页, 如果无满满足, 分配器退回ZONE_NORMAL域中继续查找
, 如果依然不能找到, 退回到ZONE_DMA域中查找, 有就成功, 没有就失败.
如果没有明确指定__GFP_HIGHMEM/__GFP_DMA默认就是__GFP_NORMAL 优先在ZONE_NORMAL域中分配, 其次
是ZONE_DMA域如果指定了__GFP_DMA, 就只能在ZONE_DMA中分配物理页, 无法满足就直接失败.
设备驱动中最常使用的是GFP_KERNEL与GFP_ATOMIC, 两者中都没有明确指定内存域标训, 所以只能在
ZONE_NORMAL/ZONE_DMA中
3.2.2 alloc_pages
alloc_pages以宏的形式出现, 定义为:
__alloc_pages负责分配2order个连续的物理页并返回起始页的struct page实例. 如果没有指定__GFP_HIGHME
M, 那么分配的物理页面必然来自ZONE_NORMAL/ZONE_DMA, 由于这两个域已经在初始化阶段就建立了映射关系,
所以内核模块可以使用page_address来获得对应页面的内核虚拟地址KVA(kernel virtual address).
如果在调用alloc_pages时在gfp_mask中指定了__GFP_HIGHMEM, 页分配器优先在ZONE_HIGHMEM域中分配物理页
, 但也不排除从其它域中分配(HIGHMEM空间不足), 对于新分配的高端物理页面, 内核尚未在页表中建立映射
关系, 所以此时需要: 1. 在内核的动态映射区分配一个KVA, 2. 通过操作页表, 将1中的KVA映射到该物理页
面上, 内核为此提供了一个函数kmap.
首先, 该函数可能睡眠, 所以不能在中断处理等上下文中, 它用PageHighMem(page)来判断页面是否真的来自
高端内存, 如果不是, 刚用page_address来返回页面所对应的KVA, 否刚将调用kamp_high在内核虚拟地址空间
的动态映射区或者固定映射区分配一个新的KVA并将其映射到物理页面上, 之后将KVA返回给调用者. 因涉及页
表操作, 从高端分配内存开销比较大.
与kmap相反的是kunmap, 函数将页表项中拆除对应的page映射, 同时将来自动态映射区中的KVA释放出去, 这
样该KVA就可以再次被映射到别的页面.
kmap_atomic该函数的执行是原子的, 且比kmap要快.
另一个页面分配函数是alloc_page, 只用于分配一个物理页. 如果系统中没有足够的空闲页, 函数返回NULL,
需做仔细检查.
3.2.3 __get_free_pages
函数负责分配2order个连续的物理页, 返回起始页所在内核线性地址, 函数内部调用alloc_pages负责实际的
页面分配, 从实现中可以看到, __get_free_pages不能从高端内存中分配物理页, VM_BUG_ON宏在CONFIG_DEB
UG_VM定义的情形下可以捕捉到这一错误, 如果CONFIG_DEBUG_VM没有定义, 且调用者在gfp_mask中设置了__G
FP_HIGHMEM, __get_free_pages返回0.
如果内核模块只想分配单个物理页面, 可以使用__get_free_page(gfP-mask).
3.2.4 get_zeroed_page
用于分配一个物理页同时将页面对应在的内容填充为0, 函数返回页面所在的内核线性地址.
3.2.5 __get_dma_pages
用于从ZONE_DMA区域中分配物理页, 返回页面所在线性地址.
alloc_pages ---- gree_pages __get_free_pages ---- __free_pages
3.3 slab分配器(slab allocator)
slab, slob, slub, slab是最早推出的小内存分配方案, slob/slub是linux 2.6开发期间新增的slab分配器
的替代品. slab的原理很简单, 基本思想是, 先利用页面分配器分配出单个或者一组连续的物理页, 然后在此
基础上将整块页分割成多个相等的小内存单元.
3.3.1 管理slab的数据结构
为了管理slab, 内核提供了两个数据结构struct kmem_cache 和struct slab, 这两个数据结构是slab分配器
的基石.
kmem_cache结构中的gfporder指名该kmem_cache中每个slab占用页面数量. gfpflags将影响buddy系统寻找空
闲页时的行为 *name 表明kmem_cache的名字, 出现在/proc/slabinfo中 struct list_head next 将该kmem_
cache加入到cache_chain链表中
?????????slab分配器可以被kmem_cache来代表吗, 还是表明一个slab分配器有多个kmem_cache(如1k,2k,4k,8k),
而单个kmem_cache有多个slab管理的物理内存页, 分别存放于三个链表中, 并非只有三个slab.
????????? 系统中的每个slab分配器, 都需要一个struct kmem_cache实例,也就是kmem_cache代表一个slab分配器.
????????? slab对象管理一块连续的物理页面中内存对象的分配, 如kmem_cache中的gfporder为1, 它会有一系
列的slab.
????????? cache_cache这个东西做了什么用?
kmem_cache用于管理其下所有的struct slab, 它通过三个链表struct list_haed slabs_full, slabs_partial
和slabs_free, 将其下所有的struct slab实例加入链表.
struct slab结构用于管理一块连续的物理页面中内存对象的分配, struct slab结构的实例存放位置有两种:
1. 将struct slab实例放在物理页首页的开始处. 2.放在物理页面的外部. 内核将从性能优化的角度出来来决
定slab实例存放的位置, CFLGS_OFF_SLAB宏用于表示slab对象存放于外部.
系统中的slab分配器并不孤立, 内核通过一个全局双向链表cache_chain将每一个slab分配器链接起来.
.cache_cache
对于每一个slab分配器, 都需要一个kmem_cache实例, 在slab系统尚未完全建立起来时, kmem_cache实例
从哪分配呢,答案是系统在初始化时提供了一个特殊的slab分配器cache_cache, 专用来分配struct kmem_
cache空间.
.size_cache
很多书中把cache_sizes叫做通过cache, 在系统初始化期间, 内核委托kmem_cache_init函数遍历malloc_
sizes数组, 对应每个元素, 都调用kmem_cache_create函数在cache_cache中分配一个struct kmem_cahce
实例, 并将实例所在的地址存在元素的cs_cachep变量中.
3.3.2 kmalloc与kzalloc
kmalloc分配出来的内存是物理上连续的, 但不会清0内容.
函数利用参数size在malloc_sizes数组中查找, 目的是查找到所有大于等于它的数列中的最小值. 找到这样的
一个数组元素后, 也就获得了该元素所对就在slab分配器的kmem_cache对象cachep.
一般都会调用kmem_cache_alloc在cachep领衔的slab分配器中进行内存分配, 如果分配器中没有这样的空闲对
象, 就必须创建,这意味着slab分配器需要利用下层的页面分配器来分配一段新的物理页面, 调用链如下: __
cache_alloc->__do_cache_alloc-> cache_alloc_refill()->cache_grow()->kmem+getpages90->alloc_pages
_exact_node()->__alloc_pages().
如果在调用kmalloc时传入__GFP_HIGHMEM/__GFP_DMA32, 将触发代码中的BUG_ON, 虽然在BUG_ON可能不会触发
kmalloc函数在执行时发生异常, 但这里体同在slab分配器的一个基本设计原则: 底层的而面分配来自低端物
理内存. 在触发BUG_ON后, 会清除掉__GFP_HIGHMEM/__GFP_DMA32, 所以即便内核模块使用了__GFP_HIGHMEM这
样的调用来分配内存, 但函数依然会返回低端物理内存页面对就原线性内核虚拟地址.
如果系统中没有足够多的内存, 分配连续的物理页面会失败, 这时kmalloc函数将返回NULL指针, 所以调用者
要仔细考量.
kzalloc函数是kmalloc设置了__GFP_ZERO情况下的简化版, 等同于kmalloc(size, flags | __GFP_ZERO).
.kfree函数
函数根据要释放内存的指针objp调用virt_to_page函数来获得objp所在页面对象指针*page, virt_to_pa
ge函数的原理是根据objp获得物理页帧号__pa(objp)-->PAGE_SHIFT, 接着取得该页帧号所对应的页对象
指针*page=mem_map+(__pa(objp)>>PAGE_SHIFT). 页对象所在slab分配器的kmem_cache指针*c保留在page
->lru.next.
由于通过objp来获得page的过程可以看出, kfree释放内存只能来自于kmalloc, 后者实际上只使用
ZONE_NORMAL/ZONE_DMA函数最后调用__cahce_free函数在c所对应的slab分配器中释放内存对象.
3.3.3 kmem_cache_create 与 kmem_cache_alloc
提供小内存分配并不是slab分配器唯一用途, 在某此情况下, 有某内核模块可能需要频繁的分配和释放相同的
内核对象. slab分配在这种情况下作为一种内核对象的缓存, 对象在slab中分配, 当释放对象时, slab分配器
并会不将对象占用的空间返回给buddy系统. 再次分配该对象中,可以从slab中直接得到对象的内存, 另外slab
分配代码经过精心而严密的设计, 充分利用了CPU硬件的高速缓存, 大大提高了系统的性能.
生成kmem_cache的函数是kmem_cache_create
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*)).
参数name是一指向字符型的指针, 用来生成kmem_cache的名称, 会导出到/proc/slabinfo. 需要注意的是, 创
建出的kmem_cache对象会用一个指针指向该name, 因此函数的调用者必须确保传入的name指针在kmem_cache的
整个生存期都有效, 否则会出现无效的引用.
参数size用来指定在缓存中分配对象的大小. 参数align用于指定数据对齐时的偏移量.
参数flags用于创建kmem_cache时的标志位掩码, 0表示默认值. 常用的标志位有:
SLAB_HWCACHE_ALIGN: 要求分配器中所有的内存对象跟处理器的高速缓存行(cache line)对齐,将频繁访问的
对象放入到高速缓存行中,会大幅提升内存访问性能, 但是对齐的要求会在对象与对象间造成无用填充, 造成
内存浪费.
SLAB_CACHE_DMA: 在slab分配器调用buddy获得内存页时, 从DMA_ZONE区域获得内存页.
SLAB_PANIC: 在kmem_cache分配失败时将导致系统panic.
最后一个ctor是一个函数指针, 是kmem_cache的构造函数, slab分配一块新页面时, 会对该页面中的每个内存
对象调用此处设定的构造函数.
函数的核心是通过cache_cache来分配kmem_cache对象, 成功将返回指向kmem_cache的指针*cachep, 否则返回
NULL, 新分配的kmem_cache对象最终会被加入到cache_chain所表示的链静听
. kmem_cache_destroy与kmem_cache_free
kmem_cache_destroy负责把kmem_cache_create创建的kmem_cache对象销毁, kmem_cache_free负责把kmem_cac
he_alloc分配的对象释放.
3.4 内存池(mempool)
设备驱动程序对内存池的使用机会已经非常少了, 内存池的思想是: 预先为将来要使用的数据对象分配几个
内存空间, 把这些空间地址存放在内存池对象中, 当代码真正需要分配空间时, 正常调用正常的分配函数
, 如果分配失败, 便可以从内存池中取得预先分配好的地址空间.
3.5 虚拟内存的管理
linux内核将4GB的虚拟地址空间划分为两大部:顶部1GB给内核使用, 称为内核空间, 底部3GB给用户空间使
用, 称为用户空间。 内核代码中用PAGE——OFFSET宏来标示虚拟地址空间中内核部分的起始地址。
3.5.1 内核虚拟地址空间构成
内核将1GB的内核空间分为三个部分:
第1部分位于1GB空间的开头, 用于对系统物理内存直接映射(线性映射),内核用全局变量high_memory表示这
段空间的上界。
.........也就是说high_memory表示系统物理内存的上线界。
第2部分位于中间, 主要用于vmalloc函数, 称之为(VM区)或者(vmalloc区).
第3部分位于1GB空间的结尾, 用于特殊映射。
3.5.2 vmalloc与vfree
vmalloc的特点是分配的虚拟地址空间是连续的, 但这段虚拟地址空间映射的物理地址可能不是连续的。 主
要是针对vmalloc区进行操作.
..........难首kmalloc分配的地址不是虚拟地址连续的?
在驱动程序中不鼓励使用vmalloc函数, 因为vmalloc实现机制决定它的使用效率没有kmalloc高, 其次,物理内
存比较大时,vmalloc区域相对变得很小, 对vmalloc调用失败的可能性增大, 最后vmalloc分本的地址空间物理
上不保证连续, 这对那些要求地址空间连续的设备如DMA造成了麻烦.
vmalloc函数的实现原理简单概括为三大步骤:
1. 在vmalloc区分配出一段连续的虚拟内存区域
2. 通过伙伴系统获得物理页
3. 通过对页表的操作将步骤1中分配的虚拟内存映射到步骤2中获得的物理页上
对于vmalloc区中每一次分配出来的虚拟内存地, 内核用struct vm_struct对象来表示.
next用来把vmalloc区中所有分配的struct vm_struct对象构成链表. 该链表表头为全局变量struct vm_struc
t *vmlist.
addr为对应虚拟内存块的起始地址, 应该是页对齐. size为虚拟内存块的大小, 总是页面大小的整数倍.
flags为表示当前虚拟内存映射的特性. VM_ALLOC表示当前虚拟内存块是给vmalloc函数使用, 映射的是实际
物理内存.
VM_IOREMAP表示当前虚拟内存块是给ioremap盯关函数使用, 映射的是I/O空间地址,也是设备内存
pages是被映射的物理内存页面所形成的数组首地址. nr_pages表示映射的物理页的数量.
phys_addr多在ioremap函数中使用, 表示映射的 I/O空间起始地址,页对齐.
内核总是会把vmalloc函数的参数size调整到页对齐,并在调整后的数值上再加一个页面的大小.内核之所以加
一个页面大小,是为了防止可能出现的越界访问.因为步骤3的页表操作中并不会向这个附加在末尾的虚拟地址
上提交实际物理页面, 所以当有访问进入到这个区间时, 处理器会产生异常.
步骤2中内核在调用伙伴系统获取物理内存页时, 使用了GFP_KERNEL | __GFP_HIGHMEM, GFP_KERNEL意味着
vmalloc函数在执行过程中可能睡眠, 因而不可以处在中断上下文, __GFP_HIGHMEM告诉buddy系统在ZONE_HIG
HMEM区中查找空闲页, 因为NORMAL区中的物理内存资源非常宝贵, 主要给kmalloc这类接口获得物理连续页.
因页vmalloc函数尽量用高端物理内存页. 此外在分配物理页时使用alloc_page/order=0的alloc_pages_node
函数, 每次只分配单个物理页, 以用来不保证物理内存空间上的连续.
步骤3没有特别需要注意的地方, 唯一的一点是不对步骤1中内存区域的末尾4KB大小部分映射.
3.5.3 ioremap
void __iomem *ioremap(unsigned long phys_addr, size_t size)
__iomem的作是只是提醒调用者返回的是io类型的地址
ioremap函数及其变种用来将vmalloc区的某段虚拟内存映射到I/O空间, 其实现原理与vmalloc
函数基本上完全一样, 都是通过vmalloc区分配虚拟地址地, 然后修改内核页表将映射到设备的
内存区,唯一不同是ioremap并不需要通过伙伴系统去分配物理页, ioremap映射的目标是I/O空间.
ioremap_nocache是通过清除页表项中的C(ache)标志, 使处理器访问这段地址时不会被cache.
3.6 per-CPU变量
per-cpu变量是Linux内核中一个非常有趣的我, 它为系统中的每个处理器都分配了该变量的一个
副本, 好处是在多处理器上, 当处理器操作属于它的变量副本时, 不需要考虑与其它处理器竞争
的问题,同时该副本还可以充分利用处理器本地的硬件缓存以提高访问速度.
per-cpu按照存储变量空间的来源可分为静态per-cpu和动态per-cpu: 前者的存储空间是代码编
译时静态分配的, 后者的存储空间是代码的执行期间动态分配的.
3.6.1 静态per-cpu变量的声明与定义
声名per-cpu变量的方法是使用DECLARE_PER_CPU宏.
DECLARE_PER_CPU(int, dolphin);
该宏在源码中声明一个变量int dolphin, 并放在名".data..percpu"的section中.
定义要用宏DEFINE_PER_CPU. 以上看来只是将per-cpu变量放在了.data..percpu段中
3.6.2 静态per-cpu变量的链接脚本
链接器会把所有静态定义的per-cpu变量统一放到".data..percpu" section中, 链接器生成
__per_cpu_start和__per_cpu_end两个变量来表示该section的起始和结束地址, 为了配合链
接器的行为, linux内核源码中针对以上链接脚本声明了如下外部变量.
extern char __per_cpu_load[], __per_cpu_start[], __per_cpu_end[];
3.6.3 setup_per_cpu_areas函数
.静态per-cpu变量副本的产生
setup_per_cpu_areas函数首先计算出".data..percpu"section的空间大小, 正是利用上节链接脚
本中的内容, static_size是内核源码中所有用DEFINE_PER_CPU及其变体定义出的静态per-cpu变量
所占空间的大小,此外还为模块使用的per-CPU变量以及动态分配的per-CPU变量预留了空间.大小分
别记为reserved_size和dyn_size.
然后setup_per_cpu_areas函数调用alloc_bootmem_nopanic分配一段内存, 用来留存per-cpu变量
副本, 由于此时内存管理系统还没有建立, 所以linux使用引导期内存分配器, 这块内存的大小
依赖于系统中cpu的数量. 内核代码称每个cpu变量副本所在内存空间为一个unit, 代码中nr_units
实际上表示了系统中cpu的数量, 每个unit的大小记为unit_size, 指针pcpu_base_addr指向副本
空间的起始地址.
容纳副本的空间有了 ,就要把内核映射".data.percpu"section中的变量数据复制到pcpu_base_addr
空间.
至此, 系统针对DEFINE_PER_CPU定义的变量已经为每个cpu产生了一个副本, 接下的问题是如何使用
这些变量的副本.
.动态per-cpu变量副本的产生
1. alloc_percpu
alloc_percpu底层调用了__alloc_percpu, 使用默认对齐参数__alignof__(type), 第一部分,为系统
中的每个cpu分配副本的空间,第二部分,通过某种机制实现对cpu特定的副本空间的访问.
chunk作为一种存放管理数据的容器存在, 根据其上空闲空间的大小而在一个pcpu_slot数组所表示的
链表中进行迁移, 数组的索引i指明了其链表中chunk空闲空间的大小, 当需要动态分配一个per-cpu
变量时, 内核在pcpu_slot数组中查找有没chunk空闲空间满足需要, 如果有, 就在此chunk的空闲空间
为系统的每个cpu生成变量的副本空间, 如果没有,就重新创建一个新的chunk, 新分配的chunk会在内核
虚拟地址空间的vmalloc区为它分配副本空间, 空间的起以地址保存在chunk的base_addr成员中. chunk
用一个整形数组map来跟踪副本空间的分配情况, 当要分配一个动态per-cpu变量时, 就在副本空间查
找空闲区域, 找到之后为每个cpu都分配出存储该per-cpou变量的存储小块. 此时存储per-cpu变量的
空间还在vmalloc区, 如果之前该存储小块上还没有映射物理页面的话, 需要为新分配变量映射新的页
面, 物理页通过页分配器从伙伴系统获得, chunk通过成员变量populated来跟踪物理页面的提交.
???????chunk, 管理单元, 每个chunk分配多大的存储空间呢? 如何知道它满了而需要创建新的空间, populated
如何跟踪物理页面的提交.
3.6.4 使用per-cpu变量
__per_cpu_offset是用来实现处理器副本访问的基础, 每个处理器副本所在空间的偏移地址都由
__per_cpu_offset引出, 这是个全局数组变量:
unsigned long __per_cpu_offset[NR_CPUS]__read_mostly;
它的初妈化出现在setup_per_cpu_areas中, 内核启动阶段初始化, 函数首先诈出副本空间首地址(pcpu_
base_addr)与".data.percpu"section首地址(__per_cpu_start)之间的偏移量delta. pcpu_unit_offset
s[cpu]保存对应cpu所在副本空间相对于pcpu_base_addr的偏移量, 这样就可以得到per-cpu变量副本的
偏移量, 放在__per_cpu_offset数组中.
其中preempt_disable()用来关闭内核可抢占性, 关闭内核可抢占性可确保在对per-cpu变量操作的临界区
中, 当前进程不会被换出处理器, 在put_cpu_var中恢复内核调度器的可抢占性.
per_cpu(var, cpu)用来读取其它处理器中的副本.为了安全起见,per_cpu_ptr结构get_cpu和put_cpu配对使用.
页分本器提供的接口总体上有两类, 一类是以alloc_pages函数领衔, 这类函数可以工作在三个内存域
ZONE_HIGHMEM, ZONE_NORMAL, ZONE_DMA, 具体在哪个区分配, 由gfp_mask参数指定,如果所指定区没有
足够页, 函数会自动到下一级区域分配, 但不会进入上级区域.
ZONE_NORMAL, ZONE_DMA在系统起动过程中, 被一一线性映射, 意味着物理内存地址与虚拟地址之间只存
在一个常量差值(PAGE_OFFSET). 所以页面分配器在内部实现上无法去更改这段映射区所在的内核页目录
表. 所以alloc_pages返回的页面位于低端物理内存, 那么通过page_address函数可以将返回的struct
page对象指针转变成内核线性地址. 如果alloc_pages返回的页位于高端内存, 调用者就需要用kmap等
函数对返回的struct page对象建立映射关系, 如果成功完成映射, 就将返回一个内核虚拟地址, 该地址
位于内核虚拟地址空间中的动态映射区. alloc_pages的默认行为是, 如果没有指定GFP_HIGHMEM/GFP_DMA
, 就会从GFP_NORMAL中分配.
另一组页面分配器以__get_free_pages函数领衔, 它们通过alloc_pages来分配物理页面,不是的是__get_
free_pages函数只能在低端内存区域中分配物理页, 函数返回的是内核线性地址, 而不是struct page指针
其次是基于slab分配器的kmalloc函数和kmem_cache_alloc函数. slab分配器是工作在页分配器基础之上
的, 但是它只能低端物理内存区中分配页面, kmalloc函数在内部的实现上基于size_cache, 这是一组
kmem_cache所构成的缓存, 每个缓存对应特定大小的内存对象. kmem_cache_alloc函数实际上是对size_cache
的一种扩展, 它可以对kmem_cache中的内存对象大小进行定制.
接下来是vmalloc, 这个函数工作在内核虚拟空间VMALLOC_START和VMALLOC_END所表示的vmalloc区. 该
虚拟空间对物理内存的映射不是一一对应的, 虚拟上连续, 物理上不一定连续. 它映射的物理内存优先从
高端内存区分配, 所以它的使用场景是, 需要分配一大段内存, 而kmalloc可能失败时, 可以使用vmalloc
在内核虚拟地址空间vmalloc区分配一段连续的虚拟地址空间. 另外, vmalloc不能保证原子性, 因此不能在
进程上下文中使用, kmalloc函数可以通过GFP_ATOMIC标志达成原子性, vmalloc在分配过程中因为涉及对
页表项的操作, 这种操作常常要重建TLB, 会导致对vmalloc返回地址进行访问时带来较大系统开销.
最后一个内存分配函数主要用于多处理器系统中, per-cpu内存分本器, 它的思想是通过为系统中每个处
理器都分配一个cpu特定的变量副本, 来减少多处理器并发访问时的锁定操作, 以达到提高系统性能的目的.
内存的管理总体上可以分为两大类:一是对物理内存的管理, 二是对虚拟内存的管理. 前者是用于特定的平台
构架上实际物理内存空间管理, 后者用于特定处理器体系架构上虚拟地址空间的管理.
本文欢迎转载
本文出处:blog.csdn.net/dyron
3.1 物理内存的管理
物理内存定义方面, 内存节点, 内存区域和内存页. 对物理内存的管理总体又可分为两大部分:最底层实现的
是页面级内存管理, 然后是基于页面级管理之上的slab内存管理.
3.1.1 内存节点node
在计算机世界中, 有两种内存管理模型被广泛使用, 它们分别是:
1. UMA(一致内存访问)模型, 该模型的内存空间在物理上也许是不连续的, 但所有的内存空间对系统中的处
理器而言具有相同的访问我, 也即系统中所有的处理器对这些内存的访问具有相同的速度.
2. NUMA(非一致内存访问)模型, 使用这种模型的总是多处理器系统, 每个处理器有自己的本地内存, 处理器
之间通过总线连接起来以支持其它处理器本地内存访问, 处理器访问本地内存快于访问其它处理器的本地内存.
linux中以struct pglist_data数据结构来表示单个内存结点. 对于NUMA模型来说, 多个内存节点通过链表串
联起来, UMA模型因为只有一个内存节点, 因而不存在这样的链表.
??????我们系统中的双核a5访问是UMA吧, 而a5与arm9之间是NUMA
3.1.2 内存区域zone
内存区域属于单个内存节点中的概念, 考虑到系统的各个模块对分配的物理内存有不同的要求, 比如x86要求
DMA只能访问16mb以下的物理地址空间, 因此linux又将每个内存节点管理的物理内存划分成不同的内存区域,
struct zone数据结构表示每一个内存区域, 内存区域的类型用zone_type表示.
3.1.3 内存页
内存页是物理内存管理中的最小单位, 有时也叫页帧, linux为系统物理内存的每个页都创建一个struct page
对象, 系统用一个全局变量struct page *mem_map来存放所有物理页page对象的指针.
页大小取决于系统中的内存管理单元MMU, MMU用来将虚拟空间地址转化为物理空间地址.
3.2 页面分配器(page allocator)
linux系统中对物理内存进行分配的核心建立在页面级的伙伴系统之上, 在系统初始化期间, 伙伴系统负责对
物理内存页面进行跟踪.
每个物理页面都有一个struct page对象与之对应. 根据内存使用及内核虚拟地址空间限制等因素, 内存将物
理内存分为三个区: ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM, 因为mem_map中每个struct page对象与物理页
面间严格的一一对应关系, 使得在mem_map所引导的struct page实例中, 事实上也形成了三个区.
linux初始化期间, 会将虚拟地址空间的物理页面直接映射区作线性地址映射到ZONE_DMA和ZONE_NORMAL, 这意
味首如果页面分配器所分配的页面落在这两个zone中, 那么对应的内核虚拟地址到物理地址的映射的页目录表
项已经建立, 就是所谓的线性映射, 也就是虚拟地址和物理地址之间只有一个差值(PAGE_OFFSET0Xc0000000)
??????页面分配器所分配的内存怎么样才落在这两个zone中, 是kmalloc分配的都在这个区域吗?
而如果页面分配器所分配的页面落在ZONE_HIGHMEM中, 那么内核此时尚没有对该页面进行地址映射, 因此页面
分配器的调用者在这种情况需要做的是, 在内核虚拟地址空间的动态映射区或者固定映射区分配一个虚拟地址
, 然后映射到该物理页面上.
以上是页面分配器的大致工作原理, 接下来讨论分配器提供的接口, 无论是对UMA还是NUMA系统而言, 这些接
口是完全一致的. 内核分配函数的核心成员只有两个, alloc_pages和__get_free_pags, 而这两个接口最终会
用到alloc_pages_node所以两者实现原理完全一样, 只是__get_free_pages不能在高端内存区分配内存, 此外
两者返回的形式有所区别.
3.2.1 gfp_mask
gfp_mask不是页面分配器函数, 而是这些分配函数中一个重要的参数, 是用于控制分配行为的掩码, 并告诉内
核到哪个ZONE中分配物理内存页面.
#define ___GFP_DMA 0x01u //在ZONE_DMA标识的内存区域中查找空闲页
#define ___GFP_HIGHMEM 0x02u //在ZONE_HIGHMEM标识的内存区域中中查找空间页
#define ___GFP_DMA32 0x04u //在ZONE_DMA32标识的内存区域中查找空间页
#define ___GFP_MOVABLE 0x08u //内核将分配的物理页标记为可移动的.
#define ___GFP_WAIT 0x10u //当前下在向内核申请页的进程可以被阻塞, 调度器在此期间
可以调度其它进程
#define ___GFP_HIGH 0x20u //内核允许使用紧急分配链表中的保留内存器, 请求以原子方
式完成, 不可中断
#define ___GFP_IO 0x40u //内核在查找空闲页的过程中可以进行I/O操作, 如将换出的
页写到硬盘.
#define ___GFP_FS 0x80u //查找空间页的过程中允许执行文件系统相关操作.
#define ___GFP_COLD 0x100u //从非缓页的"冷页"中分配
#define ___GFP_NOWARN 0x200u //禁止分配失败时告警
#define ___GFP_REPEAT 0x400u //如果分配失败, 可以自动尝试再次分配, 若干次后终止
#define ___GFP_NOFAIL 0x800u //分配失败后一直重试, 直到成功为止(2.6.39版本内核中注
释将来不再使用)
#define ___GFP_NORETRY 0x1000u //如果分配失败, 不会进行重试操作.
#define ___GFP_COMP 0x4000u //增加复合页元数据
#define ___GFP_ZERO 0x8000u //用0填充分配出来的物理页
#define ___GFP_NOMEMALLOC 0x10000u //不要使用仅限紧急分配使用的保留分配链表
#define ___GFP_HARDWALL 0x20000u //只能在当前进程允许运行的各个CPU所关系的节点分配内存.
#define ___GFP_THISNODE 0x40000u
#define ___GFP_RECLAIMABLE 0x80000u
#ifdef CONFIG_KMEMCHECK
#define ___GFP_NOTRACK 0x200000u
#else
#define ___GFP_NOTRACK 0
#endif
#define ___GFP_NO_KSWAPD 0x400000u
#define ___GFP_OTHER_NODE 0x800000u
通常上讲这些以"__"打头的GFP掩码只限于内存管理组件内部使用, 外部的接口以"GFP_"的形式出现.
#define GFP_ATOMIC (__GFP_HIGH)
#define GFP_NOIO (__GFP_WAIT)
#define GFP_NOFS (__GFP_WAIT | __GFP_IO)
#define GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS)
#define GFP_TEMPORARY (__GFP_WAIT | __GFP_IO | __GFP_FS | \
__GFP_RECLAIMABLE)
#define GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \
__GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE (__GFP_WAIT | __GFP_IO | __GFP_FS | \
__GFP_HARDWALL | __GFP_HIGHMEM | \
__GFP_MOVABLE)
#define GFP_IOFS (__GFP_IO | __GFP_FS)
#define GFP_TRANSHUGE (GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
__GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN | \
__GFP_NO_KSWAPD)
GFP_ATOMIC: 内核模块中最常使用的掩码之一,用于原子分配, 告诉页分配器, 在分配内存页时, 绝对不能中
断当前进行或者把当前进程移出调度器, 必要情况下可以使用仅限紧急情况使用的保留内存页. 一般在中断处
理例程或者非进程上下文的代码中使用, 因为这两种情况下分配都必须保证当前进程不能睡眠.
GFP_KERNEL: 内核模块中最常使用的掩码之一, 带有该掩码的内存分配可能导致当前进程进入睡眠.
GFP_USER: 用于为用户空间分配内存页, 可以引进进程休眠.
GFP_NOIO/GFP_NOFS: 都带有__GFP_WAIT, 可以被中断, 前者在分配时禁止I/O操作, 后者禁止文件系统相关的函数调用
GFP_HIGHUSER: 对GFP_USER的一个扩展, 可以使用非线性映射的高端内存.
GFP_DMA: 限制页面分配器只能在ZONE_DMA域中分配空闲物理页面, 用于分配适用于DMA缓冲区的内存.
如果没有在gfp_mask中明确指定__GFP_DMA/__GFP_HIGHMEM, 默认都是在ZONE_NORMAL中分配物理页, 如果ZONE
_NORMALk中现有空间页不足以满足当前的分配, 那么分配器会从ZONE_DMA域中查找空闲而, 而不会到ZONE_HIG
HMEM中查找.
分配域的优先次序是:
__GFP_HIGHMEM, 先在ZONE_HIGHMEM域中找空间页, 如果无满满足, 分配器退回ZONE_NORMAL域中继续查找
, 如果依然不能找到, 退回到ZONE_DMA域中查找, 有就成功, 没有就失败.
如果没有明确指定__GFP_HIGHMEM/__GFP_DMA默认就是__GFP_NORMAL 优先在ZONE_NORMAL域中分配, 其次
是ZONE_DMA域如果指定了__GFP_DMA, 就只能在ZONE_DMA中分配物理页, 无法满足就直接失败.
设备驱动中最常使用的是GFP_KERNEL与GFP_ATOMIC, 两者中都没有明确指定内存域标训, 所以只能在
ZONE_NORMAL/ZONE_DMA中
3.2.2 alloc_pages
alloc_pages以宏的形式出现, 定义为:
#define alloc_pages(gfp_mask, order) \
alloc_pages_node(numa_node_id(), gfp_mask, order)
__alloc_pages负责分配2order个连续的物理页并返回起始页的struct page实例. 如果没有指定__GFP_HIGHME
M, 那么分配的物理页面必然来自ZONE_NORMAL/ZONE_DMA, 由于这两个域已经在初始化阶段就建立了映射关系,
所以内核模块可以使用page_address来获得对应页面的内核虚拟地址KVA(kernel virtual address).
如果在调用alloc_pages时在gfp_mask中指定了__GFP_HIGHMEM, 页分配器优先在ZONE_HIGHMEM域中分配物理页
, 但也不排除从其它域中分配(HIGHMEM空间不足), 对于新分配的高端物理页面, 内核尚未在页表中建立映射
关系, 所以此时需要: 1. 在内核的动态映射区分配一个KVA, 2. 通过操作页表, 将1中的KVA映射到该物理页
面上, 内核为此提供了一个函数kmap.
首先, 该函数可能睡眠, 所以不能在中断处理等上下文中, 它用PageHighMem(page)来判断页面是否真的来自
高端内存, 如果不是, 刚用page_address来返回页面所对应的KVA, 否刚将调用kamp_high在内核虚拟地址空间
的动态映射区或者固定映射区分配一个新的KVA并将其映射到物理页面上, 之后将KVA返回给调用者. 因涉及页
表操作, 从高端分配内存开销比较大.
与kmap相反的是kunmap, 函数将页表项中拆除对应的page映射, 同时将来自动态映射区中的KVA释放出去, 这
样该KVA就可以再次被映射到别的页面.
kmap_atomic该函数的执行是原子的, 且比kmap要快.
另一个页面分配函数是alloc_page, 只用于分配一个物理页. 如果系统中没有足够的空闲页, 函数返回NULL,
需做仔细检查.
3.2.3 __get_free_pages
函数负责分配2order个连续的物理页, 返回起始页所在内核线性地址, 函数内部调用alloc_pages负责实际的
页面分配, 从实现中可以看到, __get_free_pages不能从高端内存中分配物理页, VM_BUG_ON宏在CONFIG_DEB
UG_VM定义的情形下可以捕捉到这一错误, 如果CONFIG_DEBUG_VM没有定义, 且调用者在gfp_mask中设置了__G
FP_HIGHMEM, __get_free_pages返回0.
如果内核模块只想分配单个物理页面, 可以使用__get_free_page(gfP-mask).
3.2.4 get_zeroed_page
用于分配一个物理页同时将页面对应在的内容填充为0, 函数返回页面所在的内核线性地址.
3.2.5 __get_dma_pages
用于从ZONE_DMA区域中分配物理页, 返回页面所在线性地址.
alloc_pages ---- gree_pages __get_free_pages ---- __free_pages
3.3 slab分配器(slab allocator)
slab, slob, slub, slab是最早推出的小内存分配方案, slob/slub是linux 2.6开发期间新增的slab分配器
的替代品. slab的原理很简单, 基本思想是, 先利用页面分配器分配出单个或者一组连续的物理页, 然后在此
基础上将整块页分割成多个相等的小内存单元.
3.3.1 管理slab的数据结构
为了管理slab, 内核提供了两个数据结构struct kmem_cache 和struct slab, 这两个数据结构是slab分配器
的基石.
kmem_cache结构中的gfporder指名该kmem_cache中每个slab占用页面数量. gfpflags将影响buddy系统寻找空
闲页时的行为 *name 表明kmem_cache的名字, 出现在/proc/slabinfo中 struct list_head next 将该kmem_
cache加入到cache_chain链表中
?????????slab分配器可以被kmem_cache来代表吗, 还是表明一个slab分配器有多个kmem_cache(如1k,2k,4k,8k),
而单个kmem_cache有多个slab管理的物理内存页, 分别存放于三个链表中, 并非只有三个slab.
????????? 系统中的每个slab分配器, 都需要一个struct kmem_cache实例,也就是kmem_cache代表一个slab分配器.
????????? slab对象管理一块连续的物理页面中内存对象的分配, 如kmem_cache中的gfporder为1, 它会有一系
列的slab.
????????? cache_cache这个东西做了什么用?
kmem_cache用于管理其下所有的struct slab, 它通过三个链表struct list_haed slabs_full, slabs_partial
和slabs_free, 将其下所有的struct slab实例加入链表.
struct slab结构用于管理一块连续的物理页面中内存对象的分配, struct slab结构的实例存放位置有两种:
1. 将struct slab实例放在物理页首页的开始处. 2.放在物理页面的外部. 内核将从性能优化的角度出来来决
定slab实例存放的位置, CFLGS_OFF_SLAB宏用于表示slab对象存放于外部.
系统中的slab分配器并不孤立, 内核通过一个全局双向链表cache_chain将每一个slab分配器链接起来.
.cache_cache
对于每一个slab分配器, 都需要一个kmem_cache实例, 在slab系统尚未完全建立起来时, kmem_cache实例
从哪分配呢,答案是系统在初始化时提供了一个特殊的slab分配器cache_cache, 专用来分配struct kmem_
cache空间.
.size_cache
很多书中把cache_sizes叫做通过cache, 在系统初始化期间, 内核委托kmem_cache_init函数遍历malloc_
sizes数组, 对应每个元素, 都调用kmem_cache_create函数在cache_cache中分配一个struct kmem_cahce
实例, 并将实例所在的地址存在元素的cs_cachep变量中.
3.3.2 kmalloc与kzalloc
kmalloc分配出来的内存是物理上连续的, 但不会清0内容.
函数利用参数size在malloc_sizes数组中查找, 目的是查找到所有大于等于它的数列中的最小值. 找到这样的
一个数组元素后, 也就获得了该元素所对就在slab分配器的kmem_cache对象cachep.
一般都会调用kmem_cache_alloc在cachep领衔的slab分配器中进行内存分配, 如果分配器中没有这样的空闲对
象, 就必须创建,这意味着slab分配器需要利用下层的页面分配器来分配一段新的物理页面, 调用链如下: __
cache_alloc->__do_cache_alloc-> cache_alloc_refill()->cache_grow()->kmem+getpages90->alloc_pages
_exact_node()->__alloc_pages().
如果在调用kmalloc时传入__GFP_HIGHMEM/__GFP_DMA32, 将触发代码中的BUG_ON, 虽然在BUG_ON可能不会触发
kmalloc函数在执行时发生异常, 但这里体同在slab分配器的一个基本设计原则: 底层的而面分配来自低端物
理内存. 在触发BUG_ON后, 会清除掉__GFP_HIGHMEM/__GFP_DMA32, 所以即便内核模块使用了__GFP_HIGHMEM这
样的调用来分配内存, 但函数依然会返回低端物理内存页面对就原线性内核虚拟地址.
如果系统中没有足够多的内存, 分配连续的物理页面会失败, 这时kmalloc函数将返回NULL指针, 所以调用者
要仔细考量.
kzalloc函数是kmalloc设置了__GFP_ZERO情况下的简化版, 等同于kmalloc(size, flags | __GFP_ZERO).
.kfree函数
函数根据要释放内存的指针objp调用virt_to_page函数来获得objp所在页面对象指针*page, virt_to_pa
ge函数的原理是根据objp获得物理页帧号__pa(objp)-->PAGE_SHIFT, 接着取得该页帧号所对应的页对象
指针*page=mem_map+(__pa(objp)>>PAGE_SHIFT). 页对象所在slab分配器的kmem_cache指针*c保留在page
->lru.next.
由于通过objp来获得page的过程可以看出, kfree释放内存只能来自于kmalloc, 后者实际上只使用
ZONE_NORMAL/ZONE_DMA函数最后调用__cahce_free函数在c所对应的slab分配器中释放内存对象.
3.3.3 kmem_cache_create 与 kmem_cache_alloc
提供小内存分配并不是slab分配器唯一用途, 在某此情况下, 有某内核模块可能需要频繁的分配和释放相同的
内核对象. slab分配在这种情况下作为一种内核对象的缓存, 对象在slab中分配, 当释放对象时, slab分配器
并会不将对象占用的空间返回给buddy系统. 再次分配该对象中,可以从slab中直接得到对象的内存, 另外slab
分配代码经过精心而严密的设计, 充分利用了CPU硬件的高速缓存, 大大提高了系统的性能.
生成kmem_cache的函数是kmem_cache_create
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*)).
参数name是一指向字符型的指针, 用来生成kmem_cache的名称, 会导出到/proc/slabinfo. 需要注意的是, 创
建出的kmem_cache对象会用一个指针指向该name, 因此函数的调用者必须确保传入的name指针在kmem_cache的
整个生存期都有效, 否则会出现无效的引用.
参数size用来指定在缓存中分配对象的大小. 参数align用于指定数据对齐时的偏移量.
参数flags用于创建kmem_cache时的标志位掩码, 0表示默认值. 常用的标志位有:
SLAB_HWCACHE_ALIGN: 要求分配器中所有的内存对象跟处理器的高速缓存行(cache line)对齐,将频繁访问的
对象放入到高速缓存行中,会大幅提升内存访问性能, 但是对齐的要求会在对象与对象间造成无用填充, 造成
内存浪费.
SLAB_CACHE_DMA: 在slab分配器调用buddy获得内存页时, 从DMA_ZONE区域获得内存页.
SLAB_PANIC: 在kmem_cache分配失败时将导致系统panic.
最后一个ctor是一个函数指针, 是kmem_cache的构造函数, slab分配一块新页面时, 会对该页面中的每个内存
对象调用此处设定的构造函数.
函数的核心是通过cache_cache来分配kmem_cache对象, 成功将返回指向kmem_cache的指针*cachep, 否则返回
NULL, 新分配的kmem_cache对象最终会被加入到cache_chain所表示的链静听
. kmem_cache_destroy与kmem_cache_free
kmem_cache_destroy负责把kmem_cache_create创建的kmem_cache对象销毁, kmem_cache_free负责把kmem_cac
he_alloc分配的对象释放.
3.4 内存池(mempool)
设备驱动程序对内存池的使用机会已经非常少了, 内存池的思想是: 预先为将来要使用的数据对象分配几个
内存空间, 把这些空间地址存放在内存池对象中, 当代码真正需要分配空间时, 正常调用正常的分配函数
, 如果分配失败, 便可以从内存池中取得预先分配好的地址空间.
3.5 虚拟内存的管理
linux内核将4GB的虚拟地址空间划分为两大部:顶部1GB给内核使用, 称为内核空间, 底部3GB给用户空间使
用, 称为用户空间。 内核代码中用PAGE——OFFSET宏来标示虚拟地址空间中内核部分的起始地址。
3.5.1 内核虚拟地址空间构成
内核将1GB的内核空间分为三个部分:
第1部分位于1GB空间的开头, 用于对系统物理内存直接映射(线性映射),内核用全局变量high_memory表示这
段空间的上界。
.........也就是说high_memory表示系统物理内存的上线界。
第2部分位于中间, 主要用于vmalloc函数, 称之为(VM区)或者(vmalloc区).
第3部分位于1GB空间的结尾, 用于特殊映射。
3.5.2 vmalloc与vfree
vmalloc的特点是分配的虚拟地址空间是连续的, 但这段虚拟地址空间映射的物理地址可能不是连续的。 主
要是针对vmalloc区进行操作.
..........难首kmalloc分配的地址不是虚拟地址连续的?
在驱动程序中不鼓励使用vmalloc函数, 因为vmalloc实现机制决定它的使用效率没有kmalloc高, 其次,物理内
存比较大时,vmalloc区域相对变得很小, 对vmalloc调用失败的可能性增大, 最后vmalloc分本的地址空间物理
上不保证连续, 这对那些要求地址空间连续的设备如DMA造成了麻烦.
vmalloc函数的实现原理简单概括为三大步骤:
1. 在vmalloc区分配出一段连续的虚拟内存区域
2. 通过伙伴系统获得物理页
3. 通过对页表的操作将步骤1中分配的虚拟内存映射到步骤2中获得的物理页上
对于vmalloc区中每一次分配出来的虚拟内存地, 内核用struct vm_struct对象来表示.
struct vm_struct {
struct vm_struct *next;
void *addr;
unsigned long size;
unsigned long flags;
struct page **pages;
unsigned int nr_pages;
phys_addr_t phys_addr;
void *caller;
};
next用来把vmalloc区中所有分配的struct vm_struct对象构成链表. 该链表表头为全局变量struct vm_struc
t *vmlist.
addr为对应虚拟内存块的起始地址, 应该是页对齐. size为虚拟内存块的大小, 总是页面大小的整数倍.
flags为表示当前虚拟内存映射的特性. VM_ALLOC表示当前虚拟内存块是给vmalloc函数使用, 映射的是实际
物理内存.
VM_IOREMAP表示当前虚拟内存块是给ioremap盯关函数使用, 映射的是I/O空间地址,也是设备内存
pages是被映射的物理内存页面所形成的数组首地址. nr_pages表示映射的物理页的数量.
phys_addr多在ioremap函数中使用, 表示映射的 I/O空间起始地址,页对齐.
内核总是会把vmalloc函数的参数size调整到页对齐,并在调整后的数值上再加一个页面的大小.内核之所以加
一个页面大小,是为了防止可能出现的越界访问.因为步骤3的页表操作中并不会向这个附加在末尾的虚拟地址
上提交实际物理页面, 所以当有访问进入到这个区间时, 处理器会产生异常.
步骤2中内核在调用伙伴系统获取物理内存页时, 使用了GFP_KERNEL | __GFP_HIGHMEM, GFP_KERNEL意味着
vmalloc函数在执行过程中可能睡眠, 因而不可以处在中断上下文, __GFP_HIGHMEM告诉buddy系统在ZONE_HIG
HMEM区中查找空闲页, 因为NORMAL区中的物理内存资源非常宝贵, 主要给kmalloc这类接口获得物理连续页.
因页vmalloc函数尽量用高端物理内存页. 此外在分配物理页时使用alloc_page/order=0的alloc_pages_node
函数, 每次只分配单个物理页, 以用来不保证物理内存空间上的连续.
步骤3没有特别需要注意的地方, 唯一的一点是不对步骤1中内存区域的末尾4KB大小部分映射.
3.5.3 ioremap
void __iomem *ioremap(unsigned long phys_addr, size_t size)
__iomem的作是只是提醒调用者返回的是io类型的地址
ioremap函数及其变种用来将vmalloc区的某段虚拟内存映射到I/O空间, 其实现原理与vmalloc
函数基本上完全一样, 都是通过vmalloc区分配虚拟地址地, 然后修改内核页表将映射到设备的
内存区,唯一不同是ioremap并不需要通过伙伴系统去分配物理页, ioremap映射的目标是I/O空间.
ioremap_nocache是通过清除页表项中的C(ache)标志, 使处理器访问这段地址时不会被cache.
3.6 per-CPU变量
per-cpu变量是Linux内核中一个非常有趣的我, 它为系统中的每个处理器都分配了该变量的一个
副本, 好处是在多处理器上, 当处理器操作属于它的变量副本时, 不需要考虑与其它处理器竞争
的问题,同时该副本还可以充分利用处理器本地的硬件缓存以提高访问速度.
per-cpu按照存储变量空间的来源可分为静态per-cpu和动态per-cpu: 前者的存储空间是代码编
译时静态分配的, 后者的存储空间是代码的执行期间动态分配的.
3.6.1 静态per-cpu变量的声明与定义
声名per-cpu变量的方法是使用DECLARE_PER_CPU宏.
DECLARE_PER_CPU(int, dolphin);
该宏在源码中声明一个变量int dolphin, 并放在名".data..percpu"的section中.
定义要用宏DEFINE_PER_CPU. 以上看来只是将per-cpu变量放在了.data..percpu段中
3.6.2 静态per-cpu变量的链接脚本
链接器会把所有静态定义的per-cpu变量统一放到".data..percpu" section中, 链接器生成
__per_cpu_start和__per_cpu_end两个变量来表示该section的起始和结束地址, 为了配合链
接器的行为, linux内核源码中针对以上链接脚本声明了如下外部变量.
extern char __per_cpu_load[], __per_cpu_start[], __per_cpu_end[];
3.6.3 setup_per_cpu_areas函数
.静态per-cpu变量副本的产生
setup_per_cpu_areas函数首先计算出".data..percpu"section的空间大小, 正是利用上节链接脚
本中的内容, static_size是内核源码中所有用DEFINE_PER_CPU及其变体定义出的静态per-cpu变量
所占空间的大小,此外还为模块使用的per-CPU变量以及动态分配的per-CPU变量预留了空间.大小分
别记为reserved_size和dyn_size.
然后setup_per_cpu_areas函数调用alloc_bootmem_nopanic分配一段内存, 用来留存per-cpu变量
副本, 由于此时内存管理系统还没有建立, 所以linux使用引导期内存分配器, 这块内存的大小
依赖于系统中cpu的数量. 内核代码称每个cpu变量副本所在内存空间为一个unit, 代码中nr_units
实际上表示了系统中cpu的数量, 每个unit的大小记为unit_size, 指针pcpu_base_addr指向副本
空间的起始地址.
容纳副本的空间有了 ,就要把内核映射".data.percpu"section中的变量数据复制到pcpu_base_addr
空间.
至此, 系统针对DEFINE_PER_CPU定义的变量已经为每个cpu产生了一个副本, 接下的问题是如何使用
这些变量的副本.
.动态per-cpu变量副本的产生
1. alloc_percpu
alloc_percpu底层调用了__alloc_percpu, 使用默认对齐参数__alignof__(type), 第一部分,为系统
中的每个cpu分配副本的空间,第二部分,通过某种机制实现对cpu特定的副本空间的访问.
chunk作为一种存放管理数据的容器存在, 根据其上空闲空间的大小而在一个pcpu_slot数组所表示的
链表中进行迁移, 数组的索引i指明了其链表中chunk空闲空间的大小, 当需要动态分配一个per-cpu
变量时, 内核在pcpu_slot数组中查找有没chunk空闲空间满足需要, 如果有, 就在此chunk的空闲空间
为系统的每个cpu生成变量的副本空间, 如果没有,就重新创建一个新的chunk, 新分配的chunk会在内核
虚拟地址空间的vmalloc区为它分配副本空间, 空间的起以地址保存在chunk的base_addr成员中. chunk
用一个整形数组map来跟踪副本空间的分配情况, 当要分配一个动态per-cpu变量时, 就在副本空间查
找空闲区域, 找到之后为每个cpu都分配出存储该per-cpou变量的存储小块. 此时存储per-cpu变量的
空间还在vmalloc区, 如果之前该存储小块上还没有映射物理页面的话, 需要为新分配变量映射新的页
面, 物理页通过页分配器从伙伴系统获得, chunk通过成员变量populated来跟踪物理页面的提交.
???????chunk, 管理单元, 每个chunk分配多大的存储空间呢? 如何知道它满了而需要创建新的空间, populated
如何跟踪物理页面的提交.
3.6.4 使用per-cpu变量
get_cpu_var宏,
int *p
preempt_disable();
p = (int*)(&dolphin + __per_cpu_offset[raw_smp_processor_id()]);
val = *p;vvvvvv
__per_cpu_offset是用来实现处理器副本访问的基础, 每个处理器副本所在空间的偏移地址都由
__per_cpu_offset引出, 这是个全局数组变量:
unsigned long __per_cpu_offset[NR_CPUS]__read_mostly;
它的初妈化出现在setup_per_cpu_areas中, 内核启动阶段初始化, 函数首先诈出副本空间首地址(pcpu_
base_addr)与".data.percpu"section首地址(__per_cpu_start)之间的偏移量delta. pcpu_unit_offset
s[cpu]保存对应cpu所在副本空间相对于pcpu_base_addr的偏移量, 这样就可以得到per-cpu变量副本的
偏移量, 放在__per_cpu_offset数组中.
其中preempt_disable()用来关闭内核可抢占性, 关闭内核可抢占性可确保在对per-cpu变量操作的临界区
中, 当前进程不会被换出处理器, 在put_cpu_var中恢复内核调度器的可抢占性.
per_cpu(var, cpu)用来读取其它处理器中的副本.为了安全起见,per_cpu_ptr结构get_cpu和put_cpu配对使用.
本文欢迎转载
本文出处:blog.csdn.net/dyron
页分本器提供的接口总体上有两类, 一类是以alloc_pages函数领衔, 这类函数可以工作在三个内存域
ZONE_HIGHMEM, ZONE_NORMAL, ZONE_DMA, 具体在哪个区分配, 由gfp_mask参数指定,如果所指定区没有
足够页, 函数会自动到下一级区域分配, 但不会进入上级区域.
ZONE_NORMAL, ZONE_DMA在系统起动过程中, 被一一线性映射, 意味着物理内存地址与虚拟地址之间只存
在一个常量差值(PAGE_OFFSET). 所以页面分配器在内部实现上无法去更改这段映射区所在的内核页目录
表. 所以alloc_pages返回的页面位于低端物理内存, 那么通过page_address函数可以将返回的struct
page对象指针转变成内核线性地址. 如果alloc_pages返回的页位于高端内存, 调用者就需要用kmap等
函数对返回的struct page对象建立映射关系, 如果成功完成映射, 就将返回一个内核虚拟地址, 该地址
位于内核虚拟地址空间中的动态映射区. alloc_pages的默认行为是, 如果没有指定GFP_HIGHMEM/GFP_DMA
, 就会从GFP_NORMAL中分配.
另一组页面分配器以__get_free_pages函数领衔, 它们通过alloc_pages来分配物理页面,不是的是__get_
free_pages函数只能在低端内存区域中分配物理页, 函数返回的是内核线性地址, 而不是struct page指针
其次是基于slab分配器的kmalloc函数和kmem_cache_alloc函数. slab分配器是工作在页分配器基础之上
的, 但是它只能低端物理内存区中分配页面, kmalloc函数在内部的实现上基于size_cache, 这是一组
kmem_cache所构成的缓存, 每个缓存对应特定大小的内存对象. kmem_cache_alloc函数实际上是对size_cache
的一种扩展, 它可以对kmem_cache中的内存对象大小进行定制.
接下来是vmalloc, 这个函数工作在内核虚拟空间VMALLOC_START和VMALLOC_END所表示的vmalloc区. 该
虚拟空间对物理内存的映射不是一一对应的, 虚拟上连续, 物理上不一定连续. 它映射的物理内存优先从
高端内存区分配, 所以它的使用场景是, 需要分配一大段内存, 而kmalloc可能失败时, 可以使用vmalloc
在内核虚拟地址空间vmalloc区分配一段连续的虚拟地址空间. 另外, vmalloc不能保证原子性, 因此不能在
进程上下文中使用, kmalloc函数可以通过GFP_ATOMIC标志达成原子性, vmalloc在分配过程中因为涉及对
页表项的操作, 这种操作常常要重建TLB, 会导致对vmalloc返回地址进行访问时带来较大系统开销.
最后一个内存分配函数主要用于多处理器系统中, per-cpu内存分本器, 它的思想是通过为系统中每个处
理器都分配一个cpu特定的变量副本, 来减少多处理器并发访问时的锁定操作, 以达到提高系统性能的目的.