《操作系统真象还原》第八章 内存管理系统

配合视频学习效果更佳!
第一节一部分:https://www.bilibili.com/video/BV15h4y1x7jq/?spm_id_from=333.999.0.0&vd_source=701807c4f8684b13e922d0a8b116af31
第一节二部分:https://www.bilibili.com/video/BV1QX4y187tz/?vd_source=701807c4f8684b13e922d0a8b116af31
第二节:https://www.bilibili.com/video/BV1fh4y1d7p6/?vd_source=701807c4f8684b13e922d0a8b116af31
第三节:https://www.bilibili.com/video/BV1WP411Q7FM/?vd_source=701807c4f8684b13e922d0a8b116af31
第四节:https://www.bilibili.com/video/BV1jh4y1X7iE/?vd_source=701807c4f8684b13e922d0a8b116af31#reply657233827
第五节:https://www.bilibili.com/video/BV1pk4y1n7u3/?vd_source=701807c4f8684b13e922d0a8b116af31

代码仓库:https://github.com/xukanshan/the_truth_of_operationg_system

makefile的基本语法如下:

目标文件:依赖文件

[TAB]命令

在Linux中,文件分为属性与数据两部分,每个文件有三种时间,分别是atime(记录最后一次访问时间,比如vim这种,ls不算)、ctime(记录最后一次文件属性或数据的改变时间)、mtime(记录最后一次文件数据的改变时间)。三个时间可以通过stat命令查看,make程序分别获取依赖文件和目标文件的mtime,对比依赖文件是否比目标文件的mtime新,如果是的话,就执行规则中的命令。makefile的文件名并非固定,可以在执行make时用-f参数指定。如果没有指定,make会去找名为GUNmakefile的文件,如果不存在,就找makefile,如果也不存在,就找Makefile。如果执行命令前不加@,会将执行的命令也打印出来。make 目标名称,就只会对特定目标执行指定规则。

有时候我们并不关心是否产生真实的目标文件,我们只希望make不要考虑mtime,而是总能执行一些命令。就可以用伪目标的方式:

all:

[TAB]命令

伪目标不能和真实目标文件同名,就可以用关键字.PHON:伪目标名 来修饰伪目标,如以下格式

.PHONY:clean

clean:

