操作系统实现:内存管理
本文参考书:操作系统真像还原、计算机组成原理(微课版)
所谓内存管理包含:
物理内存
虚拟地址空间
以上就是内存管理中所要管理的资源。那么内存管理的第一步就应该是整理出这两种资源。
物理内存要分为两部分:
①内核内存
②用户内存
在内核态下也经常会有一些内存申请,比如申请个pcb、页表等等。内核态和用户态所试用的内存要分隔开,用户态不能直接访问内核态的内存。
操作系统在进入保护模式前会通过
int 15h ax = E801h 获取内存大小,最大支持4G
或者其他软中断获取计算机内存大小 mm_bytes,并将mm_bytes放在一个内存地址我们这里是放在 物理地址0xb00。
这里可能有个疑问,我们将mm_bytes 放在0xb00处,进入保护模式后开启了虚拟地址,那怎么知道0xb00的虚拟地址是什么?
答:在操作系统建立页表时,低地址物理内存和虚拟地址设计为1对1映射,既 0xb00物理地址== 0xb00虚拟地址。
在本文章中操作系统实现中低1M内存是1对1映射的。
有了物理内存的大小,那下一步就应该建立结构以便对内存进行管理。
struct pool { struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存 uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址 uint32_t pool_size; // 本内存池字节容量 struct lock lock; // 申请内存时互斥 };
物理内存也需要位图进行管理 bitmap 实现之前说过了,用户位图和内核位图可以放在1M内找几个连续页存放。看个人设计。
物理内存起始地址 phy_addr_start 是需要计算的,我们要知道低段内存部分哪些空间是被占用了。
在本文章中内核放在低1M处,1M开始处又放了256个页(页表实现文章)。所以
phy_addr_start = 0x100000 + PG_SIZE * 256
pool_size 内核字节池的容量,这里分配多少按操作系统需求来设计,本文章直接将可用内存容量的一半分给内核。
free_mm = mm_bytes - phy_addr_start //可用内存
可用内存就这些但是可以直接 ÷ 2 分配给内核吗?
答:是不行的,我们知道内存只要通过位图管理的,每个位图指向4K大小的页,所以我们要先计算出可用内存有多少页,按总页数一半的来给pool_size
all_free_pages = free_mm / PG_SIZE; kernel_free_pages = all_free_pages / 2; pool_size = kernel_free_pages * PG_SIZE;
类似地用户内存池做一下减法就好了。
物理内存页申请
* 成功则返回页框的物理地址,失败则返回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); // 将此位bit_idx置1 uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start); return (void*)page_phyaddr; }
bit_idx是返回的是页起始下标, bit_idx * PG_SIZE是偏移地址 + 基地址 就是物理页位置。
虚拟内存管理主要是处理的是虚拟地址和物理地址映射问题。
虚拟地址管理分为:
内核多级页表
每个进程一个多级页表
其中进程的多级页表,是在进程创建时申请的内存页建立的这里只说内核页表,进程的是类似的只是多了个申请页的过程。
内核页表是在进入保护模式前建好了,在之前的文章里写过。
虚拟地址所使用的结构如下:
/* 用于虚拟地址管理 */ struct virtual_addr { /* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单位。*/ struct bitmap vaddr_bitmap; /* 管理的虚拟地址 */ uint32_t vaddr_start; };
内核二级页表
虚拟地址低1M的地址空间和物理地址是1-1映射关系。既0号页目录项指向第0个页表,第0个页表的0-254个页表项指向物理地址 0-1M。
在32位系统中,所有页表的虚拟地址 3G以上空间都是内核虚拟地址对应关系,3G空间的起始部位为页目录项第768项。
所以我们只需要一直更新内核页目录第768项到1023项所指向的页表,就可以保证其他进程页表的内核虚拟地址空间同步变化。
内核页目录的第768-1023项也是指向 0-255 号页表。这是在未开启保护模式前就初始化写好的。(详细看页表实现文章)
至于低1M的1-1映射,主要为了方便操作。
所以内核虚拟地址结构的 vaddr_start 应该初始化为0xc0100000 ,这里就是768项跳过了低1M空间的虚拟地址。
如何建立物理地址与虚拟地址的映射关系?
先申请一个物理地址,再申请一个虚拟地址,拿到了2个地址后,我们要在虚拟地址对应的页表项中填写对应的物理地址(高24位)。
如何拿到虚拟地址对应的页表项?
首先要找到页目录表,页目录项(虚拟地址高10位)存在则拿到页表,要判断页表项(虚拟地址中间10位)未使用后填入物理地址(高24位)。
页目录项不存在还要创建新页表,再填入页目录项后再进行后续操作。
如何拿到页表项物理地址?
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
uint32_t* pte_ptr(uint32_t vaddr) { /* 先访问到页表自己 + \ * 再用页目录项pde(页目录内页表的索引)做为pte的索引访问到页表 + \ * 再用pte的索引做为页内偏移*/ uint32_t* pte = (uint32_t*)(0xffc00000 + \ ((vaddr & 0xffc00000) >> 10) + \ PTE_IDX(vaddr) * 4); return pte; }
0xffc00000 代表选择最后一个页目录项,前10位都是1,在页表实现中,我们已经,提前设置好最后一个页目录项存放的是页目录物理基地址所以虚拟地址0xffc00000的含义是将页目录表当成页表,返回的是当前页目录表的物理地址
(vaddr & 0xffc00000) >> 10是将vaddr虚拟地址的前10位移动到中间10位,和0xffc00000相加在这里也可以理解为将页目录表当成页表在页目录表中选择页目录项,返回的是我们要的页表物理地址。
PTE_IDX(vaddr) * 4 代表选择页表中的页表项,也就是返回pte地址
如何拿到页目录项物理地址?
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
uint32_t* pde_ptr(uint32_t vaddr) { /* 0xfffff是用来访问到页表本身所在的地址 */ uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4); return pde; }
提前设置好最后一个页目录项存放的是页目录物理基地址, 0xfffff000 前20位是 1111111111 1111111111 ,代表页目录表基地址
建立一一映射关系:
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);
/************************ 注意 *************************
* 执行*pte,会访问到pde。所以确保pde创建完成后才能执行*pte,
* 否则会引发page_fault。因此在pde未创建时,
* *pte只能出现在下面最外层else语句块中的*pde后面。
* *********************************************************/
/* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
if (*pde & 0x00000001) {
ASSERT(!(*pte & 0x00000001));
if (!(*pte & 0x00000001)) { // 只要是创建页表,pte就应该不存在,多判断一下放心
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // US=1,RW=1,P=1
} else { // 调试模式下不会执行到此,上面的ASSERT会先执行.关闭调试时下面的PANIC会起作用
PANIC("pte repeat");
}
} else { // 页目录项不存在,所以要先创建页目录项再创建页表项.
/* 页表中用到的页框一律从内核空间分配 */
uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
/******************* 必须将页表所在的页清0 *********************
* 必须把分配到的物理页地址pde_phyaddr对应的物理内存清0,
* 避免里面的陈旧数据变成了页表中的页表项,从而让页表混乱.
* pte的高20位会映射到pde所指向的页表的物理起始地址.*/
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
}
}
内存回收,将分配的页表项的P位取消。
内核或者用户物理地址bitmap改为0
虚拟地址bitmap 设为0
/* 将物理地址pg_phy_addr回收到物理内存池 */ void pfree(uint32_t pg_phy_addr) { struct pool* mem_pool; uint32_t bit_idx = 0; if (pg_phy_addr >= user_pool.phy_addr_start) { // 用户物理内存池 mem_pool = &user_pool; bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE; } else { // 内核物理内存池 mem_pool = &kernel_pool; bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE; } bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0); // 将位图中该位清0 } /* 去掉页表中虚拟地址vaddr的映射,只去掉vaddr对应的pte */ static void page_table_pte_remove(uint32_t vaddr) { uint32_t* pte = pte_ptr(vaddr); *pte &= ~PG_P_1; // 将页表项pte的P位置0 asm volatile ("invlpg %0"::"m" (vaddr):"memory"); //更新tlb } /* 在虚拟地址池中释放以_vaddr起始的连续pg_cnt个虚拟页地址 */ static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) { uint32_t bit_idx_start = 0, vaddr = (uint32_t)_vaddr, cnt = 0; if (pf == PF_KERNEL) { // 内核虚拟内存池 bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE; while(cnt < pg_cnt) { bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0); } } else { // 用户虚拟内存池 struct task_struct* cur_thread = running_thread(); bit_idx_start = (vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE; while(cnt < pg_cnt) { bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0); } } } /* 释放以虚拟地址vaddr为起始的cnt个物理页框 */ void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) { uint32_t pg_phy_addr; uint32_t vaddr = (int32_t)_vaddr, page_cnt = 0; ASSERT(pg_cnt >=1 && vaddr % PG_SIZE == 0); pg_phy_addr = addr_v2p(vaddr); // 获取虚拟地址vaddr对应的物理地址 /* 确保待释放的物理内存在低端1M+1k大小的页目录+1k大小的页表地址范围外 */ ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= 0x102000); /* 判断pg_phy_addr属于用户物理内存池还是内核物理内存池 */ if (pg_phy_addr >= user_pool.phy_addr_start) { // 位于user_pool内存池 vaddr -= PG_SIZE; while (page_cnt < pg_cnt) { vaddr += PG_SIZE; pg_phy_addr = addr_v2p(vaddr); /* 确保物理地址属于用户物理内存池 */ ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= user_pool.phy_addr_start); /* 先将对应的物理页框归还到内存池 */ pfree(pg_phy_addr); /* 再从页表中清除此虚拟地址所在的页表项pte */ page_table_pte_remove(vaddr); page_cnt++; } /* 清空虚拟地址的位图中的相应位 */ vaddr_remove(pf, _vaddr, pg_cnt); } else { // 位于kernel_pool内存池 vaddr -= PG_SIZE; while (page_cnt < pg_cnt) { vaddr += PG_SIZE; pg_phy_addr = addr_v2p(vaddr); /* 确保待释放的物理内存只属于内核物理内存池 */ ASSERT((pg_phy_addr % PG_SIZE) == 0 && \ pg_phy_addr >= kernel_pool.phy_addr_start && \ pg_phy_addr < user_pool.phy_addr_start); /* 先将对应的物理页框归还到内存池 */ pfree(pg_phy_addr); /* 再从页表中清除此虚拟地址所在的页表项pte */ page_table_pte_remove(vaddr); page_cnt++; } /* 清空虚拟地址的位图中的相应位 */ vaddr_remove(pf, _vaddr, pg_cnt); } } /* 回收内存ptr */ void sys_free(void* ptr) { ASSERT(ptr != NULL); if (ptr != NULL) { enum pool_flags PF; struct pool* mem_pool; /* 判断是线程还是进程 */ if (running_thread()->pgdir == NULL) { ASSERT((uint32_t)ptr >= K_HEAP_START); PF = PF_KERNEL; mem_pool = &kernel_pool; } else { PF = PF_USER; mem_pool = &user_pool; } lock_acquire(&mem_pool->lock); struct mem_block* b = ptr; struct arena* a = block2arena(b); // 把mem_block转换成arena,获取元信息 ASSERT(a->large == 0 || a->large == 1); if (a->desc == NULL && a->large == true) { // 大于1024的内存 mfree_page(PF, a, a->cnt); } else { // 小于等于1024的内存块 /* 先将内存块回收到free_list */ list_append(&a->desc->free_list, &b->free_elem); /* 再判断此arena中的内存块是否都是空闲,如果是就释放arena */ if (++a->cnt == a->desc->blocks_per_arena) { uint32_t block_idx; for (block_idx = 0; block_idx < a->desc->blocks_per_arena; block_idx++) { struct mem_block* b = arena2block(a, block_idx); ASSERT(elem_find(&a->desc->free_list, &b->free_elem)); list_remove(&b->free_elem); } mfree_page(PF, a, 1); } } lock_release(&mem_pool->lock); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!