《操作系统真象还原》chapter8 笔记与警醒

First Post:

Last Update:


    读这章遇到的最大的问题就是:“我意会错了这一章想给我讲什么”,以至于整章读完十分困惑,一开始的问题没能解决以至于错过了很多东西,最终效果不是很好……

    现在来重新梳理一下本章的内容究竟是在讲什么,解决什么问题。


PART 1

    我将makefile、ASSERT、字符串操作,以及位图操作四个小节划分为第一部分,最后一节作为第二部分。

    第一部分的内容不多,只涉及一些操作的实现,并没有具体到“欲解决的问题”。因此只需要了解其实现的原理,在以后需要自己手动实现的时候回顾即可。笔者以为不需要对这部分做过多的记录。

    不过位图操作的概念和Part 2有一定的联系,因此在这里也需要再提几句。

    位图(bitmap)的概念即将”bit位同一个具体事物间建立映射关系,用0和1标识事物的两个状态”。具体到之后的内存管理就是:

以后的内存分配将以“内存页”为基本单位进行分配。建立位图和整块内存的映射关系,用1标识该内存页已被分配,用0标识该内存页未被分配。

建立完成以后,从此便只需要扫描位图中的每个bit就能够得知内存中哪个内存页可用,方便以后进行内存分配。

1
2
3
4
5
struct bitmap {
uint32_t btmp_bytes_len;
uint8_t* bits;
};
//位图是储存在内存里的,在平坦模式下的位图只需要一个指针加上标识位图长度的flag足矣。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int bitmap_scan(struct bitmap* btmp, uint32_t cnt) {
uint32_t idx_byte = 0;
while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) {
idx_byte++;
}

ASSERT(idx_byte < btmp->btmp_bytes_len);
if (idx_byte == btmp->btmp_bytes_len) {
return -1;
}
int idx_bit = 0;
while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]) {
idx_bit++;
}
int bit_idx_start = idx_byte * 8 + idx_bit;
if (cnt == 1) {
return bit_idx_start;
}

uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); // 记录还有多少位可以判断
uint32_t next_bit = bit_idx_start + 1;
uint32_t count = 1;

bit_idx_start = -1;
while (bit_left-- > 0) {
if (!(bitmap_scan_test(btmp, next_bit))) {
count++;
} else {
count = 0;
}
if (count == cnt) {
bit_idx_start = next_bit - cnt + 1;
break;
}
next_bit++;
}
return bit_idx_start;
}

PART 2

    节名“内存管理系统”,但本节所指的内存是“物理内存”,本节管理的对象也是“物理内存”,而不是“虚拟内存”,但因为分页机制已经启用,所以本节所用的地址却是”虚拟地址“。但本节似乎过早的介绍了多进程中”每个进程独享4G地址空间“的概念,以至于我一直以为它接下来会实现这个功能,但结论并非如此,所以我算是扑空了。

    本节的逻辑是这样的:

    首先为了区分虚拟地址和物理空间,建立了”虚拟地址池“和”物理内存池“。同时,我们将整个物理内存分为”用户物理内存池“和”内核物理内存池“。

    此处”建立“一次的过程包括:界定内存池基址、位图清零两个过程。

    然后是构建分配机制。

  • 从虚拟地址池分配内存页
  • 从物理内存池分配内存页
  • 建立虚拟地址和物理地址的映射关系

    但本节只涉及到了分配,却没用对应的归还操作。也不知道之后几章会不会涉及。

    同时需要注意到,本节并未给内核建立独立的页表。我此前一直抱有”不同进程有着相同的虚拟地址“这一问题,也知道这需要通过切换页表来实现,但本节并未实现这个功能,它只是为内核添加了分配内存的能力罢了。具体看如下内容。

    笔者认为最后一步是最难理解也最重要的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void page_table_add(void* _vaddr, void* _page_phyaddr) {
uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr;
uint32_t* pde = pde_ptr(vaddr);
uint32_t* pte = pte_ptr(vaddr);
if (*pde & 0x00000001) { // 页目录项和页表项的第0位为P,此处判断目录项是否存在
ASSERT(!(*pte & 0x00000001));
if (!(*pte & 0x00000001)) {
*pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1
} else {
PANIC("pte repeat");
*pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1
}
} else { // 页目录项不存在,所以要先创建页目录再创建页表项.
uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
*pde = (pde_phyaddr PG_US_U PG_RW_W PG_P_1);
memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);
ASSERT(!(*pte & 0x00000001));
*pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1
}
}

    虚拟地址是根据位图进行分配的,如果每个进程都存在一个位图的话,这里就可能出现相同的虚拟地址,但page_table_add函数是根据虚拟地址来获取pde和pte的,则相同的虚拟地址必然会出现冲突。因此本节并不是在解决这个问题,上面的函数实际实现的是平坦模式下单任务系统的内存分配,也就是在虚拟地址不会出现重复的情况下,为用户和内核分配内存以供其能够动态调整内存的使用。所以这里的”虚拟地址“是 ”无物理地址直接映射的虚拟地址“,而不是 ”虚拟内存空间中的虚拟地址“。理解这一点以后,本节就应该没有其他问题了。

    page_table_add的逻辑是:

  • 通过虚拟地址得到此地址会被换算到的PDE和PTE
  • 如果页目录已经有对应的页表,那么直接把页表项填入物理地址即可建立映射
  • 如果页目录本项还未映射到具体的页表,那就申请一块内存页作为新的页表把它写在此PDE处,然后在新页表出写入物理地址

        不过读的时候还在好奇为什么能用pte去当memset的参数,其实只需要记住pte是一个虚拟地址,是算出来的,在传入memset的时候还会在MMU中重新计算即可。


PART 3

总结:

    本节最终实现的是一个简化的平坦模式下内存分配器。建立的也只是平坦模式下的虚拟地址和物理地址间的映射管理。并未涉及 ”虚拟内存“ 的概,所有地址都应该保证不重复,否则会像double free那样出问题(此处会直接kernel panic)

    同时,我们用的是”虚拟地址“,只需要记住我们用到的地址大多都是虚拟地址即可,就不容易出错了。

琐碎:

    可能是因为这几天状态很糟糕,每天都处于严重的睡眠不足的情况导致的(春节期间的麻烦太多了),脑子在看书的时候很难集中注意力,以至于会意错了作者的意图……

插画ID:77309888