​ rm ./build/*.o

意为定义了一个叫clean的伪目标,不需要依赖任何文件,make都会执行rm. /build/*.o命令。

为之前写的代码添加makefilemyos/makefile

#定义一大堆变量,实质就是将需要多次重复用到的语句定义一个变量方便使用与替换
BUILD_DIR=./build
ENTRY_POINT=0xc0001500
HD60M_PATH=/home/rlk/Desktop/bochs/hd60M.img
#只需要把hd60m.img路径改成自己环境的路径,整个代码直接make all就完全写入了,能够运行成功
AS=nasm
CC=gcc-4.4
LD=ld
LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS= -f elf
CFLAGS= -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32
#-Wall warning wall的意思,产生尽可能多警告信息,-fno-builtin不要采用内部函数,
#-W 会显示警告,但是只显示编译器认为会出现错误的警告
#-Wstrict-prototypes 要求函数声明必须有参数类型,否则发出警告。-Wmissing-prototypes 必须要有函数声明,否则发出警告

LDFLAGS= -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map -m elf_i386
#-Map,生成map文件,就是通过编译器编译之后,生成的程序、数据及IO空间信息的一种映射文件
#里面包含函数大小,入口地址等一些重要信息

OBJS=$(BUILD_DIR)/main.o $(BUILD_DIR)/init.o \
	$(BUILD_DIR)/interrupt.o $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o \
	$(BUILD_DIR)/print.o $ 
#顺序最好是调用在前,实现在后

######################编译两个启动文件的代码#####################################
boot:$(BUILD_DIR)/mbr.o $(BUILD_DIR)/loader.o
$(BUILD_DIR)/mbr.o:boot/mbr.S
	$(AS) -I boot/include/ -o build/mbr.o boot/mbr.S
	
$(BUILD_DIR)/loader.o:boot/loader.S
	$(AS) -I boot/include/ -o build/loader.o boot/loader.S
	
######################编译C内核代码###################################################
$(BUILD_DIR)/main.o:kernel/main.c
	$(CC) $(CFLAGS) -o $@ $<	
# $@表示规则中目标文件名的集合这里就是$(BUILD_DIR)/main.o  $<表示规则中依赖文件的第一个,这里就是kernle/main.c 

$(BUILD_DIR)/init.o:kernel/init.c
	$(CC) $(CFLAGS) -o $@ $<

$(BUILD_DIR)/interrupt.o:kernel/interrupt.c
	$(CC) $(CFLAGS) -o $@ $<

$(BUILD_DIR)/timer.o:device/timer.c
	$(CC) $(CFLAGS) -o $@ $<

###################编译汇编内核代码#####################################################
$(BUILD_DIR)/kernel.o:kernel/kernel.S 
	$(AS) $(ASFLAGS) -o $@ $<

$(BUILD_DIR)/print.o:lib/kernel/print.S
	$(AS) $(ASFLAGS) -o $@ $<

##################链接所有内核目标文件##################################################
$(BUILD_DIR)/kernel.bin:$(OBJS)
	$(LD) $(LDFLAGS) -o $@ $^
# $^表示规则中所有依赖文件的集合,如果有重复,会自动去重

.PHONY:mk_dir hd clean build all boot	#定义了6个伪目标
mk_dir:
	if [ ! -d $(BUILD_DIR) ];then mkdir $(BUILD_DIR);fi 
#判断build文件夹是否存在,如果不存在,则创建

hd:
	dd if=build/mbr.o of=$(HD60M_PATH) count=1 bs=512 conv=notrunc && \
	dd if=build/loader.o of=$(HD60M_PATH) count=4 bs=512 seek=2 conv=notrunc && \
	dd if=$(BUILD_DIR)/kernel.bin of=$(HD60M_PATH) bs=512 count=200 seek=9 conv=notrunc
	
clean:
	@cd $(BUILD_DIR) && rm -f ./* && echo "remove ./build all done"
#-f, --force忽略不存在的文件,从不给出提示,执行make clean就会删除build下所有文件

build:$(BUILD_DIR)/kernel.bin
	
#执行build需要依赖kernel.bin,但是一开始没有,就会递归执行之前写好的语句编译kernel.bin

all:mk_dir boot build hd
#make all 就是依次执行mk_dir build hd

提问,未来加入新的模块,该怎么改写makefile文件呢?

答:A、在BOJS变量中加入我们想要的目标模块的目标文件,如string.o;B、为这个目标文件建立依赖与执行规则;

随着模块越来越多,程序出错的概率越来越大,为了方便调试,一个好的习惯就是在程序中的关键部分设置“哨兵”,让它来监督数据的正确性。接下来我们实现内核系统使用的断言函数。但是断言函数在输出错误信息时,应不被其他进程打扰,这样我们才专注处理错误信息。所以ASSERT排查出错误后,应在关闭中断的情况下打印报错信息。

所以现在我们来实现一些有关开关中断的函数:1、获取当前中断状态(调用一个取得eflags寄存器值的宏);2、开启中断;3、关闭中断;4、根据需求设置中断状态的函数(调用2与3)

在此之前,先定义用于表示中断是否打开的枚举类型(myos/kernel/interrupt.h

/* 定义中断的两种状态:
 * INTR_OFF值为0,表示关中断,
 * INTR_ON值为1,表示开中断 */
enum intr_status {		 // 中断状态
    INTR_OFF,			 // 中断关闭
    INTR_ON		         // 中断打开
};

myos/kernel/interrupt.c加入如下代码 代码剖析略 其他代码详解查看书p368

#define EFLAGS_IF   0x00000200       // eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR))
//pop到了EFLAG_VAR所在内存中,该约束自然用表示内存的字母,但是内联汇编中没有专门表示约束内存的字母,所以只能用g
//g 代表可以是任意寄存器,内存或立即数

/* 获取当前中断状态 */
enum intr_status intr_get_status() {
   uint32_t eflags = 0; 
   GET_EFLAGS(eflags);
   return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}


/* 开中断并返回开中断前的状态*/
enum intr_status intr_enable() {
   enum intr_status old_status;
   if (INTR_ON == intr_get_status()) {
      old_status = INTR_ON;
      return old_status;
   } else {
      old_status = INTR_OFF;
      asm volatile("sti");	 // 开中断,sti指令将IF位置1
      return old_status;
   }
}

/* 关中断,并且返回关中断前的状态 */
enum intr_status intr_disable() {     
   enum intr_status old_status;
   if (INTR_ON == intr_get_status()) {
      old_status = INTR_ON;
      asm volatile("cli" : : : "memory"); // 关中断,cli指令将IF位置0
                                          //cli指令不会直接影响内存。然而,从一个更大的上下文来看,禁用中断可能会影响系统状态,
                                          //这个状态可能会被存储在内存中。所以改变位填 "memory" 是为了安全起见,确保编译器在生成代码时考虑到这一点。
      return old_status;
   } else {
      old_status = INTR_OFF;
      return old_status;
   }
}

