Songqian Li's Blog

去历史上留点故事

8.1 makefile 简介

这部分可参考阮一峰的讲解:https://www.ruanyifeng.com/blog/2015/02/make.html

8.1.1 makefile 是什么

makefile 是 Linux 下编译大型程序的工具,是 make 程序的搭档,两者的主要功能就是发现某个文件更新后,只编译该文件和受该文件影响的相关文件,从而提高了编译效率。
make 和 makefile 并不是用来编译程序的,它只负责找出哪些文件有变化, 并 且根据依赖关系找出受影响的文件,然后执行事先在 makefile 中定义好的命令规则。因为 make 就是在 shell 下执行的,所以在 makefile 中,位于命令规则里的那些命令, 都是 shell 命令。

8.1.2 makefile 基本语法

1
2
目标文件:依赖文件
[Tab]命令

makefile 基本语法包括三部分,这三部分加在一起称为一组规则:

  1. 目标文件是指此规则中想要生成的文件,可以是.o 结尾的目标文件,也可以是可执行文件或伪目标。
  2. 依赖文件是指要生成此规则中的目标文件,需要哪些文件。通常依赖文件不是 1 个,所以此处是个依赖文件的列表。
  3. 命令是指此规则中要执行的动作,这些动作是指各种 shell 命令。命令可以有多个,但一个命令要单独占一行,行首必须以 Tab 开头。

    Linux 中文件分为属性和数据两部分,每个文件有三种时间:

    1. atime,即 access time,表示访问文件数据部分时间,每次读取文件数据部分时就会更新 atime。
    2. ctime,即 change time,表示文件属性或数据的改变时间。
    3. mtime,即 modify time,表示文件数据部分的修改时间。

make 程序分别获取依赖文件和目标文件的 mtime,对比依赖文件是否比目标文件的 mtime 新,从而得知是否要执行规则中的命令。
举例说明:

1
2
file1:file2
@echo "makefile test ok"

文件的含义是:若 file2 比 file1 新,则输出“makefile test ok”。“@”是关闭打印命令本身。
makefile 的文件名不固定,可以在执行 make 时用-f 参数执行。如果未用-f 指定,默认 make 会按照GNUmakefilemakefileMakefile的顺序找 makefile 文件。

8.1.3 跳到目标处执行

如果 makefile 有多个目标,可以用make target_name来执行,如果不指定目标,则默认执行第一个目标。

8.1.4 伪目标

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

可以使用关键字.PHONY来明确声明伪目标,例如:

1
2
3
.PHONY:clean
clean:
rm ./build/*.o

image.png
image.png

8.1.5 make: 递归式推导目标

举例说明:

1
2
3
4
5
6
7
8
test1.o:test1.c
gcc -c -o test1.o test1.c
test2.o:test2.c
gcc -c -o test2.o test2.c
test.bin:test1.o test2.o
gcc -o test.bin test1.o test2.o
all:test.bin
@echo "compile done"

执行make all时,make 会递归执行 all,依赖文件不存在或不是最新就去找该依赖文件名命名的规则执行,执行完后再找下一个依赖文件。故最终执行顺序是:test1.o、test2.o、test.bin、all。

8.1.6 自定义变量与系统变量

自定义变量仅支持字符串类型,即使是数字也被当做字符串来处理,变量引用格式:$(变量名),举例说明:

1
2
3
4
5
6
7
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"

还有一些系统级的变量:
image.png
在命令相关的系统变量是有默认值的,一般参数相关的变量没有默认值。

8.1.7 隐含规则

  1. 行尾添加反斜杠字符’',下一行内容会被认为是同一行;
  2. 可以用’#'进行单行注释;
  3. 语言程序的隐含规则:
    1. C 程序:“x.o"的生成依赖于"x.c”,生成 x.o 的命令为:$(CC) -c $(CPPFLAGS) $(CFLAGS)
    2. C++程序:“x.o"的生成依赖于"x.cc”,生成 x.o 的命令为:$(CXX) -c $(CPPFGLAGS) $(CFLAGS)
    3. Pascal 程序:“x.o"的生成依赖于"x.p”,生成 x.o 的命令为:$(PC) -c $(PFLAGS)

8.1.8 自动化变量

  1. $@,表示规则中的目标文件名集合。
  2. $<,表示规则中依赖文件中的第 1 个文件。
  3. $^,表示规则中所有依赖文件的集合。
  4. $?,表示规则中所有比目标文件 mtime 更新的依赖文件集合。
1
2
3
4
5
6
7
8
test1.o:test1.c
gcc -c -o test1.o test1.c
test2.o:test2.c
gcc -c -o test2.o test2.c
test.bin:test1.o test2.o
gcc -o $@ $^
all:test.bin
@echo "compile done"

8.1.9 模式规则

%用来匹配任意多个非空字符,如果是匹配文件,默认在当前路径下匹配。

1
2
3
4
5
6
7
%.o:%.c
gcc -c -o $@ $^
objfiles = test1.o test2.o
test.bin:$(objfiles)
gcc -o $@ $^
all:test.bin
@echo "compile done"

8.2 实现 assert 断言

8.2.1 实现开、关中断的函数

在相应文件添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#define EFLAGS_IF   0x00000200       // eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR))


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

/**
* @description 关中断
* @return 中断前的状态
*/
enum intr_status intr_disable() {
if (INTR_ON == intr_get_status()) {
asm volatile("cli" : : : "memory"); // 关中断,cli指令将IF位置0
return INTR_ON;
} else {
return INTR_OFF;
}
}

