Linux内存管理(1) - bootmem分配器

1.    bootmem分配器

内核中分配内存基本都基于伙伴系统,但是在内核启动之初,伙伴系统尚未建立,这时需要一个临时的内存分配器负责提供内核早期的内存需求,例如存放内核的代码段和数据段,以及将内存进行简单的管理供后续伙伴系统使用,这就是bootmem分配器。

本文基于Linux 2.6.31的内核源码对bootmem分配器的工作过程进行分析。为方便说明代码流程,我以最简单的情形为例:在内核中没有开启内核选项CONFIG_DISCONTIMEM、CONFIG_SPARSEMEM和CONFIG_NUMA。只开启了CONFIG_FLATMEM选项,不考虑SMP的情况。

并且为了让代码更容易理解,我假设内存总大小为64MB,这样一些代码会看的更直观。

1.1  建立必要的数据结构

内核启动之前,内核代码已经拷贝到内存中,这部分内存不能被修改,即不能用作其他用处。所以,这样的内存要被标记为reserved(已被使用)。

内存是划分为节点的,每个节点关联到系统中的一个处理器,在内核中表示为struct pglist_data结构的实例(该数据结构后面会讲到)。

各个节点又划分为内存域,是内存的进一步细分,内核中定义了如下的内存域类型:

enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
	__MAX_NR_ZONES
};

ZONE_DMA标记适合DMA的内存域;

ZONE_DMA32标记了使用32位地址可寻址的、适合DMA的内存域,只有64位系统上两种DMA内存域才有差异,32位计算机上,本内存域是空的;

ZONE_NORMAL标记了可直接映射到内核段的普通内存域,该内存域是唯一一个所有体系结构上都会保证存在的内存域;

ZONE_HIGHMEM标记了超出内核段物理内存的高端内存域;

ZONE_MOVABLE是一个虚拟内存域,在防止物理内存碎片的机制中需要使用该内存域。

本文中我们只考虑ZONE_NORMAL内存域。

先看一些数据结构和全局变量:

结构体struct pglist_data用于表示内存节点,每个CPU都有一个这个结构,它的bdata成员指向系统启动期间的bootmem分配器的实例,用于管理属于这个节点的内存。

typedef struct pglist_data{
   struct zone node_zones[MAX_NR_ZONES]; /* 节点中的内存域 */
   struct zonelist node_zonelists[MAX_ZONELISTS]; /* 分配内存的备用列表 */
   int nr_zones;
   struct bootmem_data *bdata;
   unsigned long node_start_pfn;
   unsigned long node_present_pages; /* total number of physicalpages */
   unsigned long node_spanned_pages; /* total size of physical page  range,including holes */
   int node_id; /* node id */
   wait_queue_head_t kswapd_wait;
   struct task_struct *kswapd;
   int kswapd_max_order;
} pg_data_t;

在单处理器的情况下,我们只需要一个节点,内核为了方便,针对这种情况进行了如下定义:

struct pglist_data __refdata contig_page_data= { .bdata = &bootmem_node_data[0] };

bootmem_node_data数组也是一个全局定义:

bootmem_data_t bootmem_node_data[MAX_NUMNODES]__initdata;

单CPU的MAX_NUMNODES = 1。按照如上所说,bootmem_node_data[0]就是该CPU上的bootmem分配器的实例。

我们在获取CPU的节点时,不直接使用contig_page_data,而是通过下面的宏:

#define NODE_DATA(nid)        (&contig_page_data)

例如通过NODE_DATA(0)  来获取node id为0的内存节点。

每个节点的bootmem分配器是一个struct bootmem_data结构,

/*
 * node_bootmem_map is a map pointer - the bitsrepresent all physical
 * memory pages (including holes) on the node.
 */
typedefstruct bootmem_data {
    unsigned long node_min_pfn; /* 可管理的第一个页帧的编号,通常是0 */
    unsigned long node_low_pfn; /* 可管理的最后一个页帧的编号 */
    void *node_bootmem_map; /* 位图,标记所有页帧的状态 */
    unsigned long last_end_off;
    unsigned long hint_idx;
    struct list_head list;
} bootmem_data_t;

其中,node_bootmem_map成员是一个位图,用于标记所有内存页帧是否是reserve的:1为reserved,0为空闲的。由于这时的系统处理启动初期,并不需要太复杂的内存分配机制,相反,bootmem分配器只用在内核启动初期,因此分配方式越简单越好,使用一个位图来管理所有的内存页就足够了。

系统中所有的bootmem分配器都放在一个全局链表上,用于全局查找特定内存区域来分配内存或标记页的状态。

static struct list_head bdata_list__initdata = LIST_HEAD_INIT(bdata_list);

在内核中,内存是以页为单位来管理的。通常约定“页帧”表示物理页,“页”表示虚拟页,注意,struct page结构描述的是物理页。在我的内核配置中,一个页的大小为4KB。

基于内存大小为64MB的假设,我们来看一下代码流程:

初始化的工作在bootmem_init()函数中完成,它做了如下的工作:

1.   给bdata->node_bootmem_map成员自身分配空间,因为我们后面会一直用到它。由于这时还没有什么内存分配机制,所以直接找能用的内存存放该成员即可。

我们已经提到,内核代码段是保留的,内核代码段中的最后一个符号为_end,在我编译内核后生成的System.map文件中可以看到它的值为0x81042470,由于0x8000000是内核态的基地址,所以,对于物理内存来说,_end的地址为0x1042470,一个page的大小为4K,则可知内核代码部分需占用0x1043个页帧的大小。

分配完成后,我们看到bdata->node_bootmem_map自身的起始地址是0x81043000,即紧跟在_end后面。每个bit位表示一个页帧是否是reserve的,1为reserve,0为可用。那标记所有0x4000(64MB)个页帧,需要的空间为(0x4000 / 8)个字节。

在这一步,所有的页帧都被标记为reserved了。

动作完成后&#x

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值