28.我居然会认为权威书籍写错了...

28.我居然会认为权威书籍写错了...

在 Linux 0.11 的设计中,进程 0 创建进程 1 时,复制了 160 个页表项。进程 1 创建进程 2 时,复制了 1024 个页表项。

之后进程 2 创建进程 3,进程 3 创建进程 4,通通都是复制 1024 个页表项。

图片

《Linux 内核设计的艺术》一书中就是这样描述的。

图片

《Linux 内核完全注释》一书中也是这么描述的。

图片

看源代码,也能很直观地看到这两个数字。

1
2
3
4
5
6
7
8
int copy_page_tables(unsigned long from,unsigned long to,long size) {        
    ...        
    nr = (from==0)?0xA0:1024;              
    for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {                
        ...        
    }        
    ...
}

0xA0 用十进制表示就是 160。

没什么好怀疑的,但我今天用 bochs 想调试一下证明这个事情,结果却和我预期的有点不符。

断点打在刚刚开启分页机制的时候,看一下页目录表信息,有四个页目录项,符合预期。

图片

继续将断点打在进程 0 创建出进程 1 之后,看一下页目录表,发现多出一项。

图片

符合预期,因为进程 0 创建的进程 1,需要复制进程 0 的页表,而进程 1 的线性地址空间是 64M,所以自然在第 17 个页目录项的位置新增了一个。(一个页目录项可以管理 4M 内存空间)

再看看开篇的图,看不懂也不要紧。注意进程 1 在线性地址空间中的起始位置。

图片

页目录项里的数据就表示页表地址,刚刚新增的页目录项的值是 0x00ffe007,那我们去这里看看是不是复制了 160 个页表项。

图片

有问题了,这个黄色的框里面一共是 160 项,但最后两个是 0,也就是一共才复制了 158 项。

这咋回事,难道书上都说错了?

就为这个事,我又重新启动,调试了好几次,debug 断点也改了好多地方,因为我怀疑是不是复制页表的代码还没有执行完,刚好少了两个。

但无论咋试,全都是精准的 158 项,不多不少。

我又改了下源码,把原来的 160 项改成了 4 项,看看会有啥结果。

1
2
3
4
5
6
7
8
int copy_page_tables(unsigned long from,unsigned long to,long size) {        
    ...        
    nr = (from==0)? 4: 1024;              
    for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {                
        ...        
    }        
    ...
}

结果再次调试发现,一共只复制了 2 项页表!还是少了俩!

我又怀疑是不是因为触发了写时复制,页表项被改到了别的位置?但我怎么看源码,都没看到复制页表的那段代码之后,有什么操作可以导致写时复制。

于是乎,我这时竟然产生了,所有 Linux 0.11 的书上这块都写错了的自信!还发到了我的操作系统催更群里求证。

但就过了几秒钟,我一拍脑门,想起了问题所在。

就是个非常二逼且简单的问题,页目录项中记录的,不仅仅是页表地址... 它的结构是这样的。

图片

所以刚刚的页表项 0x00ffe007 换成二进制是

00000000111111111110000000000111

对照结构分析出,页表地址为 0x00ffe000,存在位 P 为 1,读写位 RW 为 1,用户态内核态位 US 为 1。

所以,看页表地址,不应该是 0x00ffe007,而应该是 0x00ffe000。

就这么简单个事,我画页表结构都画了无数遍了,居然实际调试的时候还是直接把页目录项的值当成页表地址...

别说了,再次验证下吧。

图片

看,这回妥妥的 160 项了,终于可以睡个好觉了!

而且注意到,这里的页表都是只读状态,比如第一个页表项 0x00000065 换成二进制是

00000000000000000000000001100101

对照上面的页表结构发现,RW 位是 0,不再是原来的 1 了,说明从可读写,变成了只读状态。这就为之后的写时复制做了准备。

源码中很简单,就是强行把新复制的页表的 RW 位置 0,毫无神秘感。

1
2
3
4
5
6
7
8
9
int copy_page_tables(unsigned long from,unsigned long to,long size) {        
    ...            
    for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {                
        ...                
        this_page &= ~2;                
        ...        
    }        
    ...
}

这里不展开了。

本篇文章就是想说,即便是再觉得自己已经熟悉的事情,也会有脑残的时候。但这也恰恰证明了自己还不够熟悉,仅仅是记住了页表和页目录表的结构,还没有在实践中真正“玩”过它们。

另外大家遇到难啃的骨头,奇怪的问题时,也不要害怕,在源码面前一切秘密都不存在。不存在魔幻的事情,要么是你的操作有问题,要么是源码有问题,大胆去证明,去折腾,就好了。

updatedupdated2024-08-252024-08-25