/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status status) {
   return status & INTR_ON ? intr_enable() : intr_disable();   //enable与disable函数会返回旧中断状态
}

添加函数声明:myos/kernel/interrupt.h中加入如下代码

enum intr_status intr_get_status(void);
enum intr_status intr_set_status (enum intr_status);
enum intr_status intr_enable (void);
enum intr_status intr_disable (void);

接下来实现ASSERT(断言),函数功能很简单。我们用的时候,就如同ASSERT(CONDITION),如果CONDITION的值为true,那么什么也不做,如果值为false,那么就打印错误信息,然后就死循环从而让整个函数停止运行。

断言函数的核心代码panic_spin myos/kernel/debug.c

#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);
}

myos/kernel/debug.h代码 其他代码详解查看书p3701

#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic_spin(char* filename, int line, const char* func, const char* condition);

//...是可变参数,也就是随便你传多少个参数,然后原封不动地传到__VA_ARGS_那里去
//__FILE__,__LINE__,__func__是预定义宏,代表这个宏所在的文件名,行数,与函数名字,编译器处理
#define PANIC(...) panic_spin (__FILE__, __LINE__, __func__, __VA_ARGS__)

//如果定义了NDEBUG,那么下面定义的ASSERT就是个空。这样我们可以便捷的让所有ASSERT宏失效。因为有时候断言太多,程序会运行
//很慢。我们如果不想要ASSERT起作用,编译时用gcc-DNDEBUG就行了
#ifdef NDEBUG
   #define ASSERT(CONDITION) ((void)0)
