《操作系统真象还原》第八章

《操作系统真象还原》第八章

本篇对应书籍第八章的内容

本篇内容介绍了使用makefile进行编译文件,位图在内存管理中的使用,已经内存管理系统中的内存池初始化和分配页内存

makefile

到目前为止,每次编译需要用 4 次 gcc,两次 nasm,和 ld 链接,操作十分麻烦,通过makefile可以简化这一块的操作。


makefile 是 Linux 下编译大型程序用的工具,Linux 使用 make 命令配合 makefile 使用;

make 和 makefile 并不是用来编译程序的,它只负责找出哪些文件有变化,并且根据依赖关系找出受影响的文件, 然后执行事先在makefile中定义好的命令规则。make 是在 shell 下执行的,所以 makefile 里的命令规则里的命令,都是 shell 命令。

而实际上,makefile 通常用来编译程序。

总的来说就是:依赖关系是定义在文件makefile中, make程序通过解析makefile文件, 自动找出变更的文件以及依赖此变更文件的相关文件, 然后对所有受影响的相关文件执行事先定义好的命令规则。

makefile 基本语法

目标文件:依赖文件
	命令
  • 目标文件:想要生成的文件
  • 依赖文件:生成目标文件所需要的依赖文件
  • 命令:依赖文件内容有变更时(mtime),执行的shell命令,每条命令都要单独在一行

这样的三部分在一起称为一组规则

使用举例:

main.o:main.c
	@echo "hello world"
	#这是注释

如果 main.c 有改变,就打印helloworld,@的作用就是在输出中不显示命令本身


make 命令默认依次寻找当前目录下的GNUmakefile,makefile,Makefile文件,也可以用参数-f指定 makefile 文件

如果 makefile 里有多个目标,如果只想执行其中一组命令,那就将目标名称作为参数来使用:

make main

makefile 伪目标

有时候,希望 makefile 可以不管 mtime,总是能执行一些规则,那就可以用 makefile 中的伪目标来实现

当规则中不存在依赖文件时,这个目标文件名就是伪目标。

为了防止伪目标和真实目标文件同名而失效,可以显式使用关键字.PHONY来修饰伪目标,格式为:.PHONY:伪目标名称

makefile 中的伪目标名称虽然可以自己定,但是约定俗成这些名称做这些事情:

伪目标名称功能描述
all通常用来完成所有模块的编译工作
clean通常用于清空编译完成的所有目标文件,一般rm命令实现
dist通常用于将打包文件后的tar再压缩成gz文件
Install通常将编译好的程序复制到安装目录下,此目录是在执行configure脚本
printf通常用于打印已发生改变的文件
tar通常用于将文件打包成tar
test用于测试makefile流程

makefile 递归式推导目标

目标文件需要依赖文件,就会在当前 makefile 中寻找这个依赖文件,找到之后,判断这个依赖文件是否存在,不存在或者不是最新的,那就直接执行对应规则的命令,执行完毕后,去判断下一个依赖文件

makefile 自定义变量和系统变量

自定义变量的值只能是字符串,且多个值需要用空格隔开,使用$(var)进行使用

test0.o:test0.c
	gcc -c -o test0.o test0.c
test1.o:test1.c
	gcc -c -o test1.o test1.c
    
objfiles = test0.c test1.c
all:$(objfiles)
	@echo "compile done"

系统变量:

在这里插入图片描述

makefile 自动化变量

makefile 还支持一种自动化变量:

  • $@:规则中目标文件的集合
  • $<:规则中依赖文件的第1个文件
  • $^:规则中依赖文件的集合
  • $?:规则中所有比目标文件更新的依赖文件的集合

makefile 模式匹配

使用%可以自动匹配字符串,就像 Linux 中的*一样,匹配到的,且规则用不上的,依赖文件,make 命令会跳过他们不进行处理

实现 assert 断言

