XV6 main函数中的页表代码详解

#if和#ifndef用法

借鉴:

C语言#if、##ifdef、#ifndef的用法详解,C语言条件编译详解 (biancheng.net)

在xv6的kernel/main.c中出现了这种用法,在其他多个.c文件里也出现了

为何要用这种用法:

假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?

这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出不同的平台。

Windows 有专有的宏_WIN32,Linux 有专有的宏__linux__,以现有的知识,我们很容易就想到了 if else,请看下面的代码:

#include <stdio.h>
int main(){
    if(_WIN32){
        system("color 0c");
        printf("http://c.biancheng.net\n");
    }else if(__linux__){
        printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
    }else{
        printf("http://c.biancheng.net\n");
    }
    return 0;
}

但这段代码是错误的,在 Windows 下提示 linux 是未定义的标识符,在 Linux 下提示 _Win32 是未定义的标识符。对上面的代码进行改进:

#include <stdio.h>
int main(){
    #if _WIN32
        system("color 0c");
        printf("http://c.biancheng.net\n");
    #elif __linux__
        printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
    #else
        printf("http://c.biancheng.net\n");
    #endif
    return 0;
}

这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。

#if 用法的一般格式为:它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。

#ifdef 用法的一般格式为如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。

#ifndef 用法的一般格式为:与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。

最后需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。

宝图

一直用到这个图,所以取名宝图。

初始化freelist代码

这里的内容都看宝图中的右图。

首先在kernel/kernel开启debug,在main.c的kinit()处打断点:

step into kinit(),进入kalloc.c:

跳过initlock()(看代码应该就是初始化锁的功能,和页表没啥关系),step into第二个函数,还是在kalloc.c中:

PGROUNDUP() :

这个函数的功能是向上对齐页大小,xv6中一页是4kb,也就是0x1000。假设现在pa_start是0x1020,为了保证每一页都是同等大小,就需要对齐。而这个函数就是用来这样做的,PGROUNDUP(pa_start) 会对齐为 0x2000。

pa_start && pa_end :

接着我们看一下freerange的参数。首先我们应该知道freerange这个函数的功能是把当前所有物理界面给分成一页一页,加入到 freelist中。

pa_start在kinit()中传入的是end,可以看到end的定义是: extern外部定义,在kernel.ld中定义。

转到kernel.ld查看:可以看到这里巴拉巴拉处理了一堆东西,存了又存。可以看宝图的0x80000000后面的kernel text部分。

所以,通过上面的kernel.ld我们可以看到,pa_start(也就是end)应该是大于0x80000000的,这也跟宝图对应上。我们查看gdb的pa_start地址,可以看到为:pa_start:0x80027020 。 也就是初始化上面那些东西用了27页多

然后再回到freerange的代码: 因为pa_start大小不一定是整数页,所以需要对齐,通过gdb我们可以看到 p = 0x80028000。进入for循环,再通过gdb可以看到 pa_end = 0x88000000。其实这个pa_end就是宝图的PHYSTOP(虽然比他小,但就是这个),这也说明了xv6其实只有128m的内存可以使用。

for循环就是从 p 开始,每次增长一页,一直到PHYSTOP-PAGESIZE(也就是宝图的freememory底部到顶部)。

然后转到kfree()查看:

这就是整个初始化freelist的代码,结束完for的时候,我们的physical page就分配结束了。此时freelist的首页就是0x87fff000。

kvminit()函数

结束完kinit()就进入了kvminit()函数:

kvminit(),这个函数会设置好kernel的地址空间。kvminit的代码(在kernel/vm.c中):

kvmmap设置了虚拟地址va到物理地址pa的等价映射,看宝图的KernelBase以下部分

然后我们看kalloc():

打一下kalloc()的地址可以发现他是在0x80000b20,因为我们刚刚在kernel.ld中把 0x8000000到0x80027020的地址给用了,所以kalloc()也已经被加载了。

通过解读freelist那里我们已经知道,kmem的freelist里面存着所有空闲的物理地址,且都是一页一页的。所以kalloc要做的事就是检查是否还有内存,如果 r !=NULL说明还有页数,就可以把第一页分配给它。

通过gdb我们可以看到 kernel_pagetable的地址是0x87fff000,也就是freelist的第一页。

然后再看一下kvmmap():

mappages()的功能就是把va映射到pa,添加到页表中,还有赋予权限之类的:

配合这个图会好理解很多,就是返回下图的第三级页表对应的pte。alloc=1代表如果不存在对应的page directory就分配一页给它。

kvminithart()函数

回到main,结束完kvminit()之后,就来到了kvminithart()函数:

首先就是w_satp(),这个函数使用了c语言的内嵌汇编,将kernel_pagetable的地址写入了satp寄存器,然后通过sfence_vma() flush TLB。每次更新satp寄存器都应该出现刷新TLB,这样才不会读到旧数据导致出错。

procinit()函数

进入procinit():通过注释可以看到这个函数的功能应该是:在启动时初始化进程表。理解procinit()的功能对lab3也有帮助。

Last updated