#else
#define ASSERT(CONDITION)   \
    if(CONDITION){}         \
    else{PANIC(#CONDITION);}    //加#后,传入的参数变成字符串

#endif  //结束#ifdef NDEBUG
#endif  //结束#define __KERNEL_DEBUG_H

注:https://blog.csdn.net/auccy/article/details/88833659 #、##、VA_ARGS的使用

写一个测试代码 myos/kernel/main.c

#include "print.h"
#include "init.h"
#include "debug.h"
int main(void) {
   put_str("I am kernel\n");
   init_all();
   ASSERT(1==2);
   while(1);
   return 0;
}

为了后面工作更加简单,我们在这里实现一系列内存与字符串操作函数,基本和C语言是一样的

先在kernel/global.h中加入以下支持代码

#define NULL ((void*)0)
#define bool int
#define true 1
#define false 0

myos/lib/string.c 代码剖析略

#include "string.h"
#include "global.h"
#include "debug.h"  //定义了ASSERT

//将dst起始的size个字节置为value,这个函数最常用的用法就是来初始化一块内存区域,也就是置为ASCII码为0
void memset(void* dst_, uint8_t value, uint32_t size) {
    ASSERT(dst_ != NULL);            //一般开发都有这个习惯,传入进来个地址,判断不是空
    uint8_t* dst = (uint8_t*)dst_;   //强制类型转换,将对地址的操作单位变成一字节
    while (size-- > 0)               //先判断size是否>0,然后再减,然后执行循环体,size是多少,就会循环多少次
        *dst++ = value;               //*的优先级高于++,所以是先对dst指向的地址进行操作(*dst=value),然后地址+1
}

//将src地址起始处size字节的数据移入dst,用于拷贝内存数据
//src起始是有数据的,所以用const void*,const修饰void*,意味着地址内的数据是只读
void memcpy(void* dst_, const void* src_, uint32_t size) {
    ASSERT(dst_ != NULL && src_ != NULL);
    uint8_t* dst = dst_;
    const uint8_t* src = src_;
    while (size-- > 0)
        *dst++ = *src++;
}

//比较两个地址起始的size字节的数据是否相等,如果相等,则返回0;如果不相等,比较第一个不相等的数据,>返回1,<返回-1
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;
}

//将字符串从src拷贝到dst,并返回目的字符串的起始地址
char* strcpy(char* dst_, const char* src_) {
    ASSERT(dst_ != NULL && src_ != NULL);
    char* r = dst_;		       // 用来返回目的字符串起始地址
    while((*dst_++ = *src_++));  //1、*dst=*src  2、判断*dst是否为'\0',然后决定是否执行循环体,本步骤真假值不影响3   3、dst++与scr++,谁先谁后不知道
    return r;                    //上面多出来的一对括号,是为了告诉编译器,我这里的=就是自己写的,而不是将==错误写成了=
}

/* 返回字符串长度 */
uint32_t strlen(const char* str) {
    ASSERT(str != NULL);
    const char* p = str;
    while(*p++);                 //1、先取*p的值来进行2的判断     2、判断*p,决定是否执行循环体     3、p++(这一步的执行并不依赖2的判断为真) 
    return (p - str - 1);        //p最后指向'\0'后面第一个元素
}

//比较两个字符串,若a_中的字符与b_中的字符全部相同,则返回0,如果不同,那么比较第一个不同的字符,如果a_>b_返回1,反之返回-1
int8_t strcmp (const char* a, const char* b) {
    ASSERT(a != NULL && b != NULL);
    while (*a != 0 && *a == *b) {
        a++;
        b++;
    }
/* 如果*a小于*b就返回-1,否则就属于*a大于等于*b的情况。在后面的布尔表达式"*a > *b"中,
 * 若*a大于*b,表达式就等于1,否则就表达式不成立,也就是布尔值为0,恰恰表示*a等于*b */
    return *a < *b ? -1 : *a > *b;
}

/* 从左到右查找字符串str中首次出现字符ch的地址(不是下标,是地址) */
char* strchr(const char* str, const uint8_t ch) {
    ASSERT(str != NULL);
    while (*str != 0) {
        if (*str == ch) {
	        return (char*)str;	    // 需要强制转化成和返回值类型一样,否则编译器会报const属性丢失,下同.
        }
        str++;
    }
    return NULL;
}

/* 从后往前查找字符串str中首次出现字符ch的地址(不是下标,是地址) */
char* strrchr(const char* str, const uint8_t ch) {
    ASSERT(str != NULL);
    const char* last_char = NULL;
    /* 从头到尾遍历一次,若存在ch字符,last_char总是该字符最后一次出现在串中的地址(不是下标,是地址)*/
    while (*str != 0) {
        if (*str == ch) {
	        last_char = str;
        }
        str++;
    }
    return (char*)last_char;
}

/* 将字符串src_拼接到dst_后,将回拼接的串地址 */
char* strcat(char* dst_, const char* src_) {
    ASSERT(dst_ != NULL && src_ != NULL);
    char* str = dst_;
    while (*str++);
    --str;                       // 别看错了,--str是独立的一句,并不是while的循环体。这一句是为了让str指向dst_的最后一个非0字符
    while((*str++ = *src_++));	//1、*str=*src  2、判断*str     3、str++与src++,这一步不依赖2
    return dst_;
}

/* 在字符串str中查找指定字符ch出现的次数 */
uint32_t strchrs(const char* str, uint8_t ch) {
    ASSERT(str != NULL);
    uint32_t ch_cnt = 0;
    const char* p = str;
    while(*p != 0) {
        if (*p == ch) {
            ch_cnt++;
        }
        p++;
    }
    return ch_cnt;
}

其他代码详解查看书p375

建立其对应头文件 myos/lib/string.h

#ifndef __LIB_STRING_H
#define __LIB_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* strcpy(char* dst_, const char* src_);
uint32_t strlen(const char* str);
int8_t strcmp (const char *a, const char *b); 
char* strchr(const char* string, const uint8_t ch);
char* strrchr(const char* string, const uint8_t ch);
char* strcat(char* dst_, const char* src_);
uint32_t strchrs(const char* filename, uint8_t ch);
#endif

位图,实际上就是用某一位的状态(0,还是1)来表示一段连续的内存(一般为4K)区域是否已经被分配出去。而且,这样的位,在内存中是连续分布的。

接下来我们完成一系列与位图有关的函数,包含位图初始化(所有位全部置0),判断某位是0还是1,找到连续cnt个0(表示找到了某大小的连续内存),设置位图的某位。

首先创建位图的数据结构 (myos/lib/kernel/bitmap.h

#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#define BITMAP_MASK 1
struct bitmap {                 //这个数据结构就是用来管理整个位图
   uint32_t btmp_bytes_len;     //记录整个位图的大小,字节为单位
   uint8_t* bits;               //用来记录位图的起始地址,我们未来用这个地址遍历位图时,操作单位指定为最小的字节
};


#endif

myos/lib/kernel/bitmap.c 代码剖析略

#include "bitmap.h"     //不仅是为了通过一致性检查,位图的数据结构struct bitmap也在这里面
#include "stdint.h"     
#include "string.h"     //里面包含了内存初始化函数,memset
#include "print.h"
#include "interrupt.h"
#include "debug.h"      //ASSERT

/* 将位图btmp初始化 */
void bitmap_init(struct bitmap* btmp) {
   memset(btmp->bits, 0, btmp->btmp_bytes_len);   
}

//用来确定位图的某一位是1,还是0。若是1,返回真(返回的值不一定是1)。否则,返回0。传入两个参数,指向位图的指针,与要判断的位的偏移
bool 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));
}

