Chapter 8 Memory Management System of "Operating System Truth Restoration"

Work better with video learning!
Part of the first section: https://www.bilibili.com/video/BV15h4y1x7jq/?spm_id_from=333.999.0.0&vd_source=701807c4f8684b13e922d0a8b116af31
Part two of the first section: https://www.bilibili.com/video/BV1QX4y187tz/?v d_source =701807c4f8684b13e922d0a8b116af31
Section 2: https://www.bilibili.com/video/BV1fh4y1d7p6/?vd_source=701807c4f8684b13e922d0a8b116af31 Section 3: https://www.bilibili.com/video/BV1WP4 11Q7FM/?vd_source=701807c4f8684b13e922d0a8b116af31
Section
4: https://www.bilibili.com/video/BV1jh4y1X7iE/?vd_source=701807c4f8684b13e922d0a8b116af31#reply657233827
Section 5: https://www.bilibili.com/video/BV1pk4y1n7u3/?vd_source=701807c4 f8684b13e922d0a8b116af31

Code warehouse: https://github.com/xukanshan/the_truth_of_operationg_system

The basic syntax of a makefile is as follows:

target file: dependent file

[TAB] command

In Linux, files are divided into attributes and data. Each file has three types of time, which are atime (record the last access time, such as vim, ls is not counted), ctime (record the last file attribute or data) change time), mtime (record the change time of the last file data). The three times can be viewed through the stat command. The make program obtains the mtime of the dependent file and the target file respectively, and compares whether the dependent file is newer than the mtime of the target file. If so, executes the command in the rule. The file name of the makefile is not fixed, and can be specified with the -f parameter when executing make. If not specified, make will look for a file named GUNmakefile, if it does not exist, it will look for Makefile, if it does not exist, it will look for Makefile. If @ is not added before the command is executed, the executed command will also be printed. make target name, the specified rule will only be executed for the specific target.

Sometimes we don't care whether to generate real target files, we just hope that make will not consider mtime, but can always execute some commands. You can use the pseudo-target method:

all:

[TAB] command

The pseudo-target cannot have the same name as the real target file, and the keyword .PHON: pseudo-target name can be used to modify the pseudo-target, such as the following format

.PHONY:clean

clean:

