Linux内存初始化&开启分页(简)

摘要

本文主要介绍linux内核初始化时,对物理内存的使用结构,以及如何从实模式转换成保护模式,即如何开启内存分页。

1.物理内存布局

 

Linux内存初始化&开启分页(简)_第1张图片

如上图所示,在内核初始化阶段,内核被加载到物理内存地址0x00100000开始的地方,即图中_text的地方为内核代码的第一个字节,至于为何不加载到物理内存的起始处,可以参考其他资料。

2.进程的线程地址空间

我们知道,进程分为内核态和用户态,其中重要的一个区别就是运行于用户态的进程线性地址小于0xc0000000,内核态进程大于0xc0000000,因此0xc0000000就是内核态和用户态的分界线。线性地址是内核使用的逻辑地址,在真正访存时需要映射成物理地址,如果开启分页功能,则进行分页下的二级(三级)映射。

3.内核页表

内核开启分页功能,只需要一条指令,因此在开启前就需要完成页面表的设置,比如全局目录表,以及页面表,在目录表每项中填充指向页面表的物理地址。

为简单起见,我们假设内核使用的段、临时页表、及内核本身能容纳与RAM的前8M空间,按照每页4K大小,分页模式需要2个页表才能映射8M的大小。

临时页全局目录在内核编译时进行静态初始化,存放在swapper_pg_dir变量中,具体位置应该在上图_etext和_edata之间。

临时页面表被安排在上图中_end之后,即内核未初始化的数据段后面,2个页面大小,一共4K*2。

既然全局目录和临时页表的都已经清楚了,接下来将临时页表的物理地址填入全局目录表的相应表项中即可。但是需要注意的是需要同时保证在实模式下和保护模式下都能对这8M物理地址进行寻址。实模式对应线性地址0x00000000,保护模式,即内核态则对应的线程地址为0xc0000000。因此从

0x00000000到0x007fffff 和

0xc0000000到0xc07fffff的线性地址

都要映射物理内存的前8M,这样只需在全局目录表相应的表项中填上临时页表的物理地址即可。具体填什么,我们可以算一算。

首先,线性地址0x00000000起始的页面的页表的物理地址,毫无疑问填到目录表的第0项,对应着实模式。那么保护模式下的起始线性地址0xc0000000开始的页面的页表的物理地址对应到目录表中的那一项呢? 0xc00000000>>22=0x300(十进制768),即线性地址0xc0000000对应的页面的页表的物理地址应该填到目录表的第768位。

目录表的第1项和第769项填第二个临时页表的物理地址。设置完成后具体结构如下图所示

Linux内存初始化&开启分页(简)_第2张图片

 设置完成后,将目录表的物理地址swapper_gp_dir写入cr3寄存器,设置cr0的控制寄存器PG标志位开启分页。

4.当RAM小于896MB时的最终内核页表

内核全局目录仍然保存在swapper_gp_dir中,目录表项的初始化操作主要是申请内核页面表,注意是内核态的页表,然后将页面表的物理地址填入目录表项中,再将从0起始的物理地址按页划分,填入内核的页面表项中。可以从下面代码中看出,内核页表映射物理页面是静态固定的映射,和用户态页面动态映射不同。

	i = __pgd_offset(PAGE_OFFSET);
	pgd = pgd_base + i;

	for (; i < PTRS_PER_PGD; pgd++, i++) {
		vaddr = i*PGDIR_SIZE;
		if (end && (vaddr >= end))
			break;
#if CONFIG_X86_PAE
        //忽略此处,不考虑扩展模式
		pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
		set_pgd(pgd, __pgd(__pa(pmd) + 0x1));
#else
        //中间目录即页目录
		pmd = (pmd_t *)pgd;
#endif
		if (pmd != pmd_offset(pgd, 0))
			BUG();
        //非扩展模式下,PTRS_PER_PMD=1
		for (j = 0; j < PTRS_PER_PMD; pmd++, j++) {
			vaddr = i*PGDIR_SIZE + j*PMD_SIZE;
			if (end && (vaddr >= end))
				break;
			if (cpu_has_pse) {
				unsigned long __pe;

				set_in_cr4(X86_CR4_PSE);
				boot_cpu_data.wp_works_ok = 1;
				__pe = _KERNPG_TABLE + _PAGE_PSE + __pa(vaddr);
				/* Make it "global" too if supported */
				if (cpu_has_pge) {
					set_in_cr4(X86_CR4_PGE);
					__pe += _PAGE_GLOBAL;
				}
				set_pmd(pmd, __pmd(__pe));
				continue;
			}
            //设置目录表的表项为新申请的页面表的物理地址
			pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
			set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte)));

			if (pte != pte_offset(pmd, 0))
				BUG();
            //设置页面表的每个表项内容为对应的物理地址
            //可以看出,内核态的分页模式下的内存映射为固定的映射,
            //具体表现为页面表项的物理地址固定不变
			for (k = 0; k < PTRS_PER_PTE; pte++, k++) {
				vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE;
				if (end && (vaddr >= end))
					break;
				*pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL);
			}
		}        
	}

结束

还是省略了许多细节,所以欢迎互相交流讨论。

参考资料:

《深入理解Linux内核》

你可能感兴趣的:(Linux)