//用来在位图中找到cnt个连续的0,以此来分配一块连续未被占用的内存,参数有指向位图的指针与要分配的内存块的个数cnt
//成功就返回起始位的偏移(如果把位图看做一个数组,那么也可以叫做下标),不成功就返回-1
int bitmap_scan(struct bitmap* bitmap, uint32_t cnt){
    uint32_t area_start = 0, area_size = 0;    //用来存储一个连续为0区域的起始位置, 存储一个连续为0的区域大小
    while(1){                   
        while( bitmap_scan_test(bitmap, area_start) && area_start / 8 < bitmap->btmp_bytes_len) //当这个while顺利结束1、area_start就是第一个0的位置;2、area_start已经越过位图边界
            area_start++;
        if(area_start / 8 >= bitmap->btmp_bytes_len)    //上面那个循环跑完可能是area_start已经越过边界,说明此时位图中是全1,那么就没有可分配内存
            return -1;
        area_size = 1;  //来到了这一句说明找到了位图中第一个0,那么此时area_size自然就是1
        while( area_size < cnt ){
            if( (area_start + area_size) / 8 < bitmap->btmp_bytes_len ){    //确保下一个要判断的位不超过边界
                if( bitmap_scan_test(bitmap, area_start + area_size) == 0 ) //判断区域起始0的下一位是否是0
                    area_size++;
                else
                    break;  //进入else,说明下一位是1,此时area_size还没有到达cnt的要求,且一片连续为0的区域截止,break
            }
            else
                return -1;  //来到这里面,说面下一个要判断的位超过边界,且area_size<cnt,返回-1
        }
        if(area_size == cnt)    //有两种情况另上面的while结束,1、area_size == cnt;2、break;所以用需要判断
            return area_start;
        area_start += (area_size+1); //更新area_start,判断后面是否有满足条件的连续0区域
    }
}

//将位图某一位设定为1或0,传入参数是指向位图的指针与这一位的偏移,与想要的值
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;    //确定要设置的位在某个字节中的偏移

/* 一般都会用个0x1这样的数对字节中的位操作,
 * 将1任意移动后再取反,或者先取反再移位,可用来对位置0操作。*/
   if (value) {		      // 如果value为1
      btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
   } else {		      // 若为0
      btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);
   }
}

添加函数声明 myos/lib/kernel/bitmap.h

void bitmap_init(struct bitmap* btmp);
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx);
int bitmap_scan(struct bitmap* btmp, uint32_t cnt);
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value);

内存管理,其实就是管理内存空间的分配。由于现在开启了段页机制,所以内存的分配就涉及到两个部分,虚拟地址分配与物理地址分配,然后将虚拟地址与物理地址通过页表建立映射。内存的分配一定是从管理可用内存的内存池中分配,内核要申请内存,那么就从内核的可用虚拟内存池与内核的可用物理内存池中进行分配,然后将分配的虚拟地址与物理地址通过内核页表建立映射。用户程序要申请内存,那么就从用户的可用虚拟内存池与用户的可用物理内存池中进行分配,然后将分配的虚拟地址与物理地址通过进程自己的页表建立映射。

虚拟内存池是按照进程为单位进行建立,每个进程都有自己的虚拟内存池,内核也有自己的虚拟内存池。但是,所有的用户进程都使用一个共用的用户物理内存池,内核使用自己的内核物理内存池,为的是避免用户进程将物理内存申请干净之后,内核就不能申请内存了,所以让内核单独用一个物理内存池。

现在,我们就来进行内存管理的核心准备工作,初始化三个内存池:管理内核可用虚拟地址空间内存池、管理内核可用物理地址空间内存池、管理用户可用物理地址空间内存池,用户可用虚拟地址空间内存池是要等到创建用户进程时才创立,现在不用初始化。

p383,p386,p387剖析memory.c代码:

1、代码功能

初始化内核可用虚拟地址空间内存池、内核可用物理地址空间内存池、用户可用物理地址空间内存池。

2、实现原理