/**
* 将中断状态设置为status
* @param status 中断状态
* @return 中断前状态
*/
enum intr_status intr_set_status(enum intr_status status) {
return status & INTR_ON ? intr_enable() : intr_disable();
}

/**
* @description 获取当前中断状态
* @return 当前中断状态
*/
enum intr_status intr_get_status() {
uint32_t eflags = 0;
GET_EFLAGS(eflags);
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}
1
2
3
4
5
6
7
8
9
10
11
12
/* 定义中断的两种状态:
* 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);

8.2.2 实现 ASSERT

ASSERT 的原理是判断传给 ASSERT 的表达式是否成立,若表达式成立则什么都不做,否则打印出错信息并停止执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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
#define ASSERT(CONDITION) ((void)0)
#else
#define ASSERT(CONDITION) \
if (CONDITION) {} else { \
/* 符号#让编译器将宏的参数转化为字符串字面量 */ \
PANIC(#CONDITION); \
}
#endif //NDEBUG
#endif //__KERNEL_DEBUG_H

第 10 行中的宏:

  1. __FILE__表示被编译的文件名;
  2. __LINE__表示编译文件中的行号;
  3. __func__表示被编译的函数名;
  4. __VA_ARGS__是可变参数...的表示符,它代表所有与省略号相对应的参数。

第 19 行的#CONDITION,其中字符#的作用是让预处理器把 CONDITION 转换成字符串常量,比如 CONDITION 为var!=0,#CONDITION 则为字符串"var!=0"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "debug.h"
#include "print.h"
#include "interrupt.h"

/**
* @description 打印文件名,行号,函数名,条件并使程序悬停
* @param filename 文件名
* @param line 行号
* @param func 函数名
* @param condition 条件
*/
void panic_spin(char* filename,
int line,
const char* func,
const char* condition)
{
intr_disable(); // 因为有时候会单独调用panic_spin,所以在此处关中断。
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);
}

然后在 main.c 中测试一下:

1
2
3
4
5
6
7
8
9
10
11
#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;
}

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

8.2.3 通过 makefile 来编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
BUILD_DIR = ./build
DISK_IMG = hd60M.img
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
ASBINLIB = -I boot/include/
CFLAGS = -m32 -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -fno-stack-protector
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

############## MBR代码编译 ###############
$(BUILD_DIR)/mbr.bin: boot/mbr.S
$(AS) $(ASBINLIB) $< -o $@

############## bootloader代码编译 ###############
$(BUILD_DIR)/loader.bin: boot/loader.S
$(AS) $(ASBINLIB) $< -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.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 mk_img mbr2hd loader2hd kernel2hd clean all

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

mk_img:
if [ ! -e $(DISK_IMG) ];then /usr/bin/bximage -hd -mode="flat" -size=60 -q $(DISK_IMG);fi

