八. 内存池规划

bitmap简介

计算机中一些资源的数量非常庞大,比如内存容量和硬盘容量。为了使用这些资源,必须涉及到一套管理这些资源的方法,而方法中必须要构建数据结构来存储管理数据,这些数据本身也是需要占用内存的

计算机中最小的单位是位,那么用一组二进制位串来管理其他单位大小的资源是很自然的,这组二进制位中的每一位与其他资源的数据单位是一一对应的关系,这实际是一种映射关系,于是这组二进制位便有了一个名字—-位图

位图与内存的映射关系如下图所示

位图与内存的映射关系

图中的一个黑点代表计算机中的1bit,这1bit就会映射内存中4kb的空间,也就是1页的空间,这1bit有两个值,0或1。1代表这一页的内存已经被分配出去。

通过这种管理方式可以知道

1byte 的位图空间可以映射 1 * 8 * 4KB = 32KB 的内存空间
那么 一页4096Bbyte 的位图空间就可以映射 32 * 4096 / 1024 = 128MB的内存空间

位图的结构还是比较简单的,下面来看看位图具体的实现

bitmap实现

1
2
3
4
5
struct bitmap
{
uint32_t btmp_bytes_len;
uint8_t *bits;
};

位图的定义中,btmp_bytes_len用来记录位图所占字节的大小。bits用于保存位图的起始地址,bits的类型是 uint8_t* 此类型强调的是字节型指针,主要是让其以 1字节的单位进行偏移,这样可以简化对 字节 的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define BITMAP_MASK 1

// 初始化位图,将位图中的数据清0
void bitmap_init(struct bitmap *btmp)
{
memset(btmp->bits, 0, btmp->btmp_bytes_len);
}

// 判断位图中的第bit_idx位是否为1, 如果是,返回true
bool bitmap_scan_test(struct bitmap *btmp, uint32_t bit_idx)
{
uint32_t byte_idx = bit_idx / 8; // 位图中bit_idx所在的byte索引
uint32_t bit_odd = bit_idx % 8; // 该bit在 1byte中的偏移量

return btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd);
}

bitmap_scan_test函数用来对判断bit_idx位是否为1, bit_idx是相对于位图起始地址的 bit偏移量

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
// 在位图中申请连续的cnt位个空间,成功返回在位图中的bit偏移量
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))
{// 判断该字节是否全部为1, 且位图大小必须 小于 要申请的空间
// 代表该字节已全部占用,继续向下字节查找
++idx_byte;
}

ASSERT(idx_byte < btmp->btmp_bytes_len);

// 内存池中找不到可用空间
if (idx_byte == btmp->btmp_bytes_len)
{
return -1;
}

// 在idx_byte中有空闲位后,对该byte进行逐bit比对,直到有连续的bits=cnt
int idx_bit = 0;
while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte])
{// 找到idx_byte中为0的bit所在的位置
++idx_bit;
}

int bit_idx_start = idx_byte * 8 + idx_bit; // 空闲位在位图中的bit偏移量

if(cnt == 1)
{
return bit_idx_start;
}

// 位图中还剩余的bits数
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;
// 在剩余的空间中继续查找,直到有连续的bits=cnt
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;
}

++next_bit;
}
return bit_idx_start;
}

// 将位图中的bit_idx位 设置为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);
}
}

bitmap_scan 会在位图中找连续cnt位个空间,该空间必须是连续的,如果成功找到,会返回该空间起始位置相对于位图起始地址的 bit偏移量

位图的结构有了之后,就要通过它对内存进行管理,在这里,先对物理内存进行划分。

内存池规划

内核和用户进程都是要运行在物理内存之上的,操作系统为了正常运行,不能用户进程申请多少内存,就分配多少内存,否则可能导致物理内存不足,导致内核自己都无法正常运行。

基于这个原因,将物理内存划分成两部分,一为用户物理内存池,此部分的物理内存专门用于分配给用户进程。二为内核物理内存池,此内存池只供给操作系统使用

内存分配策略

在分页机制下,程序中的地址都是虚拟地址,在32位环境下,虚拟地址空间为4GB。且每个任务都有自己独立的4GB虚拟地址空间,各个程序之间的虚拟地址可以相同,不仅用户进程是这样,内核同样如此。

程序在动态申请内存的过程中,操作系统会为进程或者内核自己在堆中选择一空闲的虚拟地址,并且找个空闲的物理地址作为此虚拟地址的映射。之后将该虚拟地址返回给用户进程。

为了找出空闲的虚拟地址,就必须对虚拟地址也进行管理,需要为所有任务都维护一个虚拟内存地址池。

对内核来说也不例外,虽然内核在需要空间时可以即拿即用,但是通过这些统一的管理方式使得内核不显得如此混乱,结构性好。

内核在申请空间的时候,先从内核自己的虚拟地址池中分配好虚拟地址再从内核物理地址池中分配物理内存,最后在内核自己的页表中将这两种地址建立好映射关系,内存就分配完成

对用户进程来说,它向操作系统申请内存时,操作系统先从用户进程自己的虚拟地址分配虚拟地址,在从用户物理内存池中分配空闲的物理内存,用户物理内存池是被所有用户进程所共享的。最后在用户进程自己的页表中将这两种地址建立好映射关系

虚拟内存池与物理内存池

内存池的初始化

虚拟内存池和物理内存池的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct virtual_addr
{
struct bitmap vaddr_bitmap;
uint32_t vaddr_start;
};

struct pool
{
struct bitmap pool_bitmap; // 内存池的位图结构
uint32_t phy_addr_start;
uint32_t pool_size;
};

struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构用来给内核分配虚拟地址

在前面创建页目录和页表的时候,我们将虚拟地址0xc0000000~0xc00fffff映射到了物理地址0x0~0xfffff,0xc0000000是内核空间的起始虚拟地址,这1MB空间做的对等映射。所以内核堆空间的开始地址从0xc0100000开始

在之前的设计中,0xc009f000为内核主线程的栈顶,0xc009e000将作为主线程的pcb使用,那么在低端1MB的空间中,就只剩下0xc009a000~0xc009dfff这4 * 4KB的空间未使用,所以位图的地址就安排在0xc009a000处,这里还剩下四个页框的大小,所能表示的内存大小为512MB

1
2
#define MEM_BITMAP_BASE 0xc009a000
#define K_HEAP_START 0xc0100000

在内存池的初始化中,只需要根据物理内存的总大小,内核物理内存池和用户物理内存池平均分割物理内存

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
// 初始化内存池
static void mem_pool_init(uint32_t all_mem)
{
put_str(" mem_pool_init start\n");

// 目前只初始化了低端1MB的内存页表,也就是256个页表
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;

// 内核的位图大小,在位图中,1bit表示1页
uint32_t kbm_length = kernel_free_pages / 8;
uint32_t ubm_length = user_free_pages / 8;

// 内核内存池的起始地址
uint32_t kp_start = used_mem;

// 用户内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;

kernel_pool.phy_addr_start = kp_start;
user_pool.phy_addr_start = up_start;

kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
user_pool.pool_size = user_free_pages * PG_SIZE;

kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;

kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);

// 输出内存信息
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_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_addr_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");

// 将位图置0
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);

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");

// 物理内存的大小放在地址0xb00处
uint32_t mem_bytes_total = *((uint32_t*)0xb00);

mem_pool_init(mem_bytes_total);

put_str("mem_init done\n");
}