Makefile
前面是一些基本的Makefile知识,这里不说打算以后单独写一篇关于项目管理工具的博客
ASSERT的实现
之前的调试基本是靠while(1)的方式打断点实现,便于调试这里实现了ASSERT断言,是一个宏,此方法在很多语言中都有,一般形式是ASSERT(condition),一般情况是ASSERT里面的表达式为假的话就需要退出程序并反馈错误信息,因为断言里判断的错误一般都是严重的错误,需要的知识不多,这里是靠C的宏实现的。
//debug.h
#ifndef _KERNEL_DEBUG_H
#define _KERNEL_DEBUG_H
void panic_spin(char *filename,int line,const char *func,const char *condition);
//__VA_ARGS__是预处理器支持的专用标识符
#define PANIC(...) panic_spin (__FILE__,__LINE__,__func__,__VA_ARGS__)
#ifndef NDEBUG
#define ASSERT(CONDITION) ((void)0)
#else
#define ASSERT(CONDITION) if(CONDITION){}else{PANIC(#CONDITION);}
#endif
#endif
可以看到这里是为debug模式的开关预留了一个开关的,即NDEBUG,如果想要使用ASSERT断言就要在#include”debug.h”之前#define NDEBUG,再者就是__FILE__,LINE,__func__这三个专用标识符也可以认识一下对调试挺有用
#include"debug.h"
#include"print.h"
#include"interrupt.h"
//报错和处理的函数
void panic_spin(char *filename,\
int line,\
const char* func,\
const char* condition)\
{
intr_disable();
put_str("\n\n\n!!!!!error!!!!!\n");
put_str("filename:");
put_str(filename);
put_str("\n");
put_str("line:0x");
put_int(line);
put_str("\n");
put_str("function:");
put_str((char*)func);
put_str("\n");
put_str("condition:");
put_str((char*)condition);
put_str("\n");
while(1);
}
实现字符串的一些操作函数
虽然这里是为字符串的一些操作编写代码但是其中的memset与memcpy在其它地方用得很多,至于其具体实现这里都采用的朴素的算法比较简单,不赘述
//string.h
#ifndef _STRING_H
#define _STRING_H
#include"stdint.h"
void memset(void *dst_,uint8_t value,uint32_t size);
void memcpy(void *dst_,const void *src_,uint32_t size);
int memcmp(const void *a_,const void *b_,uint32_t size);
//char在str中首次出现的地址
char *strchr(const char *str,const uint8_t ch);
//从后往前找版本
char *strrchr(const char *str,const uint8_t ch);
char *strcat(char *dst_,const char* src_);
//ch出现次数
uint32_t strchs(const char *str,uint8_t ch);
//长度
uint32_t strlen(const char *str);
uint32_t strcpy(char *dst_,const char*src_);
int8_t strcmp(const char *a,const char*b);
#endif
//string.c
#include"string.h"
#include"global.h"
#include"debug.h"
#define NULL 0
void memset(void *dst_,uint8_t value,uint32_t size){
ASSERT(dst_ != NULL);
uint8_t *dst = (uint8_t*)dst_;
while(size-- > 0)
*dst++ = value;
}
void memcpy(void *dst_,const void *src_,uint32_t size){
ASSERT(dst_ != NULL && src_ != NULL);
uint8_t *dst = (uint8_t*)dst_;
const uint8_t *src = src_;
while(size-- > 0)
*dst++ = *src++;
}
int memcmp(const void *a_,const void *b_,uint32_t size){
const char *a = a_;
const char *b = b_;
ASSERT(a != NULL && b != NULL);
while(size-- > 0){
if(*a!=*b)
return *a>*b?1:-1;
++a;
++b;
}
return 0;
}
char *strchr(const char *str,const uint8_t ch){
ASSERT(str != NULL);
while(*str!=0){
if (*str == ch)
return (char*)str;
++str;
}
return NULL;
}
char *strrchr(const char *str,const uint8_t ch){
ASSERT(str!=NULL);
const char *last = NULL;
while(*str!=0){
if(*str == ch)
last = str;
++str;
}
return (char*)last;
}
char *strcat(char *dst_,const char* src_){
ASSERT(dst != NULL && src_!=NULL);
char *str = dst_;
while(*str++);
--str;
while((*str++=*src_++));
return dst_;
}
uint32_t strchs(const char *str,uint8_t ch){
ASSERT(str != NULL);
uint32_t cnt=0;
while(*str!=0){
if(*str==ch)
++cnt;
++str;
}
return cnt;
}
uint32_t strlen(const char *str){
ASSERT(str != NULL);
uint32_t len = 0;
while(*str++)
++len;
return len;
}
uint32_t strcpy(char *dst_,const char*src_){
uint32_t len = strlen(src_);
memcpy(dst_,src_,len);
}
int8_t strcmp(const char *a,const char*b){
while(*a&&*b){
if(*a!=*b)
return *a-*b;
++a,++b;
}
return *a-*b;
}
bitmap位图-资源管理结构的实现
bitmap是一种管理资源的方式,简单地理解就是用一个bit位来map映射某一块资源,至于后面这一个”一块资源”取多大取决于具体问题。
比如本章要实现的内存管理,管理的最小单元是一个页,一个bit的状态就对应着一个页的状态,为1表示已经用掉用为0表示还未使用。
可能看到这里还是很迷惑这是个什么,可以想象它是一个”数组”,每个元素是一个bit,每个bit对应着一个物理页。至于为什么要使用它来管理看后面的代码比较好理解。
下面是对bitmap的定义与实现:
#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#include"global.h"
#define BITMAP_MASK 1
struct bitmap{
uint32_t btmp_bytes_len;//表示这个位图的长途
uint8_t *bits;//位图数组的起始地址
};
void bitmap_init(struct bitmap *btmp);
//测试第bit_idx位是否为1
int bitmap_scan_test(struct bitmap *btmp,uint32_t bit_idx);
//在位图中连续申请cnt个位,成功返回起始下标,失败返回-1
int bitmap_scan(struct bitmap *btmp,uint32_t cnt);
void bitmap_set(struct bitmap *btmp,uint32_t bit_idx,int8_t value);
#endif
可以看到这里采用的位图结构就真的是一个数组,但C语言里数组的最小元素是一个字节而非bit,但对于位运算来说又是确确实实能操作到每一个bit的,所以这里可以看作8个bit进行一个分组然后管理。
//bitmap.c
#include"bitmap.h"
#include"stdint.h"
#include"string.h"
#include"print.h"
#include"interrupt.h"
#include"debug.h"
//简单的置零,所有字节置零了那每个比特也肯定置零了
void bitmap_init(struct bitmap *btmp){
memset(btmp->bits,0,btmp->btmp_bytes_len);
}
//测试第bit_idx个bit是0还是1
int bitmap_scan_test(struct bitmap *btmp,uint32_t bit_idx){
uint32_t byte_idx = bit_idx / 8;//是第几组
uint32_t bit_odd = bit_idx % 8;//组内第几个
return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd));
}
//扫描bitmap,看有没有连续的cnt个bit可以使用,有就返回起始地址,否则返回-1
int bitmap_scan(struct bitmap *btmp,uint32_t cnt){
uint32_t idx_byte = 0;
//0xff表示8位全为1不可能有空的
while((0xff==btmp->bits[idx_byte])&&(idx_byte < btmp->btmp_bytes_len))
++idx_byte;
ASSERT(idx_byte < btmp->btmp_bytes_len);
if(idx_byte == btmp->btmp_bytes_len)
return -1;
//现在btmp->bits[idx_byte]里有至少一个空位,开始找这个空位
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;
//只要1位直接返回
if(cnt == 1)
return bit_idx_start;
uint32_t bit_left = (btmp->btmp_bytes_len*8 - bit_idx_start);
uint32_t count=1;
uint32_t next_bit = bit_idx_start+1;
bit_idx_start = -1;
while(bit_left-- > 0){
//这里要注意的是寻找的是连续的cnt个位,如果有cnt个不连续的空位也返回-1
if(!(bitmap_scan_test(btmp,next_bit)))
++count;
else
count=0;
if(count == cnt){
bit_idx_start = next_bit - cnt + 1;
break;
}
++next_bit;
}
return bit_idx_start;
}
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;
ASSERT(value == 0 || value == 1);
if(value){
btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
}else{
btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);
}
}
内存管理系统
内存池的规划
其实内存池也是一种管理的手段,这里的内存池其实就是地址池,所谓的”池”其实就是相当于一个存储的地方,当需要用的时候去这里取,不用的时候再把地址丢回来。
- 物理内存池:我们最终要管理的是物理上的这块4GB大小的内存,后面的实现中把剩下的物理地址(因为4GB里有一部分是内核代码,剩下的才是能使用的内存)平分给了内核和用户进程,把内核看作一个特权级非常高的进程,它在运行过程中也可能会申请释放内存,所以为它单独分配了一个内存池来使用,用户内存池则是所有用户进程所共享的内存池
- 虚拟地址池:因为内核也是一个进程,所以它也有自己虚拟的一个地址空间,由于虚拟地址空间跟物理地址空间是不相干的,所以用一个虚拟的地址池来管理内核的虚拟地址空间,当然这个结构也可以用在后面管理用户进程的虚拟地址空间
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include"stdint.h"
#include"bitmap.h"
//虚拟地址池
struct virtual_addr{
//虚拟地址用到的位图结构
struct bitmap vaddr_bitmap;
uint32_t vaddr_start;
};
extern struct pool kernel_pool,user_pool;//两个物理地址池,具体定义在memory.c里面
enum pool_flags{
PF_KERNEL = 1,
PF_USER = 2
};
//一些用来组成页表项的宏
#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);
#endif
可以看到其实虚拟地址池就是用了一个位图来管理,位图反应了虚拟地址的使用情况而下面那个vaddr_start则表示管理的这块虚拟地址的起始地址是多少,这样可以使此结构管理任意一个虚拟地址开始的一片空间,否则光有位图就只能表示从0开始的虚拟空间
#define NULL 0
#include"memory.h"
#include"stdint.h"
#include"print.h"
#include"bitmap.h"
#include"string.h"
#include"debug.h"
#define PG_SIZE 4096
#define MEM_BITMAP_BASE 0xc009a000
#define K_HEAP_START 0xc0100000
/*下面两个宏用来提取一个虚拟地址对应在页表的索引
*之前分页的时候说过32位的地址前十位是页目录表的索引,中间10位是页表的索引
*所以PDE_IDX就是看addr这个地址是在页目录表的第几个表项
*PTE_IDX则是在页表中的第几个表项*/
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
//跟虚拟池差不多,这是物理池的结构,多了一个池的大小,因为现实中物理内存是有限的
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;
static void mem_pool_init(uint32_t all_mem){
//初始化,将all_mem的空间分配给内核和用户内存池
put_str("mem_pool_init\n");
//页表本身要用掉的空间
uint32_t page_table_size = PG_SIZE * 256;
/*256的由来:首先页目录表占一个页表大小,如果页目录表里每一项各异那就应该有1024页表
*也就是一共有1+1024=1025个页表,但是在前面开启分页的代码那里,把页目录表的第0和第768个目录
*项指向了同1个页表,第1~767个目录项没用,只有769~1022共254个目录项是分配了值的(因为这是属于内核的,而现在内核就已经有内容了),第1023的*值指向页目录表,不另外占空间
*所以真正用掉的页表大小就是1+1+254 = 256*/
/*一个进程要有独立的地址空间,那就肯定有一张属于这个进程的页目录表,当前只有内核一个进程,所以当前这个页目录表就是内核的页目录表,所以低端的3GB内存所对应的页目录项内容是没有的,因为内核只使用高端的1G内存
*当然这里的什么低端3GB高端1GB是虚拟地址的低端高端,并非实际的物理内存。*/
//低端1MB和页表属于用掉的的内存
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;
//计算内核和用户物理内存池需要用到的位图长度,这里位图的一个bit映射到的是一个物理页
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;
/*写到这里不知道你有没有发现前面的某个计算是有错误的
*计算用掉的内存那里是算的低端的1MB+当前页表要用掉的内存
*但是有没有想过内存池这个结构体也是需要一个地方存放的,同时
*它内部的位图结构有个指针,指向一片区域用来映射管理物理页,
*但是目前还没有分配地方给它,下面两行代码就是在为这个问题分配空间*/
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE+kbm_length);
/*所谓的分配空间其实就是把指针指到某个地方而已,至于这片区域能不能用是
*分配者考虑的问题,上面两行代码就是在充当分配者的角色分配空间。
*MEM_BITMAP_BASE的取值在代码外面解释,这里先用即可*/
put_str("kernel_pool_bitmap_start:");
put_int((int)(kernel_pool.pool_bitmap.bits));
put_str("kernel_pool_phy_addr:");
put_int((int)kernel_pool.phy_addr_start);
put_str("\n");
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);
/*K_HEAP_START的值为0xc0100000,这个好理解,在之前构造页表的时候
*0xc000 0000是映射到物理地址0的,由于已经用掉了1MB,所以要从0xc010 0000开始
*/
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");
//在mbr那里扫描过内存的大小,并把这个数值放到了0xb00处
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
mem_pool_init(mem_bytes_total);
put_str("mem_init done\n");
}
//在pf所标识的内存池内申请pg_cnt个虚拟页,成功则返回起始地址,失败则为NULL
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;
}
/*上面的函数完成了虚拟池的申请和对应位图的变化
*下面就是要将虚拟地址与实际的物理地址相关联起来
*想想CPU拿到一个虚拟地址,硬件解析(就是通过页目录表之类的)后会跳到另外一个真实的物理地址
*然后这里存的就是这个虚拟地址实际对应的物理地址,现在需要做的就是要在代码里模拟CPU得到了一个
*虚拟地址,我们也要从这个虚拟地址解析出一个物理地址然后把这个虚拟地址想要对应的物理地址写上去。
*/
/*再理一下过程,得到一个虚拟地址addr,根据前20位和页目录表与页表可以查到一个地址,这个地址与addr的后12位相加得到最后要映射成的物理地址
*现在要写一个函数,接收两个参数,addr,paddr,根据addr的前20位查表得到一个地址,将paddr的值写到这里*/
//得到虚拟地址对应的pte指针,访问这个指针能修改页表的内容
uint32_t *pte_ptr(uint32_t vaddr){
/*这个函数的目标是返回页表项的物理地址(不是页表项存的物理地址,是页表项本身所在的物理地址)
*首先要明确的是页表项的物理地址要先从页目录表那里得到页表的地址,然后加上对应的索引值
*要构建的这个地址高10位应该全为1,这里存的是页目录表自己所在的物理地址
*CPU拿着这个地址以为是页表的地址,开始取中间10位作为索引查页表项,这个时候只要把这中间10位
*写成原虚拟地址的高10位,CPU根据这10位得到一个物理地址,准备与剩下的12位相加,但是现在看看
*CPU拿到的这个物理地址,它是从页目录表里取出来的,页目录表里存的全是页表的位置,所以已经得到页表的位置了
*剩下的就是把索引与之相加,正好现在CPU差个12位的数与之相加,把原虚拟地址的中间十位左移两位给现在的CPU一加就得到到一个页表项的物理地址了*/
uint32_t *pte = (uint32_t*)(0xffc00000+\
((vaddr & 0xffc00000) >> 10) + \
PTE_IDX(vaddr) * 4);
return pte;
}
//得到虚拟地址对应的pde指针,访问这个指针能修改页目录表的内容
uint32_t *pde_ptr(uint32_t vaddr){
uint32_t *pde = (uint32_t*)((0xfffff000)+PDE_IDX(vaddr)*4);
return pde;
}
/*上面两个函数可以看到是返回一个指针的,作用就是能根据vaddr生成另外的一个地址,把这个地址给CPU
*CPU会找到vaddr对应的页目录项和页表项的物理地址,这样就能在对应位置填任何我们想要映射的物理地址了*/
//在m_pool指向的内存池分配1个物理页
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;
}
//向页表中添加虚拟地址_vaddr与物理地址page_phyaddr的映射
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);
if(*pde & 0x00000001){
ASSERT(!(*pte&0x00000001));
if(!(*pte & 0x00000001)){
*pte = (page_phyaddr | PG_US_U | PG_RW_W |PG_P_1);
}else{
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);
memset((void*)((int)pte & 0xfffff000),0,PG_SIZE);
ASSERT(!(*pte & 0x00000001));
*pte = (page_phyaddr | PG_US_U | PG_RW_W |PG_P_1);
}
}
//分配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,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;
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2128099421@qq.com