mbr2hd:
$(AS) $(ASBINLIB) -o $(BUILD_DIR)/mbr.bin boot/mbr.S
dd if=$(BUILD_DIR)/mbr.bin of=$(DISK_IMG) bs=512 count=1 conv=notrunc

loader2hd:
$(AS) $(ASBINLIB) -o $(BUILD_DIR)/loader.bin boot/loader.S
dd if=$(BUILD_DIR)/loader.bin of=$(DISK_IMG) bs=512 count=4 seek=2 conv=notrunc
kernel2hd:
dd if=$(BUILD_DIR)/kernel.bin \
of=$(DISK_IMG) \
bs=512 count=200 seek=9 conv=notrunc

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

build: $(BUILD_DIR)/kernel.bin

all: mk_dir build kernel2hd

执行make all
image.png

8.3 实现字符串操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#include "string.h"
#include "global.h"
#include "debug.h"

/**
* @description 内存赋值函数,将dst_起始的size个字节置为value
* @param dst_ 目的地址
* @param value 数值,一字节大小
* @param size 大小,单位字节
*/
void memset(void* dst_, uint8_t value, uint32_t size) {
ASSERT(dst_ != NULL);
uint8_t* dst = (uint8_t*) dst_;
while (size-- > 0)
*dst++ = value;
}

/**
* @description 内存拷贝函数,将src_起始的size个字节复制到dst_
* @param dst_ 目的地址
* @param src_ 源地址
* @param size 大小,单位字节
*/
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++;
}

/**
* @description 内存比较函数,连续比较以地址a_和地址b_开头的size个字节,若相等则返回0,若a_大于b_返回+1,否则返回-1
* @param a_ 内存块a
* @param b_ 内存块b
* @param size 大小
* @return a>b return 1; a==b return 0; a<b return -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;
}

/**
* @description 字符串拷贝函数,将字符串从src_复制到dst_
* @param dst_ 目的地址
* @param src_ 源地址
* @return 目的地址
*/
char* strcpy(char* dst_, const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* r = dst_; // 用来返回目的字符串起始地址
while ((*dst_++ = *src_++));
return r;
}

/**
* @description 字符串长度函数
* @param str 字符串
* @return 字符串长度
*/
uint32_t strlen(const char* str) {
ASSERT(str != NULL);
const char* p = str;
while (*p++);
return (p - str - 1);
}

/**
* @description 字符串比较函数
* @param a 字符串a
* @param b 字符串b
* @return a>b return 1; a==b return 0; 否则 return -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;
}

/**
* @description 字符查找函数,从左到右查找字符串str中首次出现字符ch的地址(不是下标,是地址)
* @param str 待查找的字符串
* @param ch 要查找的字符
* @return 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;
}

/**
* @description 字符反向查找函数,从后往前查找字符串str中首次出现字符ch的地址(不是下标,是地址)
* @param str 待查找的字符串
* @param ch 要查找的字符
* @return 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;
}

/**
* @description 字符串拼接函数,将字符串src_拼接到dst_后,将回拼接的串地址
* @param dst_ 目的字符串地址
* @param src_ 源字符串地址
* @return 目的字符串地址
*/
char* strcat(char* dst_, const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* str = dst_;
while (*str++);
--str; // 别看错了,--str是独立的一句,并不是while的循环体
while ((*str++ = *src_++)); // 当*str被赋值为0时,此时表达式不成立,正好添加了字符串结尾的0.
return dst_;
}

/**
* @description 字符查找函数,在字符串str中查找指定字符ch出现的次数
* @param str 待查找的字符串
* @param ch 要查找的字符
* @return 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;
}

8.4 位图 bitmap 及其函数实现

8.4.1 位图简介

位图就是用字节中的 1 位来映射其他单位大小的资源,按位与资源之间是一对一的对应关系。位图相当于一组资源的映射,故位图主要用于管理容量较大的资源。

8.4.2 位图的定义与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#include "global.h"
#define BITMAP_MASK 1
struct bitmap {
uint32_t btmp_bytes_len;
/* 在遍历位图时,整体上以字节为单位,细节上是以位为单位,所以此处位图的指针必须是单字节 */
uint8_t* bits;
};

