本随笔对应项目代码(更新中):https://github.com/himuhuan/HimuOS
位图的实现
KrBitMap
结构用作任意长度的常规用途一维位图的标头. 内核使用位图作为一种经济方式来跟踪一组可重用项。
struct KrBitMap {
uint32_t Length;
PRIVATE_DATA_MEMBER BYTE *_buffer;
};
void KrBitMapInit(struct KrBitMap *btmp, BYTE *bitsBuffer, uint32_t len);
BOOL KrBitMapCheckBit(const struct KrBitMap *btmp, uint32_t idx);
int KrBitMapFindClearBits(struct KrBitMap *btmp, uint32_t cnt);
void KrBitMapSet(struct KrBitMap *btmp, uint32_t idx, BYTE val);
#define KCHECKBIT(flag, idx) ((BYTE)(1 << idx) & (flag))
#define KSETBIT(flag, idx) ((flag) |= (1 << idx))
#define KCLEARBIT(flag, idx) ((flag) &= ~(1 << idx))
KrBitMap
结构的使用方,必须使用 KrBitMapInit
对其进行初始化。结构本身不负责内存的分配与释放。
我们约定 PRIVATE_DATA_MEMBER
标记的成员必须以下划线加小写驼峰开头表示该调用方不应在代码中直接访问它。
使用示例
BYTE buffer[4] = {0x00, 0x00, 0x00, 0x00}; // 32 bits
struct KrBitMap bitmap;
// 初始化位图
KrBitMapInit(&bitmap, buffer, sizeof(buffer));
KASSERT(KrBitMapFindClearBits(&bitmap, 1) == 0); // 第0位未设置
KrBitMapSet(&bitmap, 0, 1);
KrBitMapSet(&bitmap, 1, 1);
KASSERT(KrBitMapFindClearBits(&bitmap, 1) == 2);
KASSERT(KrBitMapFindClearBits(&bitmap, 2) == 2);
KrBitMapSet(&bitmap, 2, 1);
KASSERT(KrBitMapFindClearBits(&bitmap, 1) == 3); // 第3位未设置
KrBitMapSet(&bitmap, 3, 0);
KrBitMapSet(&bitmap, 2, 0);
KASSERT(KrBitMapFindClearBits(&bitmap, 2) == 2); // 找不到连续2个未设置位
KrBitMapSet(&bitmap, 3, 1); // 设置第3位
KASSERT(KrBitMapFindClearBits(&bitmap, 2) == 4); // 找不到未设置的位
内存池 (非分页池)
HimuOS 将物理内存划分为内核内存池与用户内存池。
位图资源
HimuOS 将 PHY 0x3DF000
到 PHY 0x3FF000
保留为维护内存池所需位图区域。在位图内 1 位就表示一个自然页 (4KB). 因此在这区域的位图最多可以映射 4,294,967,296 byte(4GiB).
HimuOS 最大支持内存
HimuOS 还需额外为用户内核内存池分别保留相同长度的位图以维护虚拟地址映射,因此最大支持内存为 2GiB, 其中用户进程最多申请大约 1 GiB 内存空间。
内存池的初始化
初始化时指定内核、用户内存池的起始物理地址与大小,以及建立位图映射管理物理页的使用情况。
注意:如图所示,HimuOS从内核堆栈顶部 (0x400000) 到PDE和PTE区域保留 0x100000 的空间,所以内核内存池从 0x600000 开始分配。
内存池大小分配策略是,除去物理内存保留为内核所用的低 0x500000 空间、PDE,内核 PTE 表空间的剩余内存,内核用户内存池均分。
所有物理内存的大小已被 LOADER 加载到内存 0xB10 处。
static void MemPoolInit(uint32_t totalMem) {
/* 1 PDE + 1 PTE (0 PTE) + 254 PTE (769 - 1022) = 256 PAGE */
uint32_t pageTableSize = MEM_PAGE_SIZE * 256;
/* NOTE: HimuOS reserves space of 0x100000 from the top of the kernel stack to the PDE and PTE regions*/
uint32_t usedMem = pageTableSize + KRNL_PHY_STACK_TOP + 0x100000;
uint32_t freeMem = totalMem - usedMem;
uint16_t allFreePages = freeMem / MEM_PAGE_SIZE;
uint16_t krnlFreePages = allFreePages / 2;
uint16_t userFreePages = allFreePages - krnlFreePages;
uint32_t krnlPoolBtmpLen = krnlFreePages / 8;
uint32_t userPoolBtmpLen = userFreePages / 8;
/* kernel pool */
gKernelPool.PhyAddrStart = usedMem;
gKernelPool.PoolSize = krnlFreePages * MEM_PAGE_SIZE;
KrBitMapInit(&gKernelPool.PoolBitmap, (BYTE *)MEM_BITMAP_BASE, krnlPoolBtmpLen);
/* user pool */
gUserPool.PhyAddrStart = gKernelPool.PhyAddrStart + krnlFreePages * MEM_PAGE_SIZE;
gUserPool.PoolSize = userFreePages * MEM_PAGE_SIZE;
KrBitMapInit(&gUserPool.PoolBitmap, (BYTE *)MEM_BITMAP_BASE + krnlPoolBtmpLen, userPoolBtmpLen);
/* kernel heap virtual address */
gKernelVrAddr.VrAddrStart = MEM_KRNL_HEAP_START;
KrBitMapInit(&gKernelVrAddr.VrAddrBitmap, (BYTE *)MEM_BITMAP_BASE + krnlPoolBtmpLen + userPoolBtmpLen,
krnlPoolBtmpLen);
// clang-format off
#if _KDBG
PrintStr("Memory Summary\n");
PrintStr(" Available/Total: "); PrintInt(freeMem); PrintChar('/'); PrintInt(totalMem); PrintStr(" BYTES\n");
PrintStr(" Kernel Pool: 0x"); PrintHex(gKernelPool.PhyAddrStart); PrintStr(" -> 0x");
PrintHex(gKernelPool.PhyAddrStart + gKernelPool.PoolSize);
PrintStr(" ("); PrintInt(gKernelPool.PoolSize); PrintStr(" bytes)\n");
PrintStr(" User Pool: 0x"); PrintHex(gUserPool.PhyAddrStart); PrintStr(" -> 0x");
PrintHex(gUserPool.PhyAddrStart + gUserPool.PoolSize);
PrintStr(" ("); PrintInt(gUserPool.PoolSize); PrintStr(" bytes)\n");
#endif
// clang-format on
}
虚拟地址
每个内存池只负责分配内存页,而对应的虚拟地址映射由结构 KR_VIRTUAL_ADDRESS
维护。
struct KR_VIRTUAL_ADDRESS {
struct KR_BITMAP VrAddrBitmap;
uint32_t VrAddrStart;
};
以下代码获取在虚拟地址池中符合大小条件的首地址
static void *GetUnallocatedVrAddr(enum KR_MEMORY_POOL_TYPE type, uint32_t pageCnt) {
int32_t vrAddrStart = 0, bitIdx = -1;
if (type == MEMORY_POOL_KERNEL) {
bitIdx = KrBitMapFindClearBits(&gKernelVrAddr.VrAddrBitmap, pageCnt);
if (bitIdx == -1)
return NULL;
KrBitMapSetBits(&gKernelVrAddr.VrAddrBitmap, bitIdx, pageCnt, 1);
vrAddrStart = gKernelVrAddr.VrAddrStart + bitIdx * MEM_PAGE_SIZE;
}
return (void *)vrAddrStart;
}
PTE & PDE 操作
对于 PDE,PTE 的动态操作基于以下事实:
- 在保护模式下必须对虚拟地址进行操作
- x86 的虚拟地址结构为:
- 高10位:用来定位页目录表中的一个页目录项 (PDE)(页目录项中包含页表的物理地址)
- 中间10位:用于在某个页表中定位页表项 (PTE)
- 低12位:页内偏移量
- 在 LOADER 阶段我们已将 最后一项 PDE 的物理地址指向 第 0 PDE
于是当给定任意虚拟地址 VAddr, 可以通过以下方式获取该地址对应的 PTE 虚拟地址:
CPU 通过三次定位寻址到真实的物理地址
将高10位指向最后 PDE,将虚拟地址的 PDE 当作 PTE,将虚拟地址的 PTE 地址(因为是中10位,乘以4以符合 12 位物理页偏移)乘以 4 当作 物理页偏移量.
由于高10位指向最后 PDE,而且最后一项 PDE 的物理地址指向 PDE 表头,这相当于“原地跳转到头部”,如此以来便使得CPU在效果上只进行了“两次”寻址。也即最后停留到 PTE 实际物理地址处
uint32_t *GetVrAddrPte(uint32_t vrAddr) {
return (uint32_t *)(0xFFC00000 + ((vrAddr & 0xFFC00000) >> 10) + (VR_ADDR_MID_PART(vrAddr) << 2));
}
同理,PDE 获取,进行两次“原地跳转到头部”操作,并将 PDE 部分(高10位)用于物理页偏移
uint32_t *GetVrAddrPde(uint32_t vrAddr) { return (uint32_t *)(0xFFFFF000 + (VR_ADDR_HIGH_PART(vrAddr) << 2)); }
虚拟地址与物理地址的关联
在分配内存时,首先向内存池请求一个新的物理页 (返回物理地址):
static void *AllocOnePhyPage(struct KR_MEMORY_POOL *pool) {
int idx = KrBitMapFindClearBits(&pool->PoolBitmap, 1);
if (idx == -1)
return NULL;
KrBitMapSet(&pool->PoolBitmap, idx, 1);
return (void *)(idx * MEM_PAGE_SIZE + pool->PhyAddrStart);
}
之后我们将分配好的物理页与虚拟地址关联:
/* Establishing a mapping between virtual addresses and physical addresses through PDE and PTE */
static void AddPageTableMap(void *virAddrPtr, void *phyPageAddrPtr) {
uint32_t vrAddr = (uint32_t)virAddrPtr, phyPageAddr = (uint32_t)phyPageAddrPtr;
uint32_t *pde = GetVrAddrPde(virAddrPtr);
uint32_t *pte = GetVrAddrPte(virAddrPtr);
if (*pde & MEM_PAGE_P_1) { /* PDE exists */
if (*pte & MEM_PAGE_P_1)
KPanic("Double allocation of PTE addresses that have already been allocated");
*pte = (phyPageAddr | MEM_PAGE_US_U | MEM_PAGE_RW_W | MEM_PAGE_P_1);
} else { /* PDE not exists */
uint32_t pdePhyAddr = (uint32_t)AllocOnePhyPage(&gKernelPool);
*pde = (pdePhyAddr | MEM_PAGE_US_U | MEM_PAGE_RW_W | MEM_PAGE_P_1);
memset((void *)((int)pte & 0xFFFFF000), 0, MEM_PAGE_SIZE);
KASSERT(!(*pte & MEM_PAGE_P_1));
*pte = (phyPageAddr | MEM_PAGE_US_U | MEM_PAGE_RW_W | MEM_PAGE_P_1);
}
}
由于分配内存时,虚拟地址必须是从 GetUnallocatedVrAddr
获取的,因此理论上不可能存在 PTE 与其存在映射。如果有,那就直接引发 panic。
如果 PDE P位标记为不存在,说明该 PDE 没有对应的页表,于是从内核内存池中直接获取一物理页用于 PDE 对应的 PTE 页表。由于 pte 对应的是虚拟地址对应 pte 的虚拟地址。如果只取高20位,那么该地址就表示 pte 所在表的表头,清空该页表项所在的页(以保证没有垃圾数据)。
内存池的整页分配
综上所述,要分配一物理页并与虚拟地址关联,大致步骤如下:
- 内核初始化时,初始化内存池
- 通过
GetUnallocatedVrAddr
获取尚未分配物理页的虚拟地址页 AllocOnePhyPage
获取空闲的物理页- 添加虚拟地址页到物理页的映射,修改虚拟地址对应的 PTE, 使其 PTE 项记录物理页地址。
使用 API KrAllocMemPage
从内存池获取中分配 pageCnt
自然页:
void *KrAllocMemPage(enum KR_MEMORY_POOL_TYPE type, uint32_t pageCnt) {
KASSERT(pageCnt > 0);
struct KR_MEMORY_POOL *pool;
void *pagePhyAddr, *vrAddrStart;
uint32_t vrAddr;
vrAddrStart = GetUnallocatedVrAddr(type, pageCnt);
if (vrAddrStart == 0)
return NULL;
vrAddr = (uint32_t)vrAddrStart;
pool = (type & MEMORY_POOL_KERNEL) ? &gKernelPool : &gUserPool;
while (pageCnt-- > 0) {
pagePhyAddr = AllocOnePhyPage(pool);
if (pagePhyAddr == NULL)
return NULL;
AddPageTableMap((void *)vrAddr, pagePhyAddr);
vrAddr += MEM_PAGE_SIZE;
}
return vrAddrStart;
}
void *KrAllocKernelMemPage(uint32_t pageCnt) {
void *krnlPage = KrAllocMemPage(MEMORY_POOL_KERNEL, pageCnt);
if (krnlPage != NULL)
memset(krnlPage, 0, pageCnt * MEM_PAGE_SIZE);
return krnlPage;
}
标签:实现,PTE,PAGE,MEM,内核,PDE,uint32,内存
From: https://www.cnblogs.com/himu-qaq/p/18530490