本节前置内容:内存管理-基础-初始化内存池
本节对应分支:memory-alloc
概述
对笔者而言,内存分配一直是操作系统最神秘的部分之一,从学习编程开始,就一直能在耳边听到这个词,所以这也是本人最期待的部分,不知读者是否也是如此呢?本节我们实现的内存分配是“整页分配”,这与 malloc 函数不同,后者能申请任意大小的内容,而前者的申请单位则是以页为计。不过,malloc 也是基于“整页分配”进行的,所以未来我们也会借助本节内容来实现 malloc 函数。
本节的函数逻辑也都很简单,只是它们的数量较多,关系稍显复杂,所以贴心的笔者(手动狗头^_^)献上一幅函数关系图以供大家参考:
上图就是内存申请的全过程,大括号中包含的函数即为括号所指函数中调用的函数,且从上到下依次调用。上图只是为了让大家稍微熟悉页分配的过程,具体过程咋们还是来看代码吧。
代码解析
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
| enum pool_flags { PF_KERNEL = 1, PF_USER = 2 };
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; };
#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
void mem_init(); void* get_kernel_pages(uint32_t pg_cnt); void* malloc_page(enum pool_flags pf, uint32_t pg_cnt); void malloc_init(); uint32_t* pte_ptr(uint32_t vaddr); uint32_t* pde_ptr(uint32_t vaddr);
|
- pool_flags 为枚举,用来指明当前的操作对象是内核内存池还是用户内存池。
- 第 18~23 行为页表项/目录项的属性,这将在我们创建页表项和页目录项时用到。读者可能已经忘了页表项/页目录项的格式:
关于这些属性的详细介绍,请回顾开启分页。
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
| #define PG_SIZE 4096
#define MEM_BITMAP_BASE 0xc009a000 #define PDE_IDX(addr) ((addr & 0xffc00000) >> 22) #define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
#define K_HEAP_START 0xc0100000 #define MEM_SIZE_ADDR 0x90c struct pool kernel_pool, user_pool; struct virtual_addr kernel_vaddr;
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 { } return (void*)vaddr_start; }
uint32_t* pte_ptr(uint32_t vaddr) { uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4); return pte; }
uint32_t* pde_ptr(uint32_t vaddr) { uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4); return pde; }
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; }
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); uint32_t* pte = pte_ptr(vaddr);
if (*pde & 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);
memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE); assert(!(*pte & 0x00000001)); *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); } }
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, 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; }
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; }
|
建议看官阅读代码时,按上面给的函数关系图的顺序进行,这样思路会更加清晰。注释很详细,下面只对几个点做强调:
-
第 41 行,获取虚拟地址对应的 PTE 地址。如何根据给定的虚拟地址定位相应的页目录和页表?这在开启分页-代码详解中提到过,请各位回顾该节,此处不再赘述。
-
第 57 行,“扫描和设置位图要保证原子操作”,这句话的意思是,扫描和设置位图必须连续,中间不能切换线程 。这里和线程切换有关,简单作下阐述:比如当线程 A 执行完第 58 行,成功找到一个物理页面;紧接着,切换到 B 线程,恰好 B 线程也执行到了 58 行,也成功找到了一个物理页面。由于线程 A 找到后还没来得及将该位置 1 就被换下 CPU,因此 A、B 这两个线程此时申请的是同一个物理页面!这必然会引发问题 。因此扫描和设置位图必须保证原子操作。需要注意的是,此处代码并没有保证原子性,未来我们会用锁来实现 。当然,如果读者实在不放心,可以先在此函数首尾分别关开中断,避免时钟中断引发任务调度。
-
同样是申请页,为什么 vaddr_get() 有申请页数的参数,而 palloc() 没有呢?这个答案在第 100 行 malloc_page() 函数中。这是因为申请的 虚拟地址必须连续,即必须是一整块虚拟内存;而申请的物理内存则无需连续 (如果要求物理内存连续,则分页机制将彻底变成鸡肋)。所以,申请一大块虚拟内存时,填写你所需的页数参数即可;而申请一大块物理内存时,则需要通过第 115 行的 while() 进行。同时注意,第 58 行的位图扫描,申请个数被指定为 1 。
-
第 79~96 行是需要重点强调的内容 。
(1)第 79 行判断该 vaddr 对应页目录项是否存在,这句话并不精确,应该是:判断该页目录项对应的页表是否存在。原因是,页目录项一定是存在的(因为页目录表是完整的),不管是现在的内核进程或是将来的用户进程,创建进程时我们都为其开辟一张完整的页目录表内存,只是说可能并不会为所有的页目录项填写信息(安装页目录项)。有人会问,既然并非每个页目录项都记录了信息,那怎么还能通过 79 行的 if 语句判断目录项对应的页表是否存在呢?好问题!这就是第 133 行将申请到的页内存全部清零的原因 。将来我们为用户进程开辟页目录表时,会通过 get_kernel_pages() 申请一页内存,并将其作为页目录表。此时页目录表所占字节全为 0(第133行),因此每个页目录项中的 P 位也为 0(表示不对应任何页表),如此一来,就可以通过 P 位来判断该目录项对应的页表是否存在。也就是说,如果不显式安装页目录项,则 P=0,无对应页表。
(2)第 85 行注释,不论是内核页表还是用户页表,所用页框一律从内核空间分配 。注意,用户进程的页目录表/页表存放在内核空间而非用户空间中,否则恶意用户进程就可以通过某些方式修改内存映射,从而访问内核或其他进程的物理内存。因此,内存管理都由内核负责!
(3)第 93 行,与前类似,须将申请到的页表内存初始化为 0,这样访问某虚拟地址时,如果对应的页表项不存在,即 P=0,则引发缺页异常。注意,笔者最初很疑惑为什么不直接利用 pde_phyadd 清零页表:
1
| memset(pde_phyadd, 0, PG_SIZE);
|
这是因为:由于开启了分页,即使 pde_phyadd 为页表的物理地址,编译器也会将其看作虚拟地址 ,所以此方式清零的内存并非物理地址 pde_phyadd!经过第 87 行安装页目录项后,(void*)((int)pte & 0xfffff000)
对应的物理地址才是 pde_phyadd 。这里很绕,请读者反复理解!
内核的页目录被创建时也被初始化为 0,参见开启分页-代码详解中 loader.s 的第 122 行代码。
在分页机制中我们说过,页目录表必须完整,通过以上解析,大家理解了其中的原因吗?由于页目录表已经覆盖所有地址,页表才能够按需创建,这相比于一级页表,大大节省了页表所占用的内存。
实际上,只有在用户进程中才会出现页目录项对应的页表不存在的情况。内核代码只运行在 1MB 内,内核堆的约 1GB 空间也已经提前创建好了页表(第769~1022号页表),所以内核不会出现此情况。
-
vaddr_get()、palloc()、page_table_add() 均被声明为静态函数,这是因为这三个函数仅供 malloc_page() 函数使用,对外部不可见。
OK,本节就到这里,内容少但密度大,注意消化。