读这章遇到的最大的问题就是:“我意会错了这一章想给我讲什么”,以至于整章读完十分困惑,一开始的问题没能解决以至于错过了很多东西,最终效果不是很好……
现在来重新梳理一下本章的内容究竟是在讲什么,解决什么问题。
PART 1
我将makefile、ASSERT、字符串操作,以及位图操作四个小节划分为第一部分,最后一节作为第二部分。
第一部分的内容不多,只涉及一些操作的实现,并没有具体到“欲解决的问题”。因此只需要了解其实现的原理,在以后需要自己手动实现的时候回顾即可。笔者以为不需要对这部分做过多的记录。
不过位图操作的概念和Part 2有一定的联系,因此在这里也需要再提几句。
位图(bitmap)的概念即将"bit位同一个具体事物间建立映射关系,用0和1标识事物的两个状态"。具体到之后的内存管理就是:
以后的内存分配将以“内存页”为基本单位进行分配。建立位图和整块内存的映射关系,用1标识该内存页已被分配,用0标识该内存页未被分配。
建立完成以后,从此便只需要扫描位图中的每个bit就能够得知内存中哪个内存页可用,方便以后进行内存分配。
struct bitmap {
uint32_t btmp_bytes_len;
uint8_t* bits;
};
//位图是储存在内存里的,在平坦模式下的位图只需要一个指针加上标识位图长度的flag足矣。
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地址空间“的概念,以至于我一直以为它接下来会实现这个功能,但结论并非如此,所以我算是扑空了。
本节的逻辑是这样的:
首先为了区分虚拟地址和物理空间,建立了”虚拟地址池“和”物理内存池“。同时,我们将整个物理内存分为”用户物理内存池“和”内核物理内存池“。
此处”建立“一次的过程包括:界定内存池基址、位图清零两个过程。
然后是构建分配机制。
- 从虚拟地址池分配内存页
- 从物理内存池分配内存页
- 建立虚拟地址和物理地址的映射关系
但本节只涉及到了分配,却没用对应的归还操作。也不知道之后几章会不会涉及。
同时需要注意到,本节并未给内核建立独立的页表。我此前一直抱有”不同进程有着相同的虚拟地址“这一问题,也知道这需要通过切换页表来实现,但本节并未实现这个功能,它只是为内核添加了分配内存的能力罢了。具体看如下内容。
笔者认为最后一步是最难理解也最重要的。代码如下:
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
Comments | NOTHING