程序运行的时候难免会出现错误,为了更好的监视错误的发生,可以用ASSERT断言排查错误,在ASSERT排查出错误的时候,为了报错信息不被其他信息所干扰,最好能在关中断的情况下输出错误信息

这里实现的是专供内核使用的 ASSERT,给用户程序使用的 ASSERT 到时候再说

实现开、关中断的函数

这里要解决的问题是:如何在开中断的情况下关中断?

kernel/source/interrupt.c:

在原有的基础上,添加如下内容:

#define EFLAGS_IF       0x00000200      // eflags中的 IF 位为 1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl%0": "=g"(EFLAG_VAR))

...;

/*开中断并返回开中断前的状态*/
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
                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();
}

/*获取当前中断状态*/
enum intr_status intr_get_status(){
        uint32_t eflags = 0;
        GET_EFLAGS(eflags);
        return (EFLAGS_IF & eflags)?INTR_ON:INTR_OFF;
}
kernel/source/interrupt.h:
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"

typedef void* intr_handler;

void idt_init(void);

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

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);                    // 关中断

#endif

在这里插入图片描述

实现 ASSERT

ASSERT 是用来辅助程序调试的,我们把程序该有的条件状态传递给他,他来监督,一旦条件不符合就报错并将程序挂起

kernel/source/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__  *******************************
 * __VA_ARGS__ 是预处理器所支持的专用标识符。
 * 代表所有与省略号相对应的参数. 
 * "..."表示定义的宏其参数可变.
 * */
#define PANIC(...) panic_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)

#ifdef NDEBUG       // 不用调试的时候, 在程序里定义 NDEBUG 即可删除这里的处理逻辑
    #define ASSERT(CONDITION) ((void) 0)