由于段页机制的存在,使得程序的地址变成了虚拟地址,这个虚拟地址需要经过转换变成真实的物理地址才能实际可用。所以分配内存,既是一个分配虚拟内存空间的过程(在进程自己独享的整个虚拟地址空间中分配),又是一个分配真实物理空间的过程。而地址分配都需要通过内存池,所以我们需要初始化三个内存池来管理地址空间。

3、代码逻辑

A、建立虚拟内存池的数据结构,建立物理内存池的数据结构;并创建管理内核可用虚拟地址空间的内存池变量,管理内核可用物理地址空间的内存池变量,管理用户进程可用物理地址空间的内存池变量。

B、初始化上面建立的三个内存池变量。

C、将B封装成一个函数,并在总初始化函数中调用

4、怎么写代码

A、建立管理可用虚拟地址空间的数据结构虚拟内存池:virtual_addr,包含一个管理位图的数据结构、管理的可用虚拟地址空间的起始地址;建立管理可用物理地址空间的数据结构物理内存池:pool,包含一个管理位图的数据结构、管理的可用物理地址空间的起始地址、这个可用物理地址内存空间的大小;

B、通过A建立的数据结构,建立管理管理内核可用虚拟地址空间的内存池变量kernel_vaddr、管理内核可用物理地址空间的内存池变量kernel_pool、管理用户进程可用的物理地址空间内存池变量user_pool

C、根据作者设置与实际情况,初始化kernel_vaddruser_poolkernel_pool,就是初始化虚拟内存池内的位图数据结构、管理的地址空间起始地址,物理内存池内的位图数据结构、管理的地址空间起始地址、可用的物理地址空间大小

D、将C封装成一个函数mem_init(),并在init_all()中调用

5、代码实现

myos/kernel/memory.h),因为这个虚拟内存池结构未来会用到进程创建中,所以要在头文件中定义;物理内存池数据结构仅在内存管理初始化中(就建立、初始化两个物理内存池变量)用到,不会在其他地方用到,所以放在了memory.c

#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;            // 虚拟地址起始地址
};

#endif

(myos/kernel/memory.c)

#include "memory.h"
#include "stdint.h"
#include "print.h"

#define PG_SIZE 4096    //一页的大小
#define MEM_BITMAP_BASE 0xc009a000  //这个地址是位图的起始地址,1MB内存布局中,9FBFF是最大一段可用区域的边界,而我们计划这个可用空间最后的位置将来用来
        //放PCB,而PCB占用内存是一个自然页,所以起始地址必须是0xxxx000这种形式,离0x9fbff最近的符合这个形式的地址是0x9f000。我们又为了将来可能的拓展,
        // 所以让位图可以支持管理512MB的内存空间,所以预留位图大小为16KB,也就是4页,所以选择0x9a000作为位图的起始地址

//定义内核堆区起始地址,堆区就是用来进行动态内存分配的地方,咱们的系统内核运行在c00000000开始的1MB虚拟地址空间,所以自然要跨过这个空间,
//堆区的起始地址并没有跨过256个页表,没关系,反正使用虚拟地址最终都会被我们的页表转换为物理地址,我们建立物理映射的时候,跳过256个页表就行了
#define K_HEAP_START 0xc0100000

/* 核心数据结构,物理内存池, 生成两个实例用于管理内核物理内存池和用户物理内存池 */
struct pool {
   struct bitmap pool_bitmap;	 // 本内存池用到的位图结构,用于管理物理内存
   uint32_t phy_addr_start;	     // 本内存池所管理物理内存的起始地址
   uint32_t pool_size;		    // 本内存池字节容量
};

struct pool kernel_pool, user_pool;      //为kernel与user分别建立物理内存池,让用户进程只能从user内存池获得新的内存空间,
        //以免申请完所有可用空间,内核就不能申请空间了
struct virtual_addr kernel_vaddr;	 // 用于管理内核虚拟地址空间

