内核初始化内存
内核需要建立一个映射来制定那些物理地址可用,那些不可用(比如对应了MMAP-IO,或者BIOS数据)
保留状态下的页不能被交换到磁盘上。比如不可用的物理地址,以及内核的数据或者代码
第0个页由BIOS使用,用于存放加电自检期间检查到的硬件数据
在启动过程的早期阶段,内核询问BIOS并了解物理地址的大小。
之后内核执行machine_specific_memory_setup
,建立物理地址映射
他会根据BIOS给出的数据映射一些保留的页。并且分析物理内存区域表来初始化一些变量来描述内核的物理内存布局
比如可用的page frame数量,被直接映射的最后一个page frame的编号等(说明后面的未分配可用页不是直接映射)
初始情况下的地址分配
进程页表
从0x00000000到0xbfffffff的地址,无论是在内核态还是在用户态都可以寻址(可能是用来存储用户进程)
从0xc0000000到0xffffffff的地址,则是内核态才能寻址。
内核开始的空间就是0xc0000000
内核页表
内核维护着自己使用的页表,驻留在master kernel page global directory中
在内核刚刚被装载进内存的时候,分页功能并没有被启用。内核会在两个阶段初始化自己的页表
第一阶段内核会创建一个有限的地址空间,包括了kernel的代码和数据段,初始的页表,以及128KB的空间用来给一些动态的数据结构用。第一阶段的小空间只是为了去加载内核以及初始化一些核心的数据结构
第二阶段内核会利用剩余的RAM,并且建立页表
临时页表是在内核编译的时候就初始化了的。对应的临时的页全局目录存在swapper_pg_dir
中。他们存储在内核未初始化的段后面(一个疑问是为什么被静态初始化的页表是在未初始化的段后面的?可能是存在bss段作为全0?)
这里有一个假设是我们第一阶段的这些数据不超过8MB(多了也没关系,类推即可)
我们希望这8MB的地址可以在实模式以及保护模式下都可以进行寻址(这里我的理解就是当我们安装页表的时候mmu开始paging,这时候我们应该还可以继续正常的执行代码,在xv6中,他们是通过恒等映射来完成的)。
注意到我们上面提到了内核在虚拟地址中是装在到0xc0000000的,但是在物理地址中,他是在0x100000的,所以我们希望在安装页表的时候不会出现断裂。
那么我们就需要让0xc0000000到0xc07fffff这段地址也映射到开始的0x00000000到0x007fffff中。
我猜测应该是前面初始化的这段代码在编译的时候是0x100000开始的,然后在之后的某一刻的地址会跳到0xc0上面,但是不清楚是怎么实现的
之后在代码中,内核会调用kernel_physical_mapping_init
,用来初始化内核的页表
他会把max_low_pfn
下的所有地址都都映射到0xc0之上
感谢这篇文章的讲解,我终于明白了linux boot时候的内存是怎么处理的了。
32位内核的入口点在head.S
中的startup_32
中。内核的具体地址都是在0xc0之上,也就是第四个GB上。但是我们加载内核的物理地址是在第一个mb上。
这个是vmlinux.lds.S中的链接脚本,可以看到我们是把.text放到了低地址中。这个.text对应的就是我们的startup_32
,这段代码在我们加载内核的物理地址中。注意到他初始化的那些数据之后我们是要在内核中用的,因为那些数据都加载到了高地址。所以在startup_32
中使用这些数据的时候,我们都会手动减去一个PAGE_OFFSET
,这样在里面初始化好页表以后,就可以进入到虚拟地址中了。
然后我们进入到正常C语言编写的内核中,初始化完整的页表,并最后重新加载。这时候才完全进入了内核的地址空间。
而书中说的所谓896MB是因为我们的内核在第四个GB中,只用了4GB中的1GB,而后128MB的虚拟空间是保留的,比如固定映射。所以我们能用的只有896MB。所以当物理内存大于内核的虚拟地址空间的时候,我们就需要动态的重映射这些地址。否则的话就可以直接把整个物理地址映射到内核中直接管理
固定映射的地址
可以看到,内核的虚拟地址的第四个GB(0xc0)负责映射系统的物理内存。
在内核中有另一种地址叫做固定映射的地址,他是一种常量地址,他的物理地址不必等于线性地址减去0xc000000。他们存在于地址空间的第四个GB的后128MB中
他们的作用是作为内核中的常量指针区。因为如果我们正常使用指针的时候,需要先找到指针,再去解引用。并且我们还需要去检查指针变量的有效性。而在固定映射的地址中,他们作为常量指针不需要额外的一次内存访问,同时不需要检查有效性。
TLB优化
就说一下这个lazy的思想。因为kernel thread不会访问user space,但是kernel thread没有自己的页表,他用的就是普通进程的页表。所以有时候其他的CPU修改了这个进程的用户地址空间并且请求刷新。这时候内核线程可以忽略这部分,因为内核线程不会去访问用户地址空间。
文章评论