Linux内存管理简述
Linux的内存管理不仅与处理器的架构相关,还要综合考虑性能需求。在Linux内核内存管理的框架中,最底层的page allocator是对物理内存进行管理的模块,负责管理所有的物理内存,分配和释放都是以page为单位,大小是2^N个连续的物理内存页。所有的内存管理都是以page allocator为基础,采用的算法为经典的Buddy(伙伴算法)。
对于UMA架构,比如SMP,只有一个物理内存节点,但是对于服务器的NUMA架构,会有多个物理内存节点。Linux为了管理所有内存节点的物理内存,将不同的节点称之为node,用专门的结构体进行管理。每个node又会划分成不同的区域(zone)。每个zone管理着属于自己的pages。
Buddy系统
不论是哪种架构,内存管理的基础都是page allocator。page allocator负责管理所有物理内存,采用的算法就是经典的Buddy算法(伙伴系统)。下面这幅图就是buddy系统的对物理内存管理的整个框架:
buddy系统的整体框架
大概的说一下上面这个框架,这其中是包含了3部分,一个是buddy系统两个主要的数据结构free_area和free_list的关系,第二个是Linux的反碎片技术,第三个就是buddy对空闲物理页的组织。
buddy的主要数据结构
buddy的主要数据结构有两个:free_area
和free_list
。第一个是为了组织和管理空闲的物理页面,第二个是内核的反碎片技术的具体实现。
free_area定义在struct zone
中:
free_area
是一个结构体数组。buddy将连续的空闲页面按照2^order的大小组织在一个free_area
数组中,每个zone
都有一个这样的数组。free_area[0]
就表示所指向的空闲块链表中,每个块的大小为2 ^0=1个page。
其中,MAX_ORDER
是内核规定的一个空闲页块所能包含的最大页数:
free_area
数组最多有11个元素,free_area[10]
就指向了最大空闲块的链表,这个链表中每个空闲块为2^10个页,也就是4M,所以一次能申请到的最大空闲内存块就是4M。
struct free_area结构体:
这里面free_list
才真正指向了空闲块链表,这里面的MIGRATE_TYPES
是内核中使用的反碎片技术。
内核的反碎片技术
首先按照物理页的属性分为不同的类型,也就是MIGRATE_TYPE
(页面迁移类型)。内核将MIGRATE_TYPE
分为五种类型:
对于Linux内核来说,主要是3种类型:UNMOVABLE
(不可移动),RECLAIMABLE
(可回收),MOVABLE
(可移动)。反碎片技术就是在分配的时候考虑到这些页的属性,比如不可移动的页不能位于可移动的内存区的中间,否则就无法从该内存区域获得较大的连续物理内存。
buddy的核心函数:
不论是UMA架构,还是NUMA架构,最终都会调用到__alloc_pages_nodemask()
,因为这个函数会进行实际物理页面的分配。
这个函数我还没有太看完,所以这里就先说一些关于这个函数细节的东西:
gfp_mask
gfp是get free page的意思,它只是一个传入的标志掩码,重要的是它的类型gfp_t
。
gfp_t类型
gfp_t
是内核使用的数据类型,内核使用的数据通常对类型安全的要求非常严格。
可以看到这里使用typedef
将__bitwise__
重新定义了一个类型就是gfp_t
。__bitwise__
是什么东西?在内核代码里这个东西应该是很常见的,它的作用就是防止不同字节序类型的数据进行运算。
上面的__le16
表示16位小端模式,__be16
表示是16位大端模式。加上__bitwise
表示这些数据类型将是字节序敏感的,绝对禁止他们之间的运算。
__ bitwise __
__bitwise__
是按位的意思,这个功能是给sparse(内核代码的静态分析工具)用的,内核的一些数据类型经常会用它修饰。
如果定义了__CHECKER__
宏,就会使能sparse的检查功能,而且sparse支持GCC的__attribute __((……))
功能。bitwise
用来确保不同位方式类型不会被弄混(大端模式,小端模式等)。
__alloc_pages_nodemask( )
做的第一件事就是要确定在哪个zone_type
中分配内存,所以它会调用gfp_zone( )
,根据传入的gfp_mask
标志掩码来确定zone的类型。
zone type
Linux将一个node
划分为以下几种类型:
对于32位来说,就是ZONE_DMA
,ZONE_NORMAL
和ZONE_HIGHMEM
,对于64位,已经不需要高端内存,所以没有了ZONE_HIGHMEM
区域,而是ZONE_DMA
和ZONE_NORMAL
,而且64位需要兼容32位,所以还会划分一个区域ZONE_DMA32
。
gfp_zone( )
gfp_zone
函数可以通过传入的标志位掩码gfp_mask
来确定需要分配内存的区域。
注意这一句:
int bit = flags & GFP_ZONEMASK;
GFP_ZONEMASK
掩码标志,本质是一个宏
这个宏经过__GFP_DMA
,__GFP_HIGHMEM
,__GFP_DMA32
,__GFP_MOVABLE
4个域掩码按位或得到。
__GFP_DMA
,__GFP_HIGHMEM
,__GFP_DMA32
,__GFP_MOVABLE
的值定义如下:
将这4个域掩码的值按位或,即0x01
| 0x02
| 0x04
| 0x08
= 1111
,所以GFP_ZONEMASK
就等于1111,这个值是一个固定的值,作为参与后期运算的掩码而设计。这里,注意以下gfp_t
的类型,是__force
。刚才在上面说过被__biewise
修饰的类型,即使强制类型转换都会被sparse警告,但是有些类型确实需要进行类型转换,总不能一棒子全打死,所以能破解这种警告的就是使用__force
来标明对应的类型。
与__bitwise
一样,force
也是sparse的属性,内核将force
重新包装了一下,使用__force
来表示这种属性。使用了__force
标明一个类型后,sparse就不再报出警告。对于gcc来说,在编译的时候,__force
是透明的,gcc只会看到gfp_t
,而看不到前面的__force
属性。
从gfp_zone( )
的代码中,可以看到gfp_zone( )
依赖两张表:GFP_ZONE_TABLE
,GFP_ZONE_BAD
GFP_ZONE_TABLE
这个GFP_ZOEN_TABLE
看着很长,实际它是一个组装后的结果,只要将它一步一步重新组装就知道它怎么来的了。前面说过掩码位标志有4种,分别是__GFP_DMA
,__GFP_HIGHMEM
,__GFP_DMA32
,__GFP_MOVABLE
,2的4次方就有16种组合,这16
种组合的结果如下:
内核规定__GFP_DMA
,__GFP_HIGHMEM
,__GFP_DMA32
这3个标志不能两个或全部出现,其中BAD就表示这种错误的组合。将所有的BAD组合或起来就构成了GFP_ZONE_BAD
表。除了BAD
外的情况就是内核允许的情况,将这些组合或在一起就构成了GFP_ZONE_TABLE
表。
关于这两张表有什么用,怎么组成的,还有__alloc_pages_nodemask( )
函数剩余部分的分析下次周报再发,我还没有全部分析完。