//初始化内核物理内存池与用户物理内存池
static void mem_pool_init(uint32_t all_mem) {
   put_str("   mem_pool_init start\n");
   uint32_t page_table_size = PG_SIZE * 256;	  // 页表大小= 1页的页目录表+第0和第768个页目录项指向同一个页表+
                                                  // 第769~1022个页目录项共指向254个页表,共256个页表
   uint32_t used_mem = page_table_size + 0x100000;	  // 已使用内存 = 1MB + 256个页表
   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;   //用于存储用户空间分到的页

/* 为简化位图操作,余数不处理,坏处是这样做会丢内存。
好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存*/
   uint32_t kbm_length = kernel_free_pages / 8;			  // 内核物理内存池的位图长度,位图中的一位表示一页,以字节为单位
   uint32_t ubm_length = user_free_pages / 8;			  // 用户物理内存池的位图长度.

   uint32_t kp_start = used_mem;				  // Kernel Pool start,内核使用的物理内存池的起始地址
   uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;	  // User Pool start,用户使用的物理内存池的起始地址

   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;   //赋值给管理用户使用的物理内存池的位图长度

/*********    内核内存池和用户内存池位图   ***********
 *   位图是全局的数据,长度不固定。
 *   全局或静态的数组需要在编译时知道其长度,
 *   而我们需要根据总内存大小算出需要多少字节。
 *   所以改为指定一块内存来生成位图.
 *   ************************************************/
// 内核使用的最高地址是0xc009f000,这是主线程的栈地址.(内核的大小预计为70K左右)
// 32M内存占用的位图是2k.内核内存池的位图先定在MEM_BITMAP_BASE(0xc009a000)处.
   kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;      //管理内核使用的物理内存池的位图起始地址
							       
/* 用户内存池的位图紧跟在内核内存池位图之后 */
   user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);     //管理用户使用的物理内存池的位图起始地址
   /******************** 输出内存池信息 **********************/
   put_str("      kernel_pool_bitmap_start:");put_int((int)kernel_pool.pool_bitmap.bits);
   put_str(" kernel_pool_phy_addr_start:");put_int(kernel_pool.phy_addr_start);
   put_str("\n");
   put_str("      user_pool_bitmap_start:");put_int((int)user_pool.pool_bitmap.bits);
   put_str(" user_pool_phy_addr_start:");put_int(user_pool.phy_addr_start);
   put_str("\n");

   /* 将位图置0*/
   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);   //赋值给管理内核可以动态使用的虚拟内存池(堆区)的位图起始地址

   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");
   uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
   mem_pool_init(mem_bytes_total);	  // 初始化内存池
   put_str("mem_init done\n");
}

添加函数声明与引入全局变量 (myos/kernle/memory.h)

extern struct pool kernel_pool, user_pool;
void mem_init(void);

(myos/kernle/init.c)

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"

/*负责初始化所有模块 */
void init_all() {
   put_str("init_all\n");
   idt_init();   //初始化中断
   timer_init();
   mem_init();	  // 初始化内存管理系统
}

(myos/kernle/main.c)

#include "print.h"
#include "init.h"
#include "memory.h"
int main(void) {
   put_str("I am kernel\n");
   init_all();
   while(1);
   return 0;
}

我们说过,内存的分配就涉及到两个部分,虚拟地址分配与物理地址分配,然后将虚拟地址与物理地址通过页表建立映射。之前的内存池就是用于管理虚拟地址空间与物理地址空间,现在我们来实现从这些内存池中分配物理地址与虚拟地址,然后将分配的物理地址与虚拟地址建立映射。

p389,p393剖析memory.c代码:

1、代码功能

从内存池中分配地址,然后将分配到的物理地址与虚拟地址建立映射关系。

2、实现原理

物理内存池与虚拟内存池已经初始化完毕,我们自然就能够从这些内存池中申请到虚拟地址与物理地址。通过建立页表,完成虚拟地址到物理地址的映射。

3、代码逻辑

A、写函数完成申请地址空间,包括物理地址与虚拟地址

B、为二者建立映射关系

4、怎么写代码?

A、在memory.h中建立枚举类型结构体pool_flags用于选择从哪个虚拟内存池中分配内存,这样就可以实现用一个函数即能完成从用户虚拟地址空间分配地址,也能完成从内核虚拟地址空间分配地址;定义模块化的页表项字段宏,用于虚拟地址到物理地址的映射时的页表构建。

B、写函数vaddr_get,通过传入的poll_flags值完成对应的从对应的虚拟内存池中分配虚拟地址;写函数palloc,完成从传入的物理内存池中分配物理地址;

C、写宏PDE_IDXPTE_IDX完成将从一个虚拟地址当中取出PDT与PTE的索引;写函数pde_ptrpte_ptr将虚拟地址转换成访问虚拟地址对应的页目录表项的地址与页表表项的地址,这是为了当一个虚拟地址没有页表映射时,我们要动态建立映射,这就需要建立页目录表项与页表表项,自然得需要知道这两个的地址。

D、写出将申请得到的虚拟地址空间与物理地址空间,通过修改页表建立映射关系的函数page_table_add

1、页表存在,那么我们只需要将物理地址填入虚拟地址对应页表表项中即可

2、页表不存在(页目录表表项为空),我们需要先申请物理地址来存放页表,然后填入页目录表项这个页表的地址,然后初始化页表,最后将传入的物理地址填入虚拟地址对应页表表项中