#else
    #define ASSERT(CONDITION)                                   \
            if (CONDITION) {} else {                            \
                /* 符号#让编译器将宏的参数转化为字符串字面量 */        \
                PANIC(#CONDITION);                              \
            }                                                   \

#endif  /* __NDEBUG */

#endif  /* __KERNEL_DEBUG_H */
  • PANIC后面是(…),我们知道括号中应该是形参,其实这是C预处理器所支持的一种用法,它允许宏支持个数不固定的参数,…"表示所定义的宏其参数可变,术语为参数个数可变的宏,只要括号中用“….”来占位,就表示此宏的参数个数不固定。悄悄说下,printf 也支持参数个数可变
  • 预处理器提供了一个标识符_VA_ARGS_,它只允许在具有可变参数的宏替换列表中出现,它代表所有与省略号“…”相对应的参数。该参数至少有一个,但可以为空。
  • __FILE__, __LINE__, __func__,这三个是预定义的宏,分别表示被编译的文件名、被编译文件中的行号、被编译的函数名。
  • 调用PANIC的形式为 PANIC(#CONDITION),即形参为#CONDITION,其中字符#的作用是让预处理器把CONDITION转换成字符串常量。比如CONDITION若为var != 0#CONDITION的效果是变成了字符串“var != 0”

宏是用来传递参数的,这里的关键在于PANIC要怎么处理了:

在这里插入图片描述

kernel/source/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();     // 因为有时候会单独调用 panic_span 所以在此处关中断

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

这里就是关中断后,打印完相关信息让程序悬停在这里

const char* 和 char* const
const char**是表示这个指针指向的是一个常量*
char* const是表示这个指针不能更改 但是指针中的地址指向的数可以更改
前者const 修饰的是*p,他是p所指向的内容为read-only;
而 后者 const 修饰的是p, 即指针变量p本身为 read-only;

接下来让ASSERT工作一下:

kernel/source/main.c:
#include "print.h"
#include "init.h"
#include "debug.h"

int main() {
        put_str("I am kernel\n");
        init_all();
        // asm volatile("sti");    // 为演示中断处理, 在此临时开中断
        ASSERT(1 == 2);
        while (1);

        return 0;
}

上次把中断打开是为了演示中断,现在暂时给关了,等时机成熟了再打开

通过 makefile 进行编译

这里进行接下来的内容之前,先把当前乱糟糟的文件重新整理一下,以便后续编写 makefile;

整理后的文件目录(之后的文件位置均以本次整理后新的位置为准):

.
├── boot
│   ├── include
│   │   └── boot.inc
│   ├── loader.asm
│   ├── loader.bin
│   ├── makefile
│   ├── mbr.asm
│   └── mbr.bin
├── build
│   ├── debug.o
│   ├── init.o
│   ├── interrupt.o
│   ├── kernel.bin
│   ├── kernel.map
│   ├── kernel.o
│   ├── main.o
│   ├── print.o
│   └── timer.o
├── device
│   ├── timer.c
│   └── timer.h
├── kernel
│   ├── debug.c
│   ├── debug.h
│   ├── global.h
│   ├── init.c
│   ├── init.h
│   ├── interrupt.c
│   ├── interrupt.h
│   ├── kernel.asm
│   └── main.c
├── lib
│   ├── kernel
│   │   ├── io.h
│   │   ├── print.asm
│   │   ├── print.h
│   │   └── stdint.h
│   ├── stdint.h
│   └── user
└── makefile
  • 目录boot:下是 MBR 和 Loader
  • 目录build:存放目标文件和编译出来的可执行程序
  • 目录device:存放和外设有关的源代码
  • 目录kernel:存放内核代码
  • 目录lib:存放封装的调用库

整理完了,接下来编写 makefile 了:

BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
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 \
       $(BUILD_DIR)/debug.o 


############ C 代码编译 ##############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h	\
					 lib/stdint.h kernel/init.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
        			 lib/stdint.h kernel/interrupt.h device/timer.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
        				  lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/stdint.h \
        			  lib/kernel/io.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
        			  lib/kernel/print.h lib/stdint.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@


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


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


##############    链接所有目标文件    #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
	$(LD) $(LDFLAGS) $^ -o $@


.PHONY: mk_dir hd clean all

mk_dir:
	if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi

hd:
	sudo dd if=$(BUILD_DIR)/kernel.bin \
            of=/home/steven/source/os/bochs/hd60M.img \
            bs=512 count=200 seek=9 conv=notrunc

clean:
	cd $(BUILD_DIR) && rm -f ./*

build: $(BUILD_DIR)/kernel.bin

all: mk_dir build hd

参数变量CFLAGS 中定义了-fno-builtin,它是告诉编译器不要采用内部函数,因为咱们在以后实现中会自定义与内部函数同名的函数,如果不添加此选项的话,编译时gcc 会提示与内部函数冲突。

-Wstrict-prototypes选项要求函数声明中必须有参数类型,否则编译时发出警告。

-Wmissing-prototypes选项要求函数必须有声明,否则编译时发出警告。

运行 Bochs

编译:make all,运行:

在这里插入图片描述

实现字符串操作函数

为了方便以后的各种工作,基础工作也得做

lib/string.c
#include "string.h"
#include "global.h"
#include "debug.h"

/* 将 dst_ 起始的 size 个字节设置为 value */
void memset(void* dst_, uint8_t value, uint32_t size) {
    ASSERT(dst_ != NULL);
    uint8_t* dst = (uint8_t*) dst_;
    while ((size--) > 0) {
        *dst++ = value;
    }
}


/* 将 src_ 起始的 size 个字节复制到 dst_ */
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++;
    }
}


/* 连续比较以地址 a_ 和地址 b_ 开头的 size 个字节,若相等则返回 0, 若 a_ 大于 b_, 返回 +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_++));
    return r;
}


/* 返回字符串长度 */
uint32_t strlen(const char* str) {
    ASSERT(str != NULL);
    const char* p = str;
    while (*p++);
    return (p - str - 1);
}