void bitmap_init(struct bitmap* btmp);
int 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);
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include "bitmap.h"
#include "stdint.h"
#include "string.h"
#include "print.h"
#include "interrupt.h"
#include "debug.h"

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

/**
* @description 判断bit_idx位是否为1,若为1则返回1,否则返回0
* @param btmp 位图指针
* @param bit_idx 位索引
* @return bit_idx==1?true:false
*/
int bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx) {
uint32_t byte_idx = bit_idx / 8; // 向下取整用于索引数组下标
uint32_t bit_odd = bit_idx % 8; // 取余用于索引数组内的位
return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd));
}

/**
* @description 在位图中申请连续cnt个位,成功则返回其起始位下标,失败返回-1
* @param btmp 位图指针
* @param cnt 位的个数
* @return 起始位下标
*/
int bitmap_scan(struct bitmap* btmp, uint32_t cnt) {
uint32_t idx_byte = 0; // 用于记录空闲位所在的字节
/* 先逐字节比较,蛮力法 */
while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) {
/* 1表示该位已分配,所以若为0xff,则表示该字节内已无空闲位,向下一字节继续找 */
idx_byte++;
}

ASSERT(idx_byte < btmp->btmp_bytes_len);
if (idx_byte == btmp->btmp_bytes_len) { // 若该内存池找不到可用空间
return -1;
}

/* 若在位图数组范围内的某字节内找到了空闲位,
* 在该字节内逐位比对,返回空闲位的索引。*/
int idx_bit = 0;
/* 和btmp->bits[idx_byte]这个字节逐位对比 */
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,若找不到连续的位就直接返回
while (bit_left-- > 0) {
if (!(bitmap_scan_test(btmp, next_bit))) { // 若next_bit为0
count++;
} else {
count = 0;
}
if (count == cnt) { // 若找到连续的cnt个空位
bit_idx_start = next_bit - cnt + 1;
break;
}
next_bit++;
}
return bit_idx_start;
}

/**
* @description 将位图btmp的bit_idx位设置为0或1
* @param btmp 位图指针
* @param bit_idx 位索引
* @param 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; // 取余用于索引数组内的位

/* 一般都会用个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);
}
}

置 1 用或操作,置 0 用与操作。

8.5 内存管理系统

本节将实现内存管理系统,并将实现 malloc 和 free 函数。

8.5.1 内存池规划

内存地址池的概念是将可用的内存地址集中放到一个“池子”中,需要的时候直接从里面取出, 完后再放回去。由于在分页机制下有了虚拟地址和物理地址,为了有效地管理它们,我们需要创建虚拟内存地址池和物理内存地址池。
物理内存地址池分为两部分:

  1. 内核物理内存池,只用来运行内核
  2. 用户物理内存池,只用来运行用户进程

为了方便实现,这里把这两个内存池的大小设为一致,各占一半的物理内存。
对于虚拟内存地址池:

  • 内核进程从内核虚拟地址池中分配地址,再从内核物理内存池中分配物理内存,然后在内核自己的页表中将两个地址建立好映射关系。
  • 用户进程从用户进程虚拟地址池中分配空闲虚拟地址,然后再从用户物理内存池中分配空闲的物理内存,然后在该用户进程自己的页表将这两种地址建立好映射关系。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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

virtualaddr 是虚拟地址池,用于虚拟地址管理。为了演示分页机制是如何将这两种独立不相关的地址关联到一起的,特意加了该结构体用于管理虚拟地址。
struct virtual
addr 包含两个成员,一个是 vaddr_bitmap,用来以页位单位管理虚拟地址的分配情况。虽然多个进程可以有相同的虚拟地址,但其实是因为这些虚拟地址对应的物理地址是不同的,在同一进程内的虚拟地址必然是唯一的,这是由链接器为其分配,由链接器负责虚拟地址的唯一性。另一个是 vaddr_start ,将来在分配虚拟地址时将以这个地址为起始分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include "memory.h"
#include "stdint.h"
#include "print.h"

#define PG_SIZE 4096 // 页大小

/*************** 位图地址 ********************
* 因为0xc009f000是内核主线程栈顶,0xc009e000是内核主线程的pcb.
* 一个页框大小的位图可表示128M内存, 位图位置安排在地址0xc009a000,
* 这样本系统最大支持4个页框的位图,即512M */
/*************************************/
#define MEM_BITMAP_BASE 0xc009a000
#define K_HEAP_START 0xc0100000 // 0xc0000000是内核从虚拟地址3G起. 0x100000意指跨过低端1M内存,使虚拟地址在逻辑上连续