E、写函数malloc_page根据传入的pool_flags的值决定是为内核空间还是用户空间分配连续的多个页面,包含从对应的虚拟内存池中分配虚拟地址(调用vaddr_get),从对应的物理内存池中分配物理地址(调用palloc),然后为虚拟地址与物理地址建立映射(调用page_talbe_add)。

F、写函数get_kernel_pages,快捷为内核申请地址空间(调用malloc_page

**5、代码实现如下: **

(myos/kernel/memory.h)

#define	 PG_P_1	  1	// 页表项或页目录项存在属性位
#define	 PG_P_0	  0	// 页表项或页目录项存在属性位
#define	 PG_RW_R  0	// R/W 属性位值, 读/执行
#define	 PG_RW_W  2	// R/W 属性位值, 读/写/执行
#define	 PG_US_S  0	// U/S 属性位值, 系统级
#define	 PG_US_U  4	// U/S 属性位值, 用户级


/* 内存池标记,用于判断用哪个内存池 */
enum pool_flags {
   PF_KERNEL = 1,    // 内核内存池
   PF_USER = 2	     // 用户内存池
};

myos/kernel/memory.c

#include "debug.h"
#include "string.h"

/* 在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;
}

/* 在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);	// 将此位bit_idx置1
   	uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
   	return (void*)page_phyaddr;
}

#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)

/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t* pde_ptr(uint32_t vaddr) {
   	/* 0xfffff是用来访问到页表本身所在的地址 */
	uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
   	return pde;
}

/* 得到虚拟地址vaddr对应的pte指针*/
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;
}


/* 页表中添加虚拟地址_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);

/************************   注意   *************************
 * 执行*pte,会访问到空的pde。所以确保pde创建完成后才能执行*pte,
 * 否则会引发page_fault。因此在*pde为0时,*pte只能出现在下面else语句块中的*pde后面。
 * *********************************************************/
   /* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
   	if (*pde & 0x00000001) {	 // 页目录项和页表项的第0位为P,此处判断目录项是否存在
      	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("pte repeat");
	 		*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);      // US=1,RW=1,P=1
     	}
   	} 
	else {			    // 页目录项不存在,所以要先创建页目录再创建页表项.
      /* 页表中用到的页框一律从内核空间分配 */
      	uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);

      	*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);

      	/* 分配到的物理页地址pde_phyaddr对应的物理内存清0,
       	* 避免里面的陈旧数据变成了页表项,从而让页表混乱.
       	* 访问到pde对应的物理地址,用pte取高20位便可.
       	* 因为pte是基于该pde对应的物理地址内再寻址,
       	* 把低12位置0便是该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
   	}
}

/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt) {
   ASSERT(pg_cnt > 0 && pg_cnt < 3840);
/***********   malloc_page的原理是三个动作的合成:   ***********
      1通过vaddr_get在虚拟内存池中申请虚拟地址
      2通过palloc在物理内存池中申请物理页
      3通过page_table_add将以上得到的虚拟地址和物理地址在页表中完成映射
***************************************************************/
   	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;
}

/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_kernel_pages(uint32_t pg_cnt) {
   	void* vaddr =  malloc_page(PF_KERNEL, pg_cnt);
   	if (vaddr != NULL) {	   // 若分配的地址不为空,将页框清0后返回
      	memset(vaddr, 0, pg_cnt * PG_SIZE);
   	}
   	return vaddr;
}

函数声明memory.h myos/kernel/memory.h

void* get_kernel_pages(uint32_t pg_cnt);
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt);
void malloc_init(void);
uint32_t* pte_ptr(uint32_t vaddr);
uint32_t* pde_ptr(uint32_t vaddr);

6、其他代码详解查看书p383

现在我们写修改main.c来测试 myos/kernel/main.c

#include "print.h"
#include "init.h"
#include "memory.h"
int main(void) {
   put_str("I am kernel\n");
   init_all();

   void* addr = get_kernel_pages(3);
   put_str("\n get_kernel_page start vaddr is ");
   put_int((uint32_t)addr);
   put_str("\n");

   while(1);
   return 0;
}

用info tab指令,查看虚拟地址与物理地址映射关系

用page+虚拟地址,可以查看虚拟地址的映射关系

用xp+真实地址,查看内存中的位图信息

所以,申请内存这个行为反映到代码上的实质是什么?那就是,将一个4k空间起始的物理地址,填入4K虚拟空间起始地址对应的页表表项中

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值