/* 比较两个字符串, 若 a_ 中的字符大于 b_ 中的字符返回 1, 相等时返回 0, 否则返回 -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 , 利用布尔表达式实现一个判断三种输出
    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;
        }
        str++;
    }
    return NULL;
}


/* 从后往前查找字符串 str 中首次出现 字符ch的地址 */
char* strrchr (const char* str, const uint8_t ch) {
    ASSERT(str != NULL);
    const char* last_char = NULL;
    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;
    // 当 *src_ != 0时, a = b 在 c 语言返回右值, 在 c++ 返回左值
    while ((*str++ = *src_++));
    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;
}
lib/string.h
#ifndef __LIB_STRING_H
#define __LIB_STRING_H

#include "stdint.h"
#define NULL 0

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* str, const uint8_t ch);

char* strrchr(const char* str, const uint8_t ch);

char* strcat(char* dst_, const char* src_);

uint32_t strchrs(const char* str, uint8_t ch);

#endif

在这里插入图片描述

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 \
      $(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o

$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
        lib/stdint.h kernel/init.h lib/string.h
	$(CC) $(CFLAGS) $< -o $@
$
(BUILD_DIR)/string.o: lib/string.c lib/string.h \
	kernel/debug.h kernel/global.h
	$(CC) $(CFLAGS) $< -o $@

稍加修改的main.c

#include "print.h"
#include "init.h"
#include "debug.h"

int main() {
        put_str("I am kernel\n");
        init_all();
        // asm volatile("sti");    // 为演示中断处理, 在此临时开中断
        ASSERT(strcmp("test", "test"));
        while (1);

        return 0;
}

在这里插入图片描述

位图 bitmap 及其函数的实现

位图简介

位图,bitmap,用于资源管理,主要用于管理大容量资源,如硬盘和内存

位图就是用1位来映射其他单位大小的资源,按位与资源一对一的对应关系

位图本质上就是一串二进制位,用字节型数组来存储比较方便

举个例子:如果用位图管理内存,则用一位对应4KB物理内存,也就是一页的大小:

  • 如果某位为 0 则表示该页未分配,可以使用
  • 如果某位为 1 则表示该页已分配,在回收之前不可再分配使用

位图的定义与实现

global.h

#define NULL ((void*)0)
#define bool int
#define true 1
#define false 0
lib/kernel/bitmap.h
#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;
};

/* 将位图btmp初始化 */
void bitmap_init(struct bitmap* btmp);

/* 判断 bit_idx 位是否为 1 ,若为 1, 则返回true, 否则返回false */ 
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx);

/* 在位图中申清连续 cnt 个位, 成功, 则返回其起始位下标, 失败, 返回 -1 */
int bitmap_scan(struct bitmap* btmp, uint32_t cnt);

/* 将位图 btmp 的 bit_idx 位设置为 value */
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value);

#endif

位图长度取决于所管理资源的大小,其长度不固定, 只能在 struct bitmap 中提供位图的指针,就是uint8_t" bits。

用指针bits来记录位图的地址,真正的位图由上一级模块提供,并由上一级模块把位图的地址赋值给bits。

lib/kernel/bitmap.c
#include "bitmap.h"
#include "stdint.h"
#include "string.h"
#include "print.h"
#include "interrupt.h"
#include "debug.h"

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


/* 判断 bit_idx 位是否为 1 ,若为 1, 则返回true, 否则返回false */ 
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)); // 00x00000, 判断第x位是否为1
}