// 内存池结构,生成两个实例用于管理内核内存池和用户内存池
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; // 此结构是用来给内核分配虚拟地址

/**
* @description 初始化内存池
* @param all_mem
*/
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; // 0x100000为低端1M内存
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE; // 1页为4k,不管总内存是不是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的长度,位图中的一位表示一页,以字节为单位
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; // 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("\n");
put_str(" kernel_pool_bitmap_end:");
put_int((int) kernel_pool.pool_bitmap.bits + kernel_pool.pool_bitmap.btmp_bytes_len);
put_str("\n");
put_str(" kernel_pool_phy_addr_start:");
put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str(" kernel_pool_phy_addr_end:");
put_int(kernel_pool.phy_addr_start + kernel_pool.pool_size);
put_str("\n");
put_str(" user_pool_bitmap_start:");

put_int((int) user_pool.pool_bitmap.bits);
put_str("\n");
put_str(" user_pool_bitmap_end:");
put_int((int) user_pool.pool_bitmap.bits + user_pool.pool_bitmap.btmp_bytes_len);
put_str("\n");
put_str(" user_pool_phy_addr_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");
put_str(" user_pool_phy_addr_end:");
put_int(user_pool.phy_addr_start + user_pool.pool_size);
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;
put_str(" kernel_vaddr.vaddr_bitmap.start:");
put_int((int) kernel_vaddr.vaddr_bitmap.bits);
put_str("\n");
put_str(" kernel_vaddr.vaddr_bitmap.end:");
put_int((int) kernel_vaddr.vaddr_bitmap.bits + kernel_vaddr.vaddr_bitmap.btmp_bytes_len);
put_str("\n");

bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str(" mem_pool_init done\n");
}

/**
* @description 内存管理部分初始化入口
*/
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");
}
  • MEM_BITMAP_BASE用于表示内存位图的基址,其值为0xc009a000。由于 PCB 占用 4KB,内核主线程栈顶0xc009f000,故 PCB 地址为0xc009e00,这里打算支持 4 页位图的内存,故再减去 0x4000,即为所得。位图 1bit 位管理 4KB 内存,一页 4KB 的位图可以管理 128MB 的内存,即最大可管理 512MB 的物理内存。
  • K_HEAP_START 是堆的起始地址,0xc0000000是内核从虚拟地址 3G 起,0x100000意指跨过低端 1M 内存,使虚拟地址在逻辑上连续,故此值为0xc0100000
  • 函数mem_pool_init是根据内存容量大小初始化物理内存池的结构。
    • 变量page_table_size是指目前分页机制已经占用了多少内存。这里包括一页页目录表+255 个内核空间页表(其中包括第 0 页表项和第 768 页表项指向的同一个页表)。
    • 为了简化位图处理,用 kernel_free_pages 除以 8 获得位图的长度,余数不做处理,故内核内存池和用户内存池各会丢 1-7 页的内存。
  • 在函数mem_init中,第 123 行,使用(*(uint32_t * )(0xb00))获取内存容量,是因为在 loader 中我们将获取到的内存容量保存在total_mem_bytes变量中,其物理地址是 0xb00

要注意的是,位图是全局数据结构,全局或静态的数组需要在编译时知道其长度,而位图的长度取决于具体要管理的内存也数量,因此是无法预计的。所以这里对位图的初始化改为指定一块内存来生成位图,这样就不需要固定长度了。
在 makefile 中添加 bitmap 和 memory 的编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \
kernel/global.h lib/stdint.h lib/string.h lib/stdint.h \
lib/kernel/print.h kernel/interrupt.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@

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

