ARM Linux (S3C6410架构/2.6.35内核)的内存映射(二)

本文讲述Linux系统启动过程中内核空间的映射。

Linux系统内核启动过程中,会在start_kernel() -> setup_arch() -> paging_init()函数中建立页表,下面详细记录一下其中每一个重要的步骤。(下面演示的代码经过删减)

先看函数prepare_page_table()

   [c]
static inline void prepare_page_table(void)
{
unsigned long addr;
for (addr = 0; addr < MODULES_VADDR; addr = PGDIR_SIZE) {
pmd_clear(pmd_off_k(addr));
}

for ( ; addr < PAGE_OFFSET; addr = PGDIR_SIZE) {
pmd_clear(pmd_off_k(addr));
}

for (addr = __phys_to_virt(bank_phys_end(&meminfo.bank[0]));
addr < VMALLOC_END; addr = PGDIR_SIZE) {
pmd_clear(pmd_off_k(addr));
}
}
[/c]

函数prepare_page_table()的作用是清空内核页表。对于我的配置来说,前两个for循环可以合并为一个,它们的作用是清空地址区间[0x00000000, 0xC0000000)的内存映射;第三个for循环有些不一样,它所清空的区间与前面是不连续的,它从bank0的末尾开始,直到VMALLOC结束。为什么要把bank0让出来呢?因为bank0是内核正在运行的空间,这段区域已经在head.S中的汇编代码里映射好了,如果在这里一并清空的话,内核就没法运行了。
有一个地方我一直不太理解,就是PGDIR_SIZE的定义,在这个版本的内核里,这个值被定义为:

   [c]
#define PGDIR_SHIFT 21
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
[/c]

就是说,PGDIR_SIZE被定义为2M,那么为什么不能定义成1M呢?1M正好是一个section,这样不是正好容易理解吗?而且如果这样定义的话,pmd方面的处理也会比较麻烦一些(在这里PMD其实就是PGD)。比如在pmd_clear()中,每次都需要设置两项:

   [c]
#define pmd_clear(pmdp) \
do { \
pmdp[0] = __pmd(0); \
pmdp[1] = __pmd(0); \
clean_pmd_entry(pmdp); \
} while (0)
[/c]

接下来的一个重要函数是map_lowmem() -> map_memory_bank() -> create_mapping()

   [c]
static void __init create_mapping(struct map_desc *md)
{
unsigned long phys, addr, length, end;
const struct mem_type *type;
pgd_t *pgd;
......
pgd = pgd_offset_k(addr);
end = addr length;
do {
unsigned long next = pgd_addr_end(addr, end);
alloc_init_section(pgd, addr, next, phys, type);
phys = next - addr;
addr = next;
} while (pgd , addr != end);
}
[/c]

map_lowmem()是为低端物理内存建立映射,在我的模拟环境中,物理内存只有一个bank,共有16M。alloc_init_section()为每一个PGD建立映射。

   [c]
static void __init alloc_init_section(pgd_t *pgd, unsigned long addr,
unsigned long end, unsigned long phys,
const struct mem_type *type)
{
pmd_t *pmd = pmd_offset(pgd, addr);

if (((addr | end | phys) & ~SECTION_MASK) == 0) {
pmd_t *p = pmd;

if (addr & SECTION_SIZE)
pmd ;

do {
*pmd = __pmd(phys | type->prot_sect);
phys = SECTION_SIZE;
} while (pmd , addr = SECTION_SIZE, addr != end);

flush_pmd_entry(p);
} else {
alloc_init_pte(pmd, addr, end, __phys_to_pfn(phys), type);
}
}
[/c]

对于low memory的情况,条件if (((addr | end | phys) & ~SECTION_MASK) == 0)得到满足,这一段是专门为段式映射而准备的。在接下来的do循环中,连续两个PMD/PGD表项会被写入新的内容,以我的系统为例,写入的第一个表项内存是:
pmd = 0xc0007000, *pmd = 0x5000040e, phys = 0x50000000
即把物理地址0x50000000映射到虚拟地址0xc0000000,PMD/PGD表项的位置是在0xc0007000,写入的内容是0x5000040e,其中高12位是段的基地址(物理地址),而低20位0x40e是段的属性。

如果条件if (((addr | end | phys) & ~SECTION_MASK) == 0)不满足的话,函数alloc_init_section()的另外一半代码是为什么设计的呢?
答案是这段代码用于设备内存的映射。

接下来,内核要为设备内存建立映射,在paging_init()->devicemaps_init()->create_mapping()->alloc_init_section()->alloc_init_pte()这个调用栈中,就将用到alloc_init_section()的另外一半代码。与用于存储数据的一般内存不同,这里所说的设备内存往往是为访问设备用的特定地址或者用于特定功能的小段内存(比如中断向量表所占用的内存),而且各块设备内存在物理上可能并不连续,如果使用段为单位来做映射的话,就会浪费很多虚拟地址空间,所以设备内存使用页式映射,即二级映射。

以“中断向量表”的映射为例,在下面这段代码中,内核使用boot memory manager为中断向量表申请一页(4K)内存,并将这页内存映射到虚拟地址的0xffff0000处。对于中断向量表的位置,ARM为操作系统提供了两个选项,可以把它配置到内存的最低地址0x00000000处,也可以把它配置到到地址0xffff0000处,这里所说的地址都是虚拟地址,即经过MMU映射过后的地址。Linux默认选择后者,即高地址。

   [c]
static void __init devicemaps_init(struct machine_desc *mdesc) {
......
vectors = alloc_bootmem_low_pages(PAGE_SIZE);
......
map.pfn = __phys_to_pfn(virt_to_phys(vectors));
map.virtual = 0xffff0000;
map.length = PAGE_SIZE;
map.type = MT_HIGH_VECTORS;
create_mapping(&map);
[/c]

至于为设备内存做二级映射的过程,我将另写一篇做详细记录,因为内容比较多。

你可能感兴趣的:(linux,it,内存映射)