/* 在位图中申清连续 cnt 个位, 成功, 则返回其起始位下标, 失败, 返回 -1 */
int bitmap_sacn(struct bitmap* btmp, uint32_t cnt) {
    uint32_t idx_byte = 0;  // 用于记录空闲位所在的字节
    /* 先逐字节比较,蛮力法 */
    // 一位16进制代表4位二进制, 所以 0xff 代表一字节且该字节全为 1
    while ((0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) {
        idx_byte++;
    }

    /* 如果找不到可用空间就返回 -1 */
    ASSERT(idx_byte < btmp->btmp_bytes_len);
    if (idx_byte == btmp->btmp_bytes_len) {
        return -1;
    }

    /* 若在位图数组范围内的某字节内找到了空闲位, 在该字节内逐位比对, 返回空闲位的索引。*/
    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; // 空闲位在位图中的下标
    if (cnt == 1) {
        return bit_idx_start;
    }

    uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); // 记录还有多少位可以判断
    uint32_t next_bit = bit_idx_start + 1;
    uint32_t count = 1;                                             // 记录找到空闲位的个数

    bit_idx_start = -1;                                             // 先将其置为-1, 若找不到连续的位就返回-1
    while ((bit_left--) > 0) {
        if (!bitmap_scan_test(btmp, next_bit)) {
            // 若 next_bit为 0
            count++;
        } else {
            // 空间不连续, 重置连续空间数为0, 继续寻找连续空间
            count = 0;
        }

        // 若找到连续的 cnt 个空位
        if (count == cnt) {
            bit_idx_start = next_bit - cnt + 1;
            break;
        }
        next_bit++;
    }
    return bit_idx_start;
}


/* 将位图 btmp 的 bit_idx 位设置为 value */
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;     // 取余用于索引数组内的位

    if (value) {
        // 如果 vaule 为 1, 按位或
        btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
    } else {
        // 如果 vaule 为 0, 取反再按位与
        // 取反: 将除所要操作的位置 0, 其余位全部置 1
        // 按位与: 1 & 1 = 1, 0 & 1 = 0, 即其余位不受影响, 目标位置 0
        btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);
    }
}

代码不难,很容易看懂,4个函数:

  • void bitmap_init (struct bitmap* btmp);

    将位图字节数组地址开始,以数组长度进行清零

  • bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx);

    已知位图位索引,在数组中找到这个位,判断是1还是0

  • int bitmap_scan(struct bitmap* btmp, uint32_t cnt);

    逐字节寻找可用位,找到了后判断需要几个位,如果是一个就可以返回位的位置了

    如果是多个,则循环判断当前找到的可用位下一位是否可用,如果可用,计数+1,如果不可用,计数清零,当可用计数和需要的数量相同时,则返回这段可用位的开始位的位置,否则失败返回-1

  • void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value);

    找到这个位,设置为1的话,就按位或,设置为0的话,就逆然后按位与

在这里插入图片描述

内存管理系统

用户进程所占用的空间是由操作系统分配的,内存如何分配并且分配多少?这就是本节要解决的问题。

内存池规划

在实模式下,内存地址就是物理内存地址

在保护模式下,引入了虚拟内存和分页机制,虚拟内存到物理内存之间会经过分页机制来映射,那么问题来了,如何分配不同进程之间虚拟内存到物理内存的映射呢?这里引入了内存池;

内存池也叫内存地址池,将可用的地址都装在一起,需要用的时候,从里面取出来用,用完了再放回去

由于在分页机制下出现了虚拟内存和物理内存,所以需要为这两个内存分别都创建对应的内存池

  • 物理内存地址池

    物理内存分为两部分:

    • 内核物理内存池,只给内核用
    • 用户物理内存池,只给用户用

    方便起见,这里把内核物理内存池和用户物理内存池分配成一样大小

  • 虚拟内存地址池

    在分页模式下,每个程序都在自己的虚拟内存中运行,访问的是虚拟内存,访问时会自动经过硬件根据页转换成物理内存

    每个程序都有自己的页表,程序使用的虚拟内存地址由链接器决定,之后就不再变了

    • 对于内核进程,申请内存地址时,从内核的虚拟内存地址池中分配虚拟地址,再从内核物理内存池中分配物理内存,然后在自己的页表中将两个地址建立好映射关系
    • 对于用户进程,申请内存地址时,从内核的虚拟内存地址池中分配虚拟地址,再从用户物理内存池中分配物理内存,然后在自己的页表中将两个地址建立好映射关系

