操作系统折腾记(二)

今天室友问了我一个有趣的问题。我们正在做 mit 的 xv6 2020 版实验,他写了一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "kernel/types.h"
#include "user/user.h"

int main(){
int bar = 0;
if(fork() == 0){
// child
bar = 1;
printf("Child: bar=%d, va=%p\n", bar, &bar);
exit(0);
}
else{
wait(0);
printf("Parent: bar=%d, va=%p\n", bar, &bar);
}
exit(0);
}

不妨设这个用户程序叫做 vapa 吧,编译、执行,得到结果:

1
2
Child:  bar=1, va=0x0000000000002FCC
Parent: bar=0, va=0x0000000000002FCC

Child 和 Parent 的 bar 变量值不同,这符合我们的预期——fork 的时候子进程复制父进程的变量值,之后对变量的修改是独立的。但是问题是——为什么两个 bar 的地址相同呢?

刚做了(其实还没完成)mit xv6 lab3 的我立刻产生了一个猜想——这个地址只是虚拟地址,fork 的时候,子进程会复制父进程的页表,修改变量的时候,只会更改它的物理地址,而不会(也没有必要)修改虚拟地址。

猜想需要验证才能成为真理,我试图在 vapa.c 里调用 walkaddr 获取物理地址,但是遇到了一堆麻烦——也是,访问物理地址这种事情,怎么会允许用户态的程序干呢。看来只能写一个系统调用 getpa 了——getpa 获取一个虚拟地址,返回物理地址(当然是在调用它的进程的页表中找)。

做过 mit xv6 lab2,所以写系统调用也不是什么难事:

  1. 首先在 user/user.h、user/usys.pl、kernel/syscall.h、kernel/syscall.c 中参照其他系统调用补上 getpa

  2. 然后在 kernel/sysproc.c 中实现一个 sys_getpa,由于内核在 kernel/vm.c 中已经为我们实现好了虚拟地址转物理地址的底层代码 walkaddr,所以直接调用之:

    1
    2
    3
    4
    5
    6
    7
    8
    uint64
    sys_getpa(void)
    {
    uint64 va;
    if(argaddr(0, &va) < 0)
    return -1;
    return walkaddr(myproc()->pagetable, va);
    }

如果我没记错的话,现在已经万事俱备了,于是我们用 getpavapa.c 中把物理地址一并输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "kernel/types.h"
#include "user/user.h"

int main(){
int bar = 0;
if(fork() == 0){
// child
bar = 1;
printf("Child: bar=%d, va=%p, pa=%p\n", bar, &bar, getpa((uint64)&bar));
exit(0);
}
else{
wait(0);
printf("Parent: bar=%d, va=%p, pa=%p\n", bar, &bar, getpa((uint64)&bar));
}
exit(0);
}

编译执行,得到结果:

1
2
Child:  bar=1, va=0x0000000000002FBC, pa=0x0000000087F4C000
Parent: bar=0, va=0x0000000000002FBC, pa=0x0000000087F41000

哈哈!物理地址果然变了,猜想正确!


操作系统折腾记(二)
https://xyfjason.github.io/blog-main/2021/10/06/操作系统折腾记(二)/
作者
xyfJASON
发布于
2021年10月6日
许可协议