​ rm ./build/*.o

It means that a pseudo-target called clean is defined, no need to rely on any files, make will execute the rm./build/*.o command.

Add a makefile for the previously written code ( myos/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

Question, how to rewrite the makefile when new modules are added in the future?

Answer: A. Add the target file of the target module we want to the BOJS variable, such as string.o; B. Establish dependencies and execution rules for this target file;

With more and more modules, the probability of program errors is increasing. In order to facilitate debugging, a good habit is to set up "sentinels" in key parts of the program to let it supervise the correctness of the data. Next we implement the assertion functions used by the kernel system. But when the assertion function outputs error information, it should not be disturbed by other processes, so that we can focus on processing error information. Therefore, after ASSERT finds out the error, it should print the error message with the interrupt turned off.

So now let's implement some functions related to switch interrupts: 1. Get the current interrupt status (call a macro to get the eflags register value); 2. Enable interrupts; 3. Disable interrupts; 4. Set interrupt status functions according to requirements (call 2 and 3)

Before that, define the enumeration type used to indicate whether the interrupt is enabled ( myos/kernel/interrupt.h )

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

add the following code to myos/kernel/interrupt.c code analysis omit other code detailed explanation see book 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函数会返回旧中断状态
}

Add function declaration: add the following code to 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);

Next, implement ASSERT (assertion), the function function is very simple. When we use it, it is like ASSERT(CONDITION). If the value of CONDITION is true, then nothing will be done. If the value is false, then an error message will be printed, and then an infinite loop will stop the entire function.

The core code of the assert function 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 code other codes detailed view book 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

Note: https://blog.csdn.net/auccy/article/details/88833659 #, ##, the use of VA_ARGS

Write a test code 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;
}

In order to make the later work easier, we implement a series of memory and string manipulation functions here, which are basically the same as the C language

First add the following support code in kernel/global.h

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

Myos/lib/string.c code analysis

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

For detailed explanation of other codes, see book p375

Create its corresponding header file 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

A bitmap actually uses the status of a certain bit (0 or 1) to indicate whether a continuous memory (usually 4K) area has been allocated. Moreover, such bits are contiguously distributed in memory.

Next, we complete a series of bitmap-related functions, including bitmap initialization (all bits are set to 0), determine whether a certain bit is 0 or 1, find consecutive cnt 0s (indicating that a certain size of continuous memory has been found), and set A bit of the bitmap.

First create the bitmap data structure ( 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

code analysis of 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);
   }
}

Add function declaration 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);

Memory management, in fact, is to manage the allocation of memory space. Since the segment page mechanism is now enabled, memory allocation involves two parts, virtual address allocation and physical address allocation, and then the virtual address and physical address are mapped through the page table. The allocation of memory must be allocated from the memory pool that manages the available memory. If the kernel wants to apply for memory, it will allocate it from the available virtual memory pool of the kernel and the available physical memory pool of the kernel, and then pass the allocated virtual address and physical address through The kernel page table builds the mapping. When the user program wants to apply for memory, it allocates from the user's available virtual memory pool and the user's available physical memory pool, and then establishes a mapping between the allocated virtual address and the physical address through the process's own page table.

The virtual memory pool is established in units of processes, each process has its own virtual memory pool, and the kernel also has its own virtual memory pool. However, all user processes use a shared user physical memory pool, and the kernel uses its own kernel physical memory pool, in order to prevent the kernel from being unable to apply for memory after the user process has applied for clean physical memory, so let the kernel use it alone A pool of physical memory.

Now, let's prepare for the core of memory management and initialize three memory pools: the memory pool for managing the available virtual address space of the kernel, the memory pool for managing the available physical address space of the kernel, the memory pool for managing the available physical address space for users, and the available virtual address for users. The space memory pool is not created until the user process is created, and there is no need to initialize it now.

p383, p386, p387 analyze memory.c code:

1. Code function

Initialize the virtual address space memory pool available to the kernel, the physical address space memory pool available to the kernel, and the physical address space memory pool available to the user.

2. Implementation principle

Due to the existence of the segment page mechanism, the address of the program becomes a virtual address, and this virtual address needs to be converted into a real physical address before it can be actually used. Therefore, allocating memory is not only a process of allocating virtual memory space (allocated in the entire virtual address space exclusive to the process itself), but also a process of allocating real physical space. The address allocation needs to go through the memory pool, so we need to initialize three memory pools to manage the address space.

3. Code logic

A. Establish the data structure of the virtual memory pool, establish the data structure of the physical memory pool; and create memory pool variables for managing the available virtual address space of the kernel, memory pool variables for managing the available physical address space of the kernel, and manage the available physical address space for user processes memory pool variables.

B. Initialize the three memory pool variables established above.

C. Encapsulate B into a function and call it in the total initialization function

4. How to write code

A. Establish a data structure virtual memory pool for managing the available virtual address space: virtual_addr, including a data structure for managing bitmaps and the start address of the managed available virtual address space; establish a data structure for managing the available physical address space Physical memory pool: pool, Contains a data structure that manages the bitmap, the starting address of the managed available physical address space, and the size of the available physical address memory space;

B. Through the data structure established by A, establish a memory pool variable for managing the available virtual address space of the kernel, a memory pool kernel_vaddrvariable for managing the available physical address space of the kernel kernel_pool, and a memory pool variable for managing the physical address space available for user processesuser_pool

C. According to the author's setting and actual situation, initialization kernel_vaddr, user_pool, kernel_poolis to initialize the bitmap data structure in the virtual memory pool, the start address of the managed address space, the bitmap data structure in the physical memory pool, the start address of the managed address space, and the available The size of the physical address space

D. Encapsulate C into a function mem_init()and init_all()call it in

5. Code implementation

( myos/kernel/memory.h ), because this virtual memory pool structure will be used in process creation in the future, it must be defined in the header file; the physical memory pool data structure is only in memory management initialization (create and initialize two Physical memory pool variable) is used and will not be used elsewhere, so it is placed memory.cin

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

Add function declaration and import global variables ( myos/kernle/memory.h )

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

( also/kernel/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();	  // 初始化内存管理系统
}

( also/kernel/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;
}

We have said that the allocation of memory involves two parts, virtual address allocation and physical address allocation, and then the virtual address and physical address are mapped through the page table. The previous memory pools were used to manage virtual address space and physical address space. Now we will allocate physical addresses and virtual addresses from these memory pools, and then map the allocated physical addresses to virtual addresses.

p389, p393 analyze memory.c code:

1. Code function

Allocate addresses from the memory pool, and then establish a mapping relationship between the allocated physical addresses and virtual addresses.

2. Implementation principle

The physical memory pool and virtual memory pool have been initialized, and we can naturally apply for virtual addresses and physical addresses from these memory pools. By establishing a page table, the mapping from virtual address to physical address is completed.

3. Code logic

A. Write a function to complete the application address space, including physical address and virtual address

B. Establish a mapping relationship between the two

4. How to write code?

A. Create an enumeration type structure in memory.h pool_flagsto select which virtual memory pool to allocate memory from, so that one function can be used to complete the allocation of addresses from the user virtual address space and from the kernel virtual address Space allocation address; define a modular page table entry field macro, which is used for page table construction when mapping virtual addresses to physical addresses.

B. Write a function to complete the corresponding allocation of virtual addresses from the corresponding virtual memory pool vaddr_getthrough the value passed in ; write a function to complete the allocation of physical addresses from the incoming physical memory pool;poll_flagspalloc

C. Writing a macro PDE_IDXand PTE_IDXcompleting it will take out the index of PDT and PTE from a virtual address; writing a function pde_ptrand pte_ptrconverting the virtual address into the address of the page directory entry corresponding to the virtual address and the address of the page table entry, this is for when When a virtual address does not have a page table mapping, we need to dynamically establish a mapping, which requires the creation of a page directory entry and a page table entry. Naturally, we need to know the addresses of these two.

D. Write the function that will apply for the virtual address space and physical address space, and establish the mapping relationship by modifying the page tablepage_table_add

1. The page table exists, so we only need to fill in the physical address into the page table entry corresponding to the virtual address.

2. The page table does not exist (the page directory table entry is empty), we need to apply for a physical address to store the page table first, then fill in the page table address of the page directory entry, then initialize the page table, and finally transfer the incoming The physical address is filled in the page table entry corresponding to the virtual address

E. The write function decides whether to allocate multiple consecutive pages for kernel space or user space malloc_pageaccording to the value passed in , including allocating virtual addresses (calls ) from the corresponding virtual memory pool and allocating physical addresses from the corresponding physical memory pool (call ), and then establish a mapping (call ) for the virtual address and the physical address.pool_flagsvaddr_getpallocpage_talbe_add

F , write function get_kernel_pages, quickly apply for address space for the kernel (call malloc_page)

**5. The code implementation is as follows: **

( 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"

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

Function declaration 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. For detailed explanations of other codes, see book p383

Now we write and modify main.c to test 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;
}

Use the info tab command to view the mapping relationship between virtual addresses and physical addresses

Use page+virtual address to view the mapping relationship of virtual addresses

Use xp+ real address to view bitmap information in memory

So, what is the essence of the behavior of applying for memory reflected in the code? That is, fill in the physical address starting from a 4K space into the page table entry corresponding to the starting address of the 4K virtual space

Guess you like

Origin blog.csdn.net/kanshanxd/article/details/131029357