内存池分配中的内存的单位是页,也就是4KB

有了物理和虚拟内存池之后,他们的关系如图:

在这里插入图片描述

具体是怎么实现的,还是得看代码:

kernel/memory.h
#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;

void mem_init(void);

#endif

虚拟地址也是需要分配的,链接器保证虚拟地址的唯一性,但是程序内申请的内存空间也需要保证唯一性,所以需要先知道虚拟地址的使用情况,所以创建一个结构用来保存虚拟地址池

kernel/memory.c

0xc009f000是内核主线程栈顶,0xc009e000是内核主线程的pcb

pcb是进程或线程的“身份证”,每个进程都要有,占一个自然页

一页大小的位图可以表示128MB内存,这里支持4页大小,即512MB,故减去4个页的大小,0xc009a000处是位图的地址

把位图地址选在1M一下是为了方便内存管理,因为1M一下的内存几乎都被占用了,1M以上我们的页表占用了空间256个页的大小

这里代码主要内容是初始化内存池,创建一个内存池需要做一些准备,需要知道该内存池的物理起始地址,内存池字节容量,位图起始地址,位长度:

  • 获取空闲的总页数,然后分成两份,一份给内核,一份给用户
  • 物理起始地址就是我们1M之后加上页表所占空间之后的位置,内核在物理空间低部分,用户在高部分
  • 主要就是建立内存池结构,设置起始位置和大小

内存池结构准备完成之后,将物理内存池位图清理,将内核虚拟内存池位图初始化

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

#define PG_SIZE 4096    // 页的大小: 4k


/************************** 位图地址 *******************************************
 * 因为 0xc009f000 是内核主线程栈顶, 0xc009e000 是内核主线程的pcb(pcb占用1页 = 4k).
 * 一个页框大小的位图可表示128M内存, 位图位置安排在地址0xc009a000,
 * 这样本系统最大支持4个页框的位图, 即512M
/******************************************************************************/
#define MEM_BITMAP_BASE 0xc009a000


/* 0xc0000000是内核从虚拟地址3G起. 0x100000意指跨过低端1M内存, 使虚拟地址在逻辑上连续 */
#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; // 生成内核物理内存池和用户物理内存池
struct virtual_addr kernel_vaddr;   // 此结构用来给内核分配虚拟地址


/* 初始化内存池 */
static void mem_pool_init(uint32_t all_mem) {
    put_str("    mem_pool_init start\n");

    // 页表大小 = 1页的页目录表 +第 0 和第 768 个页目录项指向同一个页表, 之前创建页表的时候, 挨着页目录表创建了768-1022总共255个页表 + 上页目录的1页大小, 就是256
    // 第 769~1022 个页目录项共指向 254 个页表, 共 256 个页框
    uint32_t page_table_size = PG_SIZE * 256;           // 记录页目录表和页表占用的字节大小

    uint32_t used_mem = page_table_size + 0x100000;     // 当前已经使用的内存字节数, 1M部分已经使用了, 1M往上是页表所占用的空间
    uint32_t free_mem = all_mem - used_mem;             // 剩余可用内存字节数
    uint16_t all_free_pages = free_mem / PG_SIZE;       // 所有可用的页
    // 1页为 4KB, 不管总内存是不是 4k 的倍数, 对于以页为单位的内存分配策略, 不足 1 页的内存不用考虑了

    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;        // Kernel Bitmap的长度, 位图中的一位表示一页, 以字节为单位, 也就是8页表示1字节的位图
    uint32_t ubm_length = user_free_pages / 8;          // User Bitmap 的长度

    uint32_t kp_start = used_mem;                                   // kernel pool start, 内核内存池起始地址
    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;

    /*********    内核内存池和用户内存池位图   ***********
    *   位图是全局的数据, 长度不固定。
    *   全局或静态的数组需要在编译时知道其长度,
    *   而我们需要根据总内存大小算出需要多少字节。
    *   所以改为指定一块内存来生成位图.
    *   ************************************************/
    // 内核使用的最高地址是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");
}

