《操作系统真象还原》第八章
本篇对应书籍第八章的内容
本篇内容介绍了使用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_ptr
和pde_ptr
比较复杂,需要我们先来回顾一下二级页表的原理:
CPU 会自动将地址进行处理,转换的结果是物理地址,其中进行了三步骤:
- 取出前10位*4,作为索引,加到cr3寄存器中所保存的pde物理地址上,得到pte的物理地址
- 取出中10位*4,作为索引,加到刚刚获得的pte的物理地址上,得到实际的物理地址
- 取出后12位,作为偏移量,加到刚刚获得的实际物理地址,得到最终物理地址
这里获取pte地址的方法则是:
- 构造前10位为1023,也就是0xffc00000,用来索引第1023个页目录项,1023个页目录项存入的地址还是pde的地址
- 取出要获取的vaddr的前10位,右移10位,变成中间10位*4,作为索引,加到刚刚获取到的pde地址上,得到pte的物理地址
- 取出要获取的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:
运行: