手写OS系列之内存管理
1实现位图
位图,bitmap,用于管理资源。就是使用字节中的位来映射其他单位大小的资源,一一对应的关系。
1. lib/kernel/bitmap.h:
头文件定义了位图的结构,包含一个位图的字节长度和字节指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#include "global.h"
#define BITMAP_MASK 1
struct bitmap{
uint32_t btmp_bytes_len; // the number of byte in this bitmap
uint8_t* bits; // the pointer of bitmap(unit is byte)
};
void bitmap_init(struct bitmap* btmp);
bool bitmap_scan_test(struct bitmap* btmp , uint32_t bit_idx);
int bitmap_scan(struct bitmap* btmp , uint32_t cnt);
void bitmap_set(struct bitmap* btmp , uint32_t bit_idx, int8_t value);
#endif
|
2.lib/kernel/bitmap.c:
- bitmap_init:初始化位图,使用memset将bitmap的所有字节置0(未使用)
- bitmap_scan_test:判断该位是否使用,使用return1,未使用return0
- bitmap_scan:连续申请cnt个位,申请成功返回起始bit下标,失败返回-1
- bitmap_set:将指定位设置为指定值(1或0)
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
#include "bitmap.h"
#include "stdint.h"
#include "string.h"
#include "print.h"
#include "interrupt.h"
#include "debug.h"
// initial bitmap
void bitmap_init(struct bitmap* btmp){
memset(btmp->bits, 0 , btmp->btmp_bytes_len);
}
// judge the index in bitmap is it equal to 1 ,if == 1 return true; else return false
bool bitmap_scan_test(struct bitmap* btmp , uint32_t bit_idx){
// index /8 to index the array
uint32_t byte_idx = bit_idx / 8;
// index %8 to index the bit in the array
uint32_t bit_odd = bit_idx % 8 ;
return btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd);
}
//apply for 'cnt' bit in bitmap ,if success return the index of begin bit ,else return -1
int bitmap_scan(struct bitmap* btmp , uint32_t cnt){
// get the index of byte
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;
}
//get the index of bit
int idx_bit = 0 ;
while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]){
idx_bit++;
}
// start of conserve bit
int bit_idx_start = idx_byte * 8 + idx_bit;
if (cnt == 1 ){
return bit_idx_start;
}
//remaining bit
uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start);
//current bit is remain,so count + 1 , and point to next
uint32_t bit_next = bit_idx_start + 1 ;
uint32_t count = 1;
//calculate whether have 'cnt' remain bit,if have ,return the start index
bit_idx_start = -1 ;
while(bit_left-- > 0 ) {
if (!(bitmap_scan_test(btmp,bit_next))){
// the bit of 'bit_next' is not use , so continue calculate
count ++ ;
}else {
// the bit of 'bit_next' is use ,so restart
count = 0 ;
}
if (count == cnt){
bit_idx_start = bit_next - cnt + 1 ;
break;
}
bit_next++;
}
return bit_idx_start;
}
// set the 'bit_idx' bit in the bitmap to 'value'
void bitmap_set(struct bitmap* btmp , uint32_t bit_idx , int8_t value){
ASSERT((value == 0) || (value == 1));
uint32_t byte_idx = bit_idx / 8 ;
uint32_t bit_odd = bit_idx % 8 ;
if (value){
btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
}else{
btmp->bits[byte_idx] &= (BITMAP_MASK << bit_odd);
}
}
|
2.内存管理系统
1.内存池规划:
a.物理内存池规划:
因为内核和用户进程都是运行在物理内存中,为了内核的正常运行,必须要给内核预留足够的内存,所以我们将物理内存划分成两部分,也就是分成俩个内存池,一个内核物理内存池,一个用户物理内存池。为了方便实现,这里我们将两个物理内存池大小设置为相同大小,即各占一半。
kernel/memory.c:物理内存池定义,同时定义内核物理内存池和用户物理内存池实例。
1
2
3
4
5
6
|
struct pool{
struct bitmap pool_bitmap; //to manage the physic memory
uint32_t phy_addr_start; //the start of physic memory to manage
uint32_t pool_size; //the size of pool
};
struct pool kernel_pool,user_pool;
|
b.虚拟内存池规划:
在32位环境下,虚拟内存空间可以达到4GB,由于分页机制下,每个用户进程都有自己的4GB虚拟内存空间,各个用户进程以及内核的虚拟地址可以相同(当然,物理地址肯定不相同,这是分页机制完成的),同时虚拟内存通常是连续的,但是物理内存可以连续也可以不连续。每个用户进程都应该有自己的虚拟内存池,用来映射对应的物理内存池。内核毫无疑问也是必须要有自己的虚拟内存池,即一个任务一个虚拟内存池。(注:这里我们先实现内核虚拟内存池,其他的将在以后实现用户进程时补充)。
kernel/memory.h:虚拟内存池定义
1
2
3
4
5
|
//virtual address pool
struct virtual_addr{
struct bitmap vaddr_bitmap;
uint32_t vaddr_start;
};
|
kernel/memory.c:定义了内存虚拟内存池实例
1
|
struct virtual_addr kernel_vaddr;
|
2.位图的位置:
这里需要拓展一下PCB的知识:
- 任何进程都必须包含一个PCB,PCB必须完整,单独的占用一个物理页,也就是4kb。
- PCB的最低处是包含线程或进程的信息,如pid,线程状态等,最高处是线程或进程在0特权级下使用的栈。
在loader中我们设置内核使用的栈顶是0xc009f000(对应物理地址0x9f000),因此可以推断0xc009e000是将来内核主线程的pcb。
一个页框大小为4kb,一个大小为页框的位图可表示128mb(4k X 4k X 8),所以位图位置安排在0xc009a000,这样刚好是四个页框,可以表示512mb(其实一个页框就够了),这样位图就位于低端1mb中(0x100000)之下了(可以不用位图管理自己了)。
kernel/memory.c:位图的地址
1
|
#define MEM_BITMAP_BASE 0xc009a000
|
3.初始化物理内存池:
在最初的loader.S中我们将低端1mb之上的0x100000~0x101ff用作了页目录表和页表,所以我们的虚拟内存的0xc0100000~0xc0101ff不映射这部分物理地址,其实就是将物理地址可以开始映射的部分又提高到了0x101ff。
256:一页页目录表,第0和第768个页目录项指向同一个页表算作一页,第769~1022项共254项,总共254+1+1=256
1
2
3
|
uint32_t page_table_size = PG_SIZE * 256;
uint32_t used_mem = page_table_size + 0x100000;
|
kernel/memory.c:
mem_pool_init计算未使用的所有内存,并将三个内存池的位图初始化(置0)
内核物理内存池,用户物理内存池,内核虚拟内存池,依次从0xc009a000(物理内存0x9a000)定义。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
#define K_HEAP_START 0xc0100000
//initial memory pool
static void mem_pool_init(uint32_t all_mem){
put_str(" mem_pool_init start\n");
uint32_t page_table_size = PG_SIZE * 256;
uint32_t used_mem = page_table_size + 0x100000;
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE;
uint16_t kernel_free_pages = all_free_pages /2 ;
uint16_t user_free_pages = all_free_pages - kernel_free_pages;
// a byte can express 8 pages
uint32_t kbm_length = kernel_free_pages / 8 ;
uint32_t ubm_length = user_free_pages / 8;
//kernel pool start address
uint32_t kp_start = used_mem;
//user pool start address
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;
//initial
kernel_pool.phy_addr_start = kp_start;
kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
kernel_pool.pool_bitmap.bits = (void*) MEM_BITMAP_BASE; //0x9a000
user_pool.phy_addr_start = up_start;
user_pool.pool_size = user_free_pages * PG_SIZE;
user_pool.pool_bitmap.btmp_bytes_len =ubm_length;
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length); //follow on kernel pool bitmap
/*******************print the memory pool information*****************/
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_address_start:");
put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str(" user_pool_bitmap_start:");
put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_address_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");
//set bitmap to 0
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);
/***************** initial the kernel virtual memory pool************/
//initial the bitmap of kernel virtual address
//used for maintain the kernel virtual address , so the size is equal to kernel pool
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str(" mem_pool_init done \n");
}
void mem_init(){
put_str("mem_init start\n");
uint32_t mem_bytes_total = (* (uint32_t*)(0xb00)); //set at loader.S
mem_pool_init(mem_bytes_total);
put_str("mem_init done\n");
}
|
4.分配页内存(重头戏>_<):
1.内存池标记以及页表项或页目录项属性:
kernel/memory.h:
1
2
3
4
5
6
7
8
9
10
11
12
|
//judge use which pool
enum pool_flags{
PF_KERNEL =1 ,
PF_USER = 2
};
#define PG_P_1 1
#define PG_P_0 0
#define PG_RW_R 0
#define PG_RW_W 2
#define PG_US_S 0
#define PG_US_U 4
|
kernel/memory.c:
1.获取pde和pte的索引:
开启分页机制后,高10位索引pde,中间10位索引pte。注意是索引
1
2
|
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22) //get the hight 10 bit
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12) //get the middle 10 bit
|
2.申请虚拟内存:
这里仅实现了向内核申请,用户进程部分以后补充
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//apply for 'pg_cnt' virtual page in the 'pf' virtual memory pool
//success :return the begin of address , failed: return NULL
static void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt){
int vaddr_start = 0 , bit_idx_start = -1;
uint32_t cnt = 0;
if (pf == PF_KERNEL){
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap,pg_cnt);
if (bit_idx_start == -1 ){
return NULL;
}
while (cnt < pg_cnt){
bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx_start+cnt++,1);
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}else {
// will add in future !!!!!!!!!!!!
}
return (void*) vaddr_start;
}
|
3.获取指定虚拟内存的页目录项和页表项的地址:
超级超级绕的要来了x_x:
先说正常情况:cpu处理32位的虚拟地址,先将高10位作为pde索引,找到对应页表地址,在使用中间10位,索引pte,找到物理页地址,最后用后12位直接用作偏移地址,加上刚才的物理页地址,得到最终物理地址。
最初我们设计页目录表时,设计最后一个页目录项是该页目录表的地址!
获取pte指针:
因为最后一个页目录项是页目录表的地址,所以我们用第1023项,最后一个页目录项,作为目标虚拟地址的高10位,此时地址就为0xffc00000
。cpu处理这里完成后以为找到了页表的地址,实际上是页目录表的地址。
然后cpu将使用虚拟地址的中间十位来索引pte,我们需要的是现在cpu来索引pde(因为刚才获取的是页目录表地址),所以这时就需要参数vaddr的高十位来凑目标虚拟地址的中间10位,此时地址就为0xffc00000 +( (vaddr & 0xffc00000 ) >> 10 )
。cpu这里完成后以为获取了物理页的地址,实际上是页表的地址。
最后我们需要让cpu索引pte,获取到pte(注意这里并没有寻址pte中的物理页地址,而是pte本身,所以需要偏移来获取),这是cpu会使用低12位来偏移求得目标地址,所以我们使用参数vaddr的pte部分来求偏移,因为是偏移,pte只是索引且pte大小为4byte,所以我们需要手动乘四来凑目标虚拟地址的最后10位,此时地址构成为0xffc00000 + ((vaddr & 0xffc00000 ) >> 10 ) + PTE_IDX(vaddr) * 4)
获取pde指针:
同上思路一致,这里需要套两层娃,也就是高10位和中间10位均代表最后一个页目录项。
1
2
3
4
5
6
7
8
9
10
11
|
//get the PTE pointer of vaddr;!!!!!
uint32_t* pte_ptr(uint32_t vaddr){
uint32_t* pte = (uint32_t*) (0xffc00000 + ((vaddr & 0xffc00000) >> 10) +PTE_IDX(vaddr) * 4 );
return pte;
}
//get the PDE pointer of vaddr;!!!!!
uint32_t* pde_ptr(uint32_t vaddr){
uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}
|
4.申请物理页:
kernel/memory.c:好理解
1
2
3
4
5
6
7
8
9
10
|
//get the physic page from m_pool
static void* palloc(struct pool* m_pool){
int bit_idx = bitmap_scan(&m_pool->pool_bitmap,1);
if (bit_idx == -1 ){
return NULL;
}
bitmap_set(&m_pool->pool_bitmap,bit_idx,1);
uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
return (void*)page_phyaddr;
}
|
5.虚拟地址和物理地址的映射:
kernel/memory.c:
需要注意的点是,页目录项(*pde)必须先于页表项(*pte)存在,也就是说必须要先创建好了页表,才能有对应的物理页存在,不然就会缺页中断。
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
|
//make the map of _vaddr and _page_phyaddr in page table
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);
// execute *pte must after pde had create,otherwise wile Page_Fault
if(*pde & 0x00000001){
//make sure pte is not exist
ASSERT(!(*pte & 0x00000001));
if (!(*pte & 0x00000001)){
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}else {
PANIC("pte repeat");
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
}else {
//pde wasn't exist ,so create pde first
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);
}
}
|
6.分配指定个页空间
kernel/memory.c:
分配流程:
- 从虚拟内存池中申请虚拟内存(vaddr_get)
- 从物理内存池中申请物理地址(palloc)
- 进行虚拟地址和物理地址的映射(page_table_add)
get_kernel_pages 是malloc_page的封装。
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
|
//malloc 'pg_cnt' page memory
/*********************** malloc_page ******************
* 1.vaddr_get : apply for virtual memory in virtual memory pool
* 2.palloc: apply for physic memory in physic memory pool
* 3.page_table_add: make the map
******************************************************************/
void* malloc_page(enum pool_flags pf,uint32_t pg_cnt){
ASSERT(pg_cnt > 0 && pg_cnt < 3840); //here set max is 15*1024*1024 /4096 = 3840 page
void * vaddr_start = vaddr_get(pf,pg_cnt);
if (vaddr_start == NULL ){
return NULL;
}
uint32_t vaddr = (uint32_t)vaddr_start , cnt = pg_cnt;
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
//physic memory isn't continuous
while (cnt-- > 0){
void * page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL){
//physic memory will roll back , will write in future
return NULL;
}
page_table_add((void*)vaddr,page_phyaddr);
vaddr += PG_SIZE;
}
return vaddr_start;
}
//apply for 'pg_cnt' page in kernel physic memory pool
void* get_kernel_pages(uint32_t pg_cnt){
void* vaddr = malloc_page(PF_KERNEL,pg_cnt);
if (vaddr != NULL){
memset(vaddr , 0 , pg_cnt * PG_SIZE);
}
return vaddr;
}
|
3.总结
虚拟空间巧调度,进程安然各运行。
物理资源精分配,内存高效无忧愁。