在这里插入图片描述

makefile

新增如下内容:

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 \
       $(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
	   $(BUILD_DIR)/bitmap.o
	   

$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
					   kernel/debug.h kernel/global.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
					   lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h
	$(CC) $(CFLAGS) $< -o $@
	

$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \
					   lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@
kernel/init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"

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

运行 Bochs

编译运行:

在这里插入图片描述

内存管理系统第1步–分配页内存

完成了内存池的划分之后,接下来还要继续做基础工作,现在距离malloc函数还挺远,当前要做的工作是按页分配内存

kernel/memory.h

内存管理中,必不可少的就是修改页表,需要一些页表相关的宏定义

页表的地址都是真实的物理地址,页表的工作是将虚拟地址转换成物理地址

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

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

添加如下内容:

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

#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)   // 返回虚拟地址高10位, 用于定位pde
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)   // 返回虚拟地址中间10位, 用于定位pte

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

        // 将位起始值开始连续置1,直到设置完需要的页位置
        while (cnt < pg_cnt) {
            bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt, 1);
            cnt++;
        }
        // 获取起始页的虚拟地址
        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;

    } else {
        // 用户内存池,将来实现用户进程再来补充
    }

    return (void*) vaddr_start;
}


/* 得到虚拟地址vaddr计算得到对应的pte指针(虚拟地址) */
uint32_t* pte_ptr(uint32_t vaddr) {
    // 先访问到页表自己
    // 再用页目录项 pde(页目录内页表的索引)作为pte的索引访问到页表
    // 再用pte的索引作为页内偏移

    // 第一步:0xffc00000 是取出第1023个页目录项进行索引, 其实就是页目录表的物理地址
    // 第二步:((vaddr & 0xffc00000) >> 10) 是将原来vaddr的前10位取出, 放在中间10位的位置上 用来获取 pte 的
    // 第三步:PTE_IDX(vaddr) * 4 会被当作物理偏移直接加上, 而不会像其前面10位会被cpu自动*4再加上, 所以这里手动*4, 获取PTE索引, 得到PTE物理地址
    uint32_t* pte = (uint32_t*) (0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
    return pte;
}


/* 得到虚拟地址vaddr计算得到对应的pde的指针(虚拟地址) */
uint32_t* pde_ptr(uint32_t vaddr) {
    // 0xfffff 用来访问到页表本身所在的地址
    // 前10位是1023, 是页目录表的物理地址
    // 中10位是1023, 索引到的还是页目录表的物理地址
    // 后12位是addr的前10位*4, 也就是页目录表的索引
    uint32_t* pde = (uint32_t*) ((0xfffff000) + PDE_IDX(vaddr) * 4);
    return pde;
}


/* 
 * 在 m_pool 指向的物理内存池中分配 1 个物理页,
 * 成功则返回页框的物理地址, 失败则返回 NULL 
 * */
static void* palloc(struct pool* m_pool) {
    // 扫描或设置位图要保证原子操作
    int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1);     // 找一个物理页面, 位图中1位表示实际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;
}

这里的pte_ptrpde_ptr比较复杂,需要我们先来回顾一下二级页表的原理:

在这里插入图片描述

CPU 会自动将地址进行处理,转换的结果是物理地址,其中进行了三步骤:

  1. 取出前10位*4,作为索引,加到cr3寄存器中所保存的pde物理地址上,得到pte的物理地址
  2. 取出中10位*4,作为索引,加到刚刚获得的pte的物理地址上,得到实际的物理地址
  3. 取出后12位,作为偏移量,加到刚刚获得的实际物理地址,得到最终物理地址