运行结果:
image.png
已知物理机 32MB 内存,由图所示,内核物理内存池可分配 15MB,用户物理内存池可分配 15MB,内核物理内存池位图、用户物理内存池位图以及内核虚拟地址位图各占用 480 字节。

8.5.2 内存管理系统第一步,分配页内存

在上一小节的基础上,对 memory.h 和 memory.c 进行改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 内存池标记,用于判断用哪个内存池 */
enum pool_flags {
PF_KERNEL = 1, // 内核内存池
PF_USER = 2 // 用户内存池
};

#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 属性位值, 用户级

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

C 语言中申请内存时,由于我们是用户程序,操作系统直接会在用户内存池中分配内存,但这对应对应到内核中具体的操作时,必须要“显式”指定在哪个内存池中申请,故在 memory.h 中新增了枚举结构 pool_flags 来区分这两个内存池。
在内存管理中必不可少要修改页表,故这里又定义了一些页表项或页目录项的属性。
要注意的是,页表的作用是将虚拟地址转换成物理地址,其转换锅中涉及访问的页目录表、页目录项以及页表项都是通过真是物理地址访问的,若用虚拟地址访问的话会陷入转换的死循环中。
memory.c 中新增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include "bitmap.h"
#include "global.h"
#include "debug.h"
#include "string.h"

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


