尊天命,尽人事
分类: 嵌入式
2011-08-23 10:57:08
注:本文中提到的ICE为一Android工程,对应Linux内核版本为2.6.29。
为了合理快速的获取内存,Linux分多层管理结构以适应不同架构及不同的使用方式。图2-1描述了内存管理的架构(注意一个系统中并不一定是2个node,一般情况下UMA只使用1个node,而NUMA最多使用8个或16个)。
图2-1:内存管理架构
下面将依据此图由底向上依次介绍各功能块的作用。
NUMA(Non-Uniform Memory Access)即非一致内存访问。在多CPU系统中有可能出现给定CPU对不同内存单元访问的时间不同。为了让指定CPU总能最先使用访问时间最短的内存,Linux把物理内存分成几块并以节点(node)标识。这样一来,每个CPU都有最快访问内存的节点,但并不等于只能访问这个节点。下表中node_zonelists将其他节点的各管理区也链了进来,但均排在本节点管理区之后,以示其它节点优先级低于本节点。
在ARM体系下不会出现这种情况,但为了统一代码,Linux仍然使用这种管理方式,只是所有内存都归一个节点管理,比如都在上图的node1中。
内核中用struct pglist_data结构体来存放节点信息,各成员解释如下:
类型 | 名字 | 说明 |
struct zone [] | node_zones | 节点中管理区描述符的数组,参考下一节 |
struct zonelist [] | node_zonelists | zone表,页分配器获取内存的入口 |
int | nr_zones | 节点中管理区的个数 |
struct page * | node_mem_map | 节点中页描述符数组 |
struct bootmem_data * | bdata | 内核初始化阶段内存管理入口 |
unsigned long | node_start_pfn | 节点中第一个叶框下标 |
unsigned long | node_present_pages | 属于本节点的内存的页框数(不包括洞) |
unsigned long | node_spanned_pages | 属于本节点的内存的页框数(包括洞) |
int | node_id | 节点标识符 |
wait_queue_head_t | kswapd_wait | kswapd页换出守护进程使用的等待队列 |
struct task_struct * | kswapd | 指针指向kswapd内核线程的进程描述符 |
int | kswapd_max_order | kswapd将要创建空闲块大小取对数的值 |
表2-1:节点描述符
出于各种原因,Linux把每个节点内的内存划分成下面三个管理区:
ZONE_DMA
有些体系的DMA总线只能访问内存的前16M,因此将内存的前16M分出,优先给直接内存访问使用。在ARM体系中不存在这种情况,所以也没有DMA管理区。
ZONE_HIGHMEM
如果内存足够大(比如用户:内核线性空间=3:1,内核就只能访问线性空间的第4GB内容,如果物理内存超过1GB则视为足够大),内核线性空间无法同时映射所有内存。这就需要将内核线性空间分出一段不直接映射物理内存,而是作为窗口分时映射使用到的未映射的内存。在ICE平台上用户:内核线性空间是2:2,即用户和内核均拥有2G的线性空间,而物理内存只有464M给Linux使用,所以无需高端内存管理区。
ZONE_NORMAL
除去上面两个区的内存,剩下的放到ZONE_NORMAL区中。在ICE平台上,所有内存都在该区。
内核用struct zone来管理内存区,各成员解释如下:
类型 | 名字 | 说明 |
unsigned long | pages_min | 管理区中保留页的数目 |
unsigned long | pages_low | 回收页框时使用的下界,同时也被管理区分配器作为阈值使用 |
unsigned long | pages_high | 回收页框时使用的上界,同时也被管理区分配器作为阈值使用 |
unsigned long [] | lowmem_reserve | 指明在处理内存不足的临界情况下管理区必须保留的页框数 |
struct per_cpu_pageset [] | pageset | 用于实现单一页框的特殊高速缓存 |
spinlock_t | lock | 保护该描述符的自旋锁 |
struct free_area [] | free_area | 标识出管理区中的空闲页框 |
spinlock_t | lru_lock | 活动以及非活动链表使用的自旋锁 |
struct lru [] | lru | 活动以及非活动链表 |
unsigned long | flags | 管理区标志 |
atomic_long_t [] | vm_stat | 管理区内各类型内存统计 |
int | prev_priority | 管理区优先级,由回收页框算法使用 |
wait_queue_head_t * | wait_table | 进程等待队列的散列表,这些进程正在等待管理区中的某项。 |
unsigned long | wait_table_bits | 等待队列散列表数组大小,置位2的order幂 |
struct pglist_data * | zone_pgdat | 包含该管理区的节点指针 |
unsigned long | zone_start_pfn | 管理区的起始页框号 |
unsigned long | spanned_pages | 管理区的页框数,包括洞 |
unsigned long | present_pages | 管理区的页框数,不包括洞 |
const char * | name | 指向管理区名字,一般为"DMA" "Normal" "HighMem" |
表2-2:管理区描述符
页描述符(page)内核必须记录每个页框当前的状态。例如,内核必须能区分哪些页框包含的是属于进程的页,而哪些页框包含的是内核代码或内核数据。类似地,内核还必须能够确定动态内存中的页框是否空闲。如果动态内存中的页框不包含有用的数据,那么这个页框就是空闲的。在以下情况下页框是不空闲的:包含用户态进程的数据、某个软件高速缓存的数据、动态分配的内核数据结构、设备驱动程序缓冲的数据、内核模块的代码等等。
页框的状态信息保存在一个类型为page的页描述符中,其中的字段如下表所示。所有的页描述符存放在mem_map数组中。因为每个描述符长度为32字节,所以mem_map所需要的空间略小于整个内存的1%。virt_to_page(addr)宏产生线性地址addr对应的页描述符地址。pfn_to_page(pfn)宏产生与页框号pfn对应的页描述符地址。
页描述符,在内核中表示为struct page:
类型 | 名字 | 说明 |
unsigned long | flags | 页标志,包括所在的节点及管理区编号(高位上) |
atomic_t | _count | 页框的引用计数器 |
atomic_t | _mapcount | 页框中的页表项数目,如果没有则为-1 |
unsigned long | private | 可用于正在使用页的内核成分(例如,在缓冲页的情况下它是一个缓冲器头指针)。如果页是空闲的,则该字段由伙伴系统使用(存放order值) |
struct address_space * | mapping | 当页被插入页高速缓存中时使用,或者当页属于匿名区时使用 |
pgoff_t | index | 作为不同的含义被几种内核成分使用。例如,它在页磁盘映像或匿名区中标识存放在页框中的数据的位置,或者它存放一个换出页标识符 |
struct list_head | lru | 包含页的最近最少使用(LRU)双向链表的指针 |
表2-3:页描述符
下面这两个字段非常重要,需作重点描述:
_count
页的引用计数器。如果该字段为-1,则相应页框空闲,并可被分配给任一进程或内核本身;如果该字段的值大于或等于0,则说明页框被分配给了一个或多个进程,或用于存放一些内核数据结构。page_count()函数返回_count加1后的值,也就是该页的使用者的数目。
flags
包含多达32个用来描述页框状态的标志,如下表。对于每个PG_xyz标志,内核都定义了操作其值的一些宏。通常,PageXyz宏返回标志的值,而SetPageXyz和ClearPageXyz宏分别设置和清除相应的位。页标志32位分布如下:
Page flags: | [SECTION] | [NODE] | ZONE | ... | FLAGS |
标志名 | 含义 |
PG_locked | 页被锁定。例如,在磁盘I/O操作中涉及的页 |
PG_error | 在传输页时发生I/O错误 |
PG_referenced | 刚刚访问过的页 |
PG_uptodate | 在完成读操作后置位,除非发生磁盘I/O错误 |
PG_dirty | 页已经被修改 |
PG_lru | 页在活动或非活动页链表中 |
PG_active | 页在活动页链表中 |
PG_slab | 包含在slab中的页框 |
PG_highmem | 页框属于ZONE_HIGHMEM管理区 |
PG_checked | 由一些文件系统(如Ext3)使用的标志 |
PG_reserved | 页框留给内核代码或没有使用 |
PG_private | 页描述符的private字段存放了有意义的数据 |
PG_writeback | 正在使用writepage方法将页写到磁盘上 |
PG_nosave | 系统挂起/唤醒时使用 |
PG_compound | 通过扩展分页机制处理页框 |
PG_swapcache | 页属于对换高速缓存 |
PG_mappedtodisk | 页框中的所有数据对应于磁盘上分配的块 |
PG_reclaim | 为回收内存对页已经做了写入磁盘的标记 |
PG_nosave_free | 系统挂起/恢复时使用 |
表2-4:页标志
伙伴系统算法伙伴系统(buddy system)算法及下节要讲的slab分配器是Linux内存分配的两大核心算法。它们的目的都是提高内存分配效率的同时降低内存碎片。
其实解决内存碎片有两种办法:
I 将不连续的内存页映射到连续的线性地址区间;
II 将大内存分成各种固定大小的块,每次分配时,尽量使用最接近申请大小的那一块,尽量避免为满足小块内存的申请而分拆大的块。
办法I即是vmalloc的思想,但有时后要求申请到内存必须是连续,这就需要方法II。其实方法I还有一个缺点:它需要修改页表,需要硬件的频繁动作,这增加了内存申请和使用的时间。因此多数情况都采用方法II来申请内存。伙伴系统算法即是方法II的一个使用算法。
伙伴系统特征如下:
I 把所有的空闲页框分组为MAX_ORDER(ICE为11)个块链表,每个块链表分别包含大小为2n(n为0~10的整数)个连续的页框,即1、2、4、6、8、16、32、64、128、256、512和1024个连续的页框。
II 每个块的第一个页框的物理地址是该块大小的整数倍。
下图显示了伙伴系统内存组织图,对应着管理区结构中的free_area数组,它们通过页描述符中的list_head链在一起。页描述符首地址放在节点结构的node_mem_map,也可通过全局变量mem_map访问。free_area结构中还有一个nr_free字段,表示各个链表中空闲块的数目。
图2-2:伙伴系统内存组织图
只以这两个特性不足以表现伙伴系统的强大。它的卓越点在动态管理上。比如要申请一页内存,但0链表中的块已经全部用完了。一般系统通常的做法是到1链表中找一个块给申请者。这无疑浪费了一页的内存。伙伴系统的做法是将这个块分成两个一页的块,一块给申请者,另一块插到0链表中。同样,如果1链表中的块也用完了,就会到2链表中找,找到后会将该块分成三块:两个一页的块和一个两页的块。一个一页的块给申请者,另一个插入到0链表,两页的块插入到1链表。依此类推,直到查到10链表,仍然没有空闲块,才会返回出错。
同样原理用在释放内存上。假设有一页的块要释放,伙伴系统会把它插入到0链表的相应位置,同时查看与前后块是否为伙伴(地址是否连续,如连续再查看排在前面的块的地址是否是两页的整数倍)。如果是,则将这两块合并成一个两页的块插入到1链表的相应位置,然后再查看与前后块是否是伙伴,依此递归,直到已不是伙伴或到10链表。
伙伴系统用__rmqueue()函数实现对空闲块的分配,它的行为远远比上面分析的复杂。它主要通过两个函数来实现:__rmqueue_smallest()和__rmqueue_fallback()。上面描述的其实只是__rmqueue_smallest()的行为,当__rmqueue_smallest()获取不到块时,会继续执行__rmqueue_fallback()来拆分大的块。这就有个疑问了,不是说获取不到块了吗?又哪来的大块?事实上图2-2并没有把细节画出来,把它再放大,如下图:
图2-3:zone的free_area分解
事实上11个块链表中的每一个又分成了五种类型的块表,每个表中包含了n个块。我们使用__rmqueue()函数申请块时要指定块的类型,当__rmqueue_smallest()从指定的类型块中找不到块,将会执行__rmqueue_fallback()从其他类型块表中借块来使用,这些都是细节的东西,也比较复杂,不再细述。
释放申请的块的函数是free_pages_bulk()。它是__rmqueue()的逆过程,不再分析。
页框分配器页框分配器核心函数是__alloc_pages_internal()它主要调用的函数是__rmqueue()。简单的说页框分配器是对伙伴系统的一种封装。我们常用的页框分配函数又是对__alloc_pages_internal()的封装,如:alloc_pages_node()。alloc_pages_node()返回给调用者的是页框描述符。下面slab分配器使用的kmem_getpages()仅是将alloc_pages_node()返回的页框描述符转换成了线性地址。