这里获取pte地址的方法则是:

  1. 构造前10位为1023,也就是0xffc00000,用来索引第1023个页目录项,1023个页目录项存入的地址还是pde的地址
  2. 取出要获取的vaddr的前10位,右移10位,变成中间10位*4,作为索引,加到刚刚获取到的pde地址上,得到pte的物理地址
  3. 取出要获取的vaddr的中10位,右移12位,变成后12位,手动*4,作为pte的偏移,自动加到pte地址上之后,得到实际上的pte地址

获取pde地址原理类似

此处的vaddr与它所在的pde,pte是否存在无关,仅仅是用构造的vaddr获取该地址的pte和pde地址

kernel/memory.c

添加如下内容:

/* 页表中添加虚拟地址 _vaddr 与物理地址 _page_phyaddr 的映射 */
static void page_table_add(void* _vaddr, void* _page_phyaddr) {
    uint32_t vaddr = (uint32_t) _vaddr;
    uint32_t 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));       // 此时pte应该不存在

        // 只要是创建页表, pte就应该不存在, 多判断一下放心
        if (!(*pte & 0x00000001)) {
            *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);     // 创建pte
        
        } else {
            // 目前执行不到这里
            PANIC("pte repeat");
            *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
        }

    } else {
        // 页目录项不存在, 所以要先创建页目录项再创建页表项
        // 页表中用到的页框一律从内核空间分配
        // pte 和 pde 都是真实的 pde 和 pte物理地址对应的虚拟地址 
        // pde_phyaddr是物理地址
        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对应的物理页的起始"虚拟地址"
        * */

        // 把分配到的物理页地址 pde_phyaddr(物理地址) 对应的物理内存清 0
        // (int) pte & 0xfffff000: vaddr 所在页表的虚拟地址, 即 pde_phyaddr 的虚拟地址
        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 个页空间, 成功则返回起始虚拟地址, 失败时返回 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;
    uint32_t 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;
}

page_table_add函数是建立虚拟地址和物理地址的映射,所以需要修改pde和pte的内容,通常情况下,pte是不存在的,创建pte之前需要先创建好pde,通过palloc从物理内存池获取一个页的大小,存入pde,将pte所在位置那一页清空,然后写入pte。

虚拟地址用来获取页表地址,页表地址里存储的是对应物理地址的映射,所以要先通过虚拟地址获取页表地址,然后修改页表映射到物理地址,其中页表项的内存也是从物理内存池中申请的

后面两个函数很好看懂,就不解释了

kernel/main.c

修改main来测试一下:

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

int main() {
        put_str("I am kernel\n");
        init_all();

        // asm volatile("sti");    // 为演示中断处理, 在此临时开中断
        
        void* addr = get_kernel_pages(5);
        put_str("\n get_kernel_page start vaddr is: ");
        put_int((uint32_t) addr);
        put_str("\n");

        while (1);
        return 0;
}

这里就是申请一块内存空间

完整代码:

memory.h

#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H

#include "stdint.h"
#include "bitmap.h"

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

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

/* 用于虚拟地址管理 */
struct virtual_addr {
    struct bitmap vaddr_bitmap;     // 虚拟地址用到的位图结构
    uint32_t vaddr_start;           // 虚拟地址起始地址
};

extern struct pool kernel_pool, user_pool;

void mem_init(void);

/* 从内核物理内存池中申请 pg_cnt 页内存, 成功则返回其虚拟地址, 失败则返回 NULL */
void* get_kernel_pages(uint32_t pg_cnt);

/* 分配 pg_cnt 个页空间, 成功则返回起始虚拟地址, 失败时返回 NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt);

void malloc_init(void);

/* 得到虚拟地址vaddr对应的pte指针 */
uint32_t* pte_ptr(uint32_t vaddr);

/* 得到虚拟地址vaddr对应的pde指针 */
uint32_t* pde_ptr(uint32_t vaddr);

#endif

memory.c

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

运行Bochs:

运行:

在这里插入图片描述

在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值