/**
* @description 在pf表示的虚拟内存池中申请pg_cnt个虚拟页
* @param pf 内存池类型标志
* @param pg_cnt 申请内存的页数
* @return 成功则返回虚拟页的起始地址, 失败则返回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;
}

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

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

/**
* @description 在m_pool指向的物理内存池中分配1个物理页
* @param m_pool 内存池指针
* @return 成功则返回页框的物理地址,失败则返回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;
}

/**
* @description 页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射
* @param _vaddr 虚拟地址
* @param _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
}
}

/**
* @description 分配pg_cnt个页空间
* @param pf 内存池标志
* @param pg_cnt 页数
* @return 成功则返回起始虚拟地址,失败时返回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;
}

/**
* @description 从内核物理内存池中申请pg_cnt页内存
* @param pg_cnt 页数
* @return 成功则返回其虚拟地址,失败则返回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;
}

需要说明的是,函数pte_ptrpde_ptr都是将虚拟地址赋值给指针,从而触发 CPU 进行寻址操作,从而找到实际 pte 或 pde 的地址。
对于函数pte_ptr,pte 指针的值存储在页目录表的指定页目录项中,故重新构造虚拟地址:

  1. 高 10 位用于定位页目录项。页目录地址保存在最后一个页目录项中,也就是页目录表的第 1023 个目录项地址0xffc00000
  2. 中间 10 位用于定位页表项。存储 pte 的页表项其实就是页目录项,故需要用到 vaddr 的高 10 位,执行(vaddr & 0xffc00000) >> 10
  3. 低 12 位用于定位 pte 地址。在第 2 步中我们定位到存储该 pte 的页表地址,该步需要构造寻址 pte 的页表索引。页表索引即为虚拟地址的中间 10 位,故可直接使用PTE_IDX(vaddr)来获得页表索引,但内存地址低 12 位 CPU 不会自动乘 4,故这里需要手动乘 4 从而得到地址。

函数pde_ptr同理。

注意:直接将低 12 位置 0 取得的是 pte 的值,也就是页表的地址,不是 pte 的地址。pde 同理。

函数palloc表示物理内存申请,vaddr_get表示虚拟内存申请,page_table_add表示添加虚拟地址和物理地址映射,malloc_page就按照上述顺序执行操作。
函数page_table_add不仅完成虚拟地址到物理地址的映射,同时还创建了页目录项、页表、页表项,这里使用页表项的P位属性来判断该页是否在内存中存在。
函数malloc_page中考虑到虚拟地址连续但物理地址可能不连续,故一次性申请完虚拟页之后循环申请物理页,每次申请一页,然后再将其关联。
最后进行代码测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "print.h"
#include "init.h"
#include "debug.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;
}

编译后执行:
image.png
使用page 0xc0100000命令可以看到虚拟内存映射情况:
image.png
虚拟地址范围为0xc0100000-0xc0102fff,其所映射的物理地址范围为0x200000-0x202fff
image.png
本章结束。

本章残留问题:未实现内存回收,将在第 12 章时完善。

相关文章
评论
分享
  • 《操作系统真象还原》:第七章 中断

    7.1 中断是什么,为什么要有中断 运用中断能够显著提升并发,从而大幅提升效率。 7.2 操作系统是中断驱动的 略 7.3 中断分类 把中断按事件来源分类,来自 CPU 外部的中断就称为外部中断,来自 CPU 内部的中断称为内部...

    《操作系统真象还原》:第七章 中断
  • 《操作系统真象还原》:第六章 完善内核

    6.1 函数调用约定简介 咱们实验使用cdecl。这里提一下stdcall,cdecl与stdcall的区别在于由谁来回收栈空间。 stdcall是被调用者清理参数所占的栈空间。 举例来说: 12int subtract(int ...

    《操作系统真象还原》:第六章 完善内核
  • 《操作系统真象还原》:第五章 保护模式进阶——加载内核

    5.3 加载内核 5.3.1 用 C 语言写内核 第一个 C 语言代码: 1234int main(void) { while(1); return 0;} 这个内核文件什么都没做,通过while(1)这个死循...

    《操作系统真象还原》:第五章 保护模式进阶——加载内核
  • 《操作系统真象还原》:第五章 保护模式进阶——内存分页机制

    从这一刻起,我们才算开始了真正的操作系统学习之旅 5.1 获取物理内存容量 5.1.1 Linux 获取内存的方法 在 Linux 2.6 内核总是用detect_memory函数来获取内存容量的。其函数本质上是通过调用 BI...

    《操作系统真象还原》:第五章 保护模式进阶——内存分页机制
  • 《操作系统真象还原》:第四章 保护模式入门

    4.1 保护模式概述 在本章大家会见到全局描述符表、中断描述符表、各种门结构,这是 CPU 提供给应用的,咱们用好就行。 保护模式强调的是“保护”,它是在 Intel 80286 CPU 中首次出现,这是继 8086 之后,Inte...

    《操作系统真象还原》:第四章 保护模式入门
  • 《操作系统真象还原》:第三章 完善MBR——I/O接口

    3.3 让我们对显示器说点什么吧 3.3.1 CPU 如何与外设通信——IO 接口 IO 接口功能: 设置数据缓冲,解决 CPU 与外设的速度不匹配 设置信号电平转换电路 设置数据格式转换 设置时序控制电路来同步 CPU 和外部...

    《操作系统真象还原》:第三章 完善MBR——I/O接口
  • 《操作系统真象还原》:第三章 完善MBR——CPU的实模式

    针对汇编 几个知识点: 第 1 行和第 4 行的 mov 操作,机器码第 1 个宇节都是B8,而另外第 2、3 行同样是 mov 指令,机器码却有天壤之别,似乎找不到共性。原因是机器码是由很多部分组成的,比如指令前缀、主操作码字...

    《操作系统真象还原》:第三章 完善MBR——CPU的实模式
  • 《操作系统真象还原》:第二章 编写 MBR

    先了解 CPU 的两种工作模式:实模式和保护模式 实模式(英语:Real mode)是 Intel 80286 和之后的 x86 兼容 CPU 的操作模式。实模式的特性是一个 20 比特的区段存储器地址空间(意思为只有 1MB 的存...

    《操作系统真象还原》:第二章 编写 MBR
  • 《操作系统真象还原》:第一章 环境配置

    第 0 章:一些你可能正感到迷惑的问题 摘记 0.28 MBR、EBR、DBR 和 OBR 各是什么 MBR 位于整个硬盘最开始的块, EBR 位于每个子扩展分区,各子扩展分区中只有一个逻辑分区。 MBR 和 EBR 位于分...

    《操作系统真象还原》:第一章 环境配置
  • 6月阅读总结

    “零拷贝”技术 Sogou C++ Workflow:搜狗公司的 C++服务器引擎,支持 500k QPS Reducing CPU scheduler latency in Linux:CPU 调度算法 BMQ 和 CFS 的对比...

    6月阅读总结