实现位图
#define BITMAP_MASK 1
struct bitmap
{
uint32_t btmp_bytes_len; // 位图字节长度(注意是字节长度)
uint8_t* bits; // 位图的起始地址(字节)
};
void bitmap_init(struct bitmap* btmp);
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx);
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value);
int bitmap_scan(struct bitmap* btmp, uint32_t cnt);
- 位图初始化
void bitmap_init(struct bitmap *btmp)
{
memset(btmp->bits, 0, btmp->btmp_bytes_len);
}
- 判断bit_idx位是否为1,若为1则返回true,否则返回false
bool bitmap_scan_test(struct bitmap *btmp, uint32_t bit_idx)
{
uint32_t byte_index = bit_idx / 8;
uint32_t bit_odd = bit_idx % 8;
return (btmp->bits[byte_index] & (BITMAP_MASK << bit_odd));
}
- 将位图btmp的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) // value为0x1
btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
else // value为0x0
btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);
}
- 在位图中申请连续 cnt个位,成功则返回其起始位下标,失败返回-1
int bitmap_scan(struct bitmap *btmp, uint32_t cnt)
{
uint32_t idx_byte = 0;
// 0xff 表示该位已被分配
while((0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len))
idx_byte++;
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; // 先将其置为-1,若找不到连续的位就直接返回
while (bit_left-- > 0)
{
if (!(bitmap_scan_test(btmp, next_bit))) // 若next_bit为0
count++;
else
count = 0;
if (count == cnt) // 若找到连续的cnt个空位
{
bit_idx_start = next_bit - cnt + 1;
break;
}
next_bit++;
}
return bit_idx_start;
}
内存池规划
在实模式下,程序中的地址就等于物理地址,在保护模式下,程序地址变成了虚拟地址。虚拟地址转换为物理地址由分页机制完成。因此,OS有责任把这两种地址分别管理,并通过页表将这两类地址关联。
先讨论下如何规划物理内存:
一种方案是将物理内存划分成两部分,一部分只用来运行内核,另一部分只用来运行用户进程,所以,将其分为内核物理内存池,用户物理内存池。
再讨论虚拟地址内存:
分页机制一个好处就是每个任务都有自己的 4GB 虚拟地址空间,各程序中的虚拟地址可以相同,只需要映射的物理地址不一样就可以。
#define MEM_BITMAP_BASE 0xc009a000
#define K_HEAP_START 0xc0100000 // 内核进程堆的起始地址
struct pool
{
struct bitmap pool_bitmap; // 本内存池用到的位图,用于管理物理内存
uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
uint32_t pool_size; // 本内存池容量
};
struct pool kernel_pool; // 内核物理内存池
struct pool user_pool; // 用户物理内存池
struct virtual_addr
{
struct bitmap vaddr_bitmap; // 虚拟地址池用到的位图
uint32_t vaddr_start; // 虚拟地址池起始地址
};
struct virtual_addr kernel_vaddr; // 内核虚拟地址池
- mem_pool_init 初始化内存池信息
void mem_pool_init(uint32_t all_mem) // all_mem 表示物理内存总大小,这里32MB { uint32_t page_table_size = PG_SIZE * 256; // 1个页目录表 + 0和768页目录项指向同一个页表,+ 769~1023=254个页表=256 uint32_t used_mem = 0x100000 + page_table_size; // 0x100000, 1MB低端内存, 用于OS uint32_t free_mem = all_mem - used_mem; // 32MB-2MB = 30MB uint16_t all_free_pages = free_mem / PG_SIZE; // 30MB/4K = 7680 uint16_t kernel_free_pages = all_free_pages / 2; // 7680/2 = 3840 uint16_t user_free_pages = all_free_pages - kernel_free_pages; // 7680-3840 = 3840 uint32_t kbm_length = kernel_free_pages / 8; // kernel 内存池中位图长度,位图中每一位表示一个页框 3840/8 = 480 uint32_t ubm_length = user_free_pages / 8; // user 内存池中位图长度,位图中每一位表示一个页框 3840/8 = 480 uint32_t kp_start = used_mem; // kernel内存池能用的内存起始地址 uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE; // user内存池能用的内存起始地址 kernel_pool.phy_addr_start = kp_start; kernel_pool.pool_size = kernel_free_pages * PG_SIZE; // 15MB kernel 内存池 kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE; // MEM_BITMAP_BASE = 0xc009a000 存放内核内存池位图 kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length; bitmap_init(&(kernel_pool.pool_bitmap)); user_pool.phy_addr_start = up_start; user_pool.pool_size = user_free_pages * PG_SIZE; // 15MB user 内存池 user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length); // 存放用户内存池位图紧随其后 user_pool.pool_bitmap.btmp_bytes_len = ubm_length; bitmap_init(&(user_pool.pool_bitmap)); /* 下面初始化内核虚拟地址池的位图,按实际物理内存大小生成数组 */ kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length; // 用于维护内核堆的虚拟地址,所以要和内核内存池(15MB)大小一致 /* 虚拟地址0xc0000000~0xc00fffff 映射 物理地址0x00000000~0x000fffff(1MB) */ kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length); // 0xc009a3c0 bitmap_init(&kernel_vaddr.vaddr_bitmap); kernel_vaddr.vaddr_start = K_HEAP_START; // 0xc0100000 }
-
算出虚拟地址vaddr的页目录项pde地址
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22) // 算出页目录项 uint32_t* pde_ptr(uint32_t vaddr) { // 虚地址 0xfffff000 映射物理地址:0x00100000(页目录表起始地址) uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4); return pde; }
-
算出虚拟地址vaddr对应的页表项pte地址
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12) // 算出页表项 uint32_t* pte_ptr(uint32_t vaddr) { // 虚地址 0xffc00000 映射物理地址 0x00100000(页目录表地址) uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr)*4); return pte; }
-
算出虚拟地址映射的物理地址
uint32_t addr_v2p(uint32_t vaddr) { uint32_t* pte = pte_ptr(vaddr); return ((*pte & 0xfffff000) + (vaddr & 0x00000fff)); }
-
在 pf 表示的虚拟地址池中申请 pg_cnt 个连续的虚拟页,成功则返回虚拟页的起始地址,失败则返回NULL
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) { int vaddr_start = 0; int bit_idx_start = -1; uint32_t cnt = 0; if (pf == PF_KERNEL) { bit_idx_start = bitmap_scan(&(kernel_vaddr.vaddr_bitmap), pg_cnt); // bit_idx_start 虚拟页起始位 if (bit_idx_start == -1) // 没有足够的虚拟页 return NULL; while(cnt < pg_cnt) { bitmap_set(&(kernel_vaddr.vaddr_bitmap), bit_idx_start + cnt, 1); // 更新位图 cnt++; } vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE; } else if (pf == PF_USER) { struct task_struct* cur = running_thread(); bit_idx_start = bitmap_scan(&(cur->userprog_vaddr.vaddr_bitmap), pg_cnt); if (bit_idx_start == -1) return NULL; while (cnt < pg_cnt) { bitmap_set(&(cur->userprog_vaddr.vaddr_bitmap), bit_idx_start + cnt, 1); cnt++; } vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE; /* (0xc0000000 - PG_SIZE)做为用户3级栈已经在start_process被分配 */ ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE)); } return (void*)vaddr_start; }
-
在 m_pool 指向的物理内存池中分配 1 个物理页,成功则返回页框的物理地址,失败则返回NULL
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 = ( m_pool->phy_addr_start + (bit_idx * PG_SIZE)); return (void*)page_phyaddr; }
-
页表中添加虚拟地址_vaddr到物理地址_page_phyaddr的映射,设置页目录项和页表项
static void page_table_add(void* _vaddr, void* _page_phyaddr) { uint32_t vaddr = (uint32_t)_vaddr; uint32_t page_phyaddr = (uint32_t)_page_phyaddr; uint32_t* pde = pde_ptr(vaddr); // 虚地址对应的pde uint32_t* pte = pte_ptr(vaddr); // 虚地址对应的pte if (*pde & 0x00000001) // 页目录项存在 { ASSERT(!(*pte & 0x00000001)); if (!(*pte & 0x00000001)) // 页表项不存在 *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // 创建页表项 } else // 页目录项不存在,先创建页目录项,再创建页表项 { uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool); // 页表用到的物理内存在内核空间 *pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // 创建页目录项 ASSERT(!(*pte & 0x00000001)); *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // 创建页表项 memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE); // 初始化页框 } }
-
在内核或者用户空间分配 pg_cnt 个物理页
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt) { ASSERT(pg_cnt > 0 && pg_cnt < 3840); void* vaddr_start = vaddr_get(pf, pg_cnt); if (vaddr_start == NULL) return NULL; uint32_t vaddr = (uint32_t)vaddr_start; uint32_t cnt = pg_cnt; struct pool* mem_pool = ((pf == PF_KERNEL) ? &kernel_pool : &user_pool); while(cnt-- > 0) { void* page_phyaddr = palloc(mem_pool); if (page_phyaddr == NULL) return NULL; page_table_add((void*)vaddr, page_phyaddr); vaddr += PG_SIZE; } return vaddr_start; }