2.2.26 内核在启动初期,会创建空闲区域来为内存分配提供地址空间。通过 free_area_init 接口来初始空闲区域。
// 初始空闲区域
unsigned long __init free_area_init(unsigned long start_mem, unsigned long end_mem)
{
mem_map_t *p;
unsigned long i, j;
i = (end_mem - PAGE_OFFSET) >> (PAGE_SHIFT + 7); // 空闲区域的大小在 10 ~ 256 页之内,避免内存浪费
// 加 7 的操作是为了处理存在余数的情况下增加 1 页
if (i < 10)
i = 10;
if (i > 256)
i = 256;
freepages.min = i;
freepages.low = i * 2;
freepages.high = i * 3;
mem_map = (mem_map_t *)LONG_ALIGN(start_mem); // 确定 mem_map 数组起始位置
p = mem_map + MAP_NR(end_mem);
start_mem = LONG_ALIGN((unsigned long) p);
memset(mem_map, 0, satrt_mem - (unsigned long)mem_map); // 将 mem_map 数组全部清空
do {
--p;
atomic_set(&p->count, 0); // mem_map 数组元素计数清 0,设置为 DMA 和预留
p->flags = (1 << PG_DMA) | (1 << PG_reserved);
} while (p > mem_map)
for (j = 0; j < NR_MEM_TYPES; j++) { // 空闲区域划分为多个类型,每个类型中包含一系列页
unsigned long mask = PAGE_MASK;
for (i = 0; i < NR_MEM_LISTS; i++) {
unsigned long bitmap_size;
init_mem_queue(free_area[j] + i); // 初始链表
mask += mask;
end_mem = (end_mem + ~mask) & mask;
bitmap_size = (end_mem - PAGE_OFFSET) >> (PAGE_SHIFT + i); // 计算位图所需大小
bitmap_size = (bitmap_size + 7) >> 3;
bitmap_size = LONG_ALIGN(bitmap_size);
free_area[j][i].map = (unsigned int *)start_mem; // 位图地址
memset((void *)start_mem, 0, bitmap_size); // 位图清 0
start_mem += bitmap_size;
}
}
return start_mem;
}
分析上述位图创建过程,可知结构如下:
bit_map |--0--|...|--n--|
|
|
|--0--|...|--n--|
|
|
上述接口涉及的结构体和变量如下:
// 页结构体
typedef struct page {
struct page *next;
struct page *prev;
struct inode *inode;
unsigned long offset;
struct page *next_hash;
atomic_t count;
unsigned long flags;
struct wait_queue *wait;
struct page **pprev_hash;
struct buffer_head * buffers;
} mem_map_t;
// 全局定义页
mem_map_t * mem_map = NULL;
// 空闲区域结构体
struct free_area_struct {
struct page *next; // 空闲区域中的页链表
struct page *prev;
unsigned int *map;
unsigned long count;
};
// 全局定义的空闲区域
static struct free_area_struct free_area[NR_MEM_TYPES][NR_MEM_LIST];
申请地址空间时,内核将根据 free_area 和 mem_map 数组来决定,内核提供了宏 RMQUEUE_TYPE 作为内存分配的最终执行者。
#define RMQUEUE_TYPE(order, type) \
// 通过下行代码可知,内核将空闲区域划分后,区域内的每个页又会按照 2^n 次方划分,并将指数级一致的页归类到一个链表中
do { struct free_area_struct * area = free_area[type] + order; \
unsigned long new_order = order; \
do { struct page *prev = memory_head(area), *ret = prev->next; \
if (memory_head(area) != ret) { \ // 判断当前指数级中是否存在空闲页
unsigned long map_nr; \
(prev->next = ret->next)->prev = prev; \ // 取走满足的空闲页,并在空闲链表中删除
map_nr = ret - mem_map; \ // 计算页号
MARK_USED(map_nr, new_order, area); \
nr_free_pages -= 1 << order; \
area->count--; \
EXPAND(ret, map_nr, order, new_order, area); \
spin_unlock_irqrestore(&page_alloc_lock, flags); \
return ADDRESS(map_nr); \ // 返回页所指向的地址
} \
new_order++; area++; \
} while (new_order < NR_MEM_LISTS); \
} while (0)
上述宏定义在 kmem_cache 结构被应用于内存分配接口中。
关于上述中的 free_are_init 接口则是在 paging_init 接口中被调用,不同的架构实现的页初始化不同,以 i386 为例。
__initfunc(unsigned long paging_init(unsigned long start_mem, unsigned long end_mem))
{
pgd_t *pg_dir; // 页目录
pte_t *pg_table; // 页表
unsigned long tmp;
unsigned long address;
start_mem = PAGE_ALIGN(start_mem); // 页对齐: return start_mem & ~(PAGE_SIZE - 1);
address = PAGE_OFFSET; // PAGE_OFFSET 代表页管理的起始位置
pg_dir = swapper_pg_dir; // 页目录入口地址
pgd_val(pg_dir[0]) = 0;
// #define USER_PGD_PTRS (PAGE_OFFSET >> PGDIR_SHIFT)
// 页表的应用从 start_mem 位置开始,因此页目录的起始填充位置需要改变为 start_mem 对应的
pgd_dir += USER_PGD_PTRS;
while (address < end_mem) { // 页起始位置小于页结束位置
...
pg_table = (pte_t *) (PAGE_MASK & pgd_val(*pgd_dir)); // 查看页目录中是否已存在页表地址
if (!pg_table) { // 当页目录中不存在页表信息时,将占用 start_mem 起始的部分空间来作为 pte 信息的存储空间
pg_table = (pte_t *) __pa(start_mem);
start_mem += PAGE_SIZE;
}
pgd_val(*pg_dir) = _PAGE_TABLE | (unsigned long) pg_table; // 页目录中填充页表地址
pg_dir++;
pg_table = (pte_t *) __va(pg_table);
for (tmp = 0; tmp < PTRS_PER_PTE; tmp++, pg_table++) { // 循环填满一张页表
pte_t pte = mk_pte(address, PAGE_KERNEL); // 创建 pte,主要记录页地址和页属性
if (address >= end_mem)
pte_val(pte) = 0;
set_pte(pg_table, pte); // pte 填充 pg_table
address += PAGE_SIZE;
}
}
start_mem = fixmap_init(start_mem);
#ifdef __SMP__
start_mem = init_smp_mapping(start_mem);
#endif
local_flush_tlb(); // 刷新页表(i386 主要修改 cr3 来改变页目录地址),其他架构可能存在其他方式
return free_area_init(start_mem, end_mem); // 开始构建空闲区域结构
}
根据上述分析可知 i386 内核页管理模式所占用的空间分布如下:
swapper_pg_dir
|------------| start_memroy
| pgd |---->|----------|
|------------| | pte |
|----------|
此外,关于代码中涉及的物理地址和虚拟地址的转换,分析接口 __pa / __va 可知 i386 的映射关系如下:
虚拟地址空间 物理地址空间
|------------|
kernel_base | 0xc0000000 | ---|
|------------| | | |
| | | |----------|
|---> | 0x0 |
|----------|
综上,便是 free_area 的入口调用分析。