Linux系统内核启动过程中,会在start_kernel() ->
先看函数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]
至于为设备内存做二级映射的过程,我将另写一篇做详细记录,因为内容比较多。
上一篇:ARM Linux (S3C6410架构/2.6.35内核)的内存映射(三)
下一篇:Arm-Linux二级页表的问题
推荐阅读最新更新时间:2024-03-16 14:58
设计资源 培训 开发板 精华推荐
- 希润医疗孟铭强:手功能软体机器人,让脑卒中患者重获新生
- 柔灵科技陈涵:将小型、柔性的脑机接口睡眠设备,做到千家万户
- 微灵医疗李骁健:脑机接口技术正在开启意识与AI融合的新纪元
- USB Type-C® 和 USB Power Delivery:专为扩展功率范围和电池供电型系统而设计
- 景昱医疗耿东:脑机接口DBS治疗技术已实现国产替代
- 首都医科大学王长明:针对癫痫的数字疗法已进入使用阶段
- 非常见问题解答第223期:如何在没有软启动方程的情况下测量和确定软启动时序?
- 兆易创新GD25/55全系列车规级SPI NOR Flash荣获ISO 26262 ASIL D功能安全认证证书
- 新型IsoVu™ 隔离电流探头:为电流测量带来全新维度
- 英飞凌推出简化电机控制开发的ModusToolbox™电机套件