目录
本章节所有代码托管在miniOS_32
章节任务介绍
任务简介
上一节,我们成功为我们的内核开启了中断机制,并使用时钟中断进行了测试
本节我们将开启操作系统内存管理的相关内容
本章的主要任务有:
实现ASSERT断言
实现字符串处理函数
实现管理物理内存的数据结构位图
实现assert断言
断言介绍
断言(Assertion) 可视为一种调试工具,主要用于检测程序在运行时是否满足某个条件。如果断言条件为 真 ,程序继续执行;如果为 假,程序通常会停止执行并抛出错误信息,帮助开发者发现潜在的问题。
断言语句通常具有以下结构:
assert( condition, "Error message if condition is false")
-
condition
是你期望为真的条件。 -
如果
condition
为True
,程序继续执行。 -
如果
condition
为False
,则会抛出一个异常或错误,并输出后面的错误信息。
断言实现
中断状态控制
一方面,当内核运行中出现问题时,多属于严重的错误,着实没必要再运行下去了。另一方面,断言在输出报错信息时,屏幕输出不应该被其他进程干扰,这样咱们才能专注于报错信息。
综上两点原因,ASSERT排查出错误后,最好在关中断的情况下打印报错信息。
内核运行时,为了通过时钟中断定时调度其他任务,大部分情况下中断是打开的,因此我们需要实现一些可以控制中断开关的函数
如下,在/kernel/interrupt.h中添加中断状态的数据结构,以及控制中断状态的函数声明
cpp
/*定义中断状态*/
enum intr_status
{
INTR_OFF, // 表示中断关闭
INTR_ON // 表示中断打开
};
/* 获取当前中断状态 */
enum intr_status intr_get_status(void);
/* 开中断并返回开中断前的状态*/
enum intr_status intr_enable(void);
/* 关中断,并且返回关中断前的状态 */
enum intr_status intr_disable(void);
/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status);
然后我们在/kernel/interrupt.c
中实现其功能
获取中断状态
如下是eflags
寄存器中所有的控制状态位
其中第9位,IF位用于表示和控制中断状态
IF位为1 ,表示系统处于开中断状态
IF位为0 ,表示系统处于关中断状态
第9位的十六进制数为0x2000
,故我们以此作为掩码,来获取eflags中IF位的值,从而获取中断状态
如下定义IF位的掩码
cpp
/*eflags寄存器中的if位的掩码*/
#define EFLAGS_IF 0x00000200 // 表示eflags寄存器中的if位为1
// 将该宏与当前的if位(中断状态)进行与操作,即可获取当前的中断状态
同时利用IF位的掩码定义获取IF位的宏
cpp
/*
功能:获取eflags寄存器中的值
pushfl将eflags寄存器中的值压入栈顶,再使用popl将栈顶的值弹出到EFLAG_VAR所在内存中
该约束自然用表示内存的字母,但是内联汇编中没有专门表示约束内存的字母,所以只能用g
代表可以是任意寄存器,内存或立即数
*/
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl;pop %0" : "=g"(EFLAG_VAR))
接下来就可以实现获取中断状态的函数了,如下所示
cpp
/* 获取当前中断状态 */
enum intr_status intr_get_status()
{
uint32_t eflags = 0;
GET_EFLAGS(eflags);
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}
开中断控制函数
cpp
/* 开中断并返回开中断前的状态*/
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;
}
}
注,
sti
指令用于开中断
关中断控制函数
cpp
/* 关中断,并且返回关中断前的状态 */
enum intr_status intr_disable()
{
enum intr_status old_status;
if (INTR_ON == intr_get_status())
{
old_status = INTR_ON;
asm volatile("cli" : : : "memory"); // 关中断,cli指令将IF位置0
// cli指令不会直接影响内存。然而,从一个更大的上下文来看,禁用中断可能会影响系统状态,
// 这个状态可能会被存储在内存中。所以改变位填 "memory" 是为了安全起见,确保编译器在生成代码时考虑到这一点。
return old_status;
}
else
{
old_status = INTR_OFF;
return old_status;
}
}
设置中断状态函数
cpp
/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status status)
{
return status & INTR_ON ? intr_enable() : intr_disable(); // enable与disable函数会返回旧中断状态
}
断言实现
我们首先在/kernel/debug.h
中定义ASSERT
的断言宏
cpp
/*
如果CONDITION条件为真则什么也不做
否则就输出相关断言信息
*/
#define ASSERT(CONDITION) \
if (CONDITION) \
{ \
} \
else \
{ \
PANIC(#CONDITION); \
} // 加#后,传入的参数变成字符串
其实现逻辑很简单,如果
CONDITION
条件为真,则什么也不做,否则就调用PANIC
宏,将判断为假的条件作为字符串参数传入,然后打印错误信息
其中PANIC
宏的实现如下
cpp
void panic_spin(char *filename, int line, const char *func, const char *condition);
/*
...是可变参数,也就是随便你传多少个参数,然后原封不动地传到__VA_ARGS_那里去
__FILE__,__LINE__,__func__是预定义宏,代表这个宏所在的文件名,行数,与函数名字,编译器处理
*/
#define PANIC(...) panic_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)
可以看到,PANIC
宏的实现是通过调用panic_spin
函数将错误信息进行打印实现的
panic_spin
函数定义在/kernel/debug.c
中,如下所示
cpp
/* 打印文件名,行号,函数名,条件,并使程序悬停 */
void panic_spin(char *filename, int line, const char *func, const char *condition)
{
intr_disable(); // 发生错误时打印错误信息,不应该被打扰,因此需要关中断
put_str("\n\n\n!!!!! error !!!!!\n");
put_str("filename:");
put_str(filename);
put_str("\n");
put_str("line:0x");
put_int(line);
put_str("\n");
put_str("function:");
put_str((char *)func);
put_str("\n");
put_str("condition:");
put_str((char *)condition);
put_str("\n");
while (1) ;
}
代码编译
cpp
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
编译通过,没有出现任何问题
实现字符串操作函数
本节的代码依旧是准备工作,为了将来的开发工作更得心应手,本节打算实现与字符串相关的函数,此类函数以后会被经常用到
以下是字符串操作函数的相关定义
/lib/kernel/string.h
cpp
#ifndef __LIB_STRING_H
#define __LIB_STRING_H
#include "stdint.h"
/*内存初始化函数,将dst_起始的size个字节的初始化为value*/
void memset(void *dst_, uint8_t value, uint32_t size);
/*内存拷贝函数,将src_起始处的size个字节的内容拷贝到dst_*/
void memcpy(void *dst_, const void *src_, uint32_t size);
/*
比较两个地址的起始size字节的数据是否相等
相等则返回0
如果不相等,则比较第一个不相等的字节数据
*/
int memcmp(const void *a_, const void *b_, uint32_t size);
/*字符串拷贝*/
char *strcpy(char *dst_, const char *src_);
/*获取字符串的长度*/
uint32_t strlen(const char *str);
/*
比较两个字符串
若a_中的字符与b_中的字符全部相同,则返回0
如果不同,则比较第一个不同的字符
如果a_>b_返回1,反之返回-1
*/
int8_t strcmp(const char *str1, const char *str2);
/*从左到右查找字符串str中首次出现字符ch的地址*/
char *strchr(const char *string, const uint8_t ch);
/*从后往前查找字符串str中首次出现字符ch的地址(不是下标,是地址)*/
char *strrchr(const char *string, const uint8_t ch);
/* 将字符串src_拼接到dst_后,将回拼接的串地址 */
char *strcat(char *dst_, const char *src_);
/* 在字符串str中查找指定字符ch出现的次数 */
uint32_t strchrs(const char *filename, uint8_t ch);
#endif
可以看到,都是我们平时使用c语言时经常用到的函数,我们在这里只是简单重新实现了一下
/lib/kernel/string.c
cpp
#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_;
/*
先执行*dst=value
然后执行dst++
*/
while (size--)
*dst++ = value;
}
/*
将src_起始处的size个字节的内容拷贝到dst_
*/
void memcpy(void *dst_, const void *src_, uint32_t size)
{
ASSERT(dst_ != NULL && src_ != NULL);
uint8_t *src = (uint8_t *)src_;
uint8_t *dst = (uint8_t *)dst_;
while (size--)
{
*dst++ = *src++;
}
}
/*
比较两个地址的起始size字节的数据是否相等
相等则返回0
如果不相等,则比较第一个不相等的字节数据
*/
int memcmp(const void *a_, const void *b_, uint32_t size)
{
ASSERT(a_ != NULL && b_ != NULL);
const char *a = a_;
const char *b = b_;
while (size--)
{
if (*a != *b)
{
return *a > *b ? 1 : -1;
}
a++;
b++;
}
return 0;
}
/*
字符串拷贝
*/
char *strcpy(char *dst_, const char *src_)
{
ASSERT(dst_ != NULL && src_ != NULL);
char *res = dst_;
while ((*dst_++ = *src_++))
;
return res;
}
/*
返回字符串的长度
*/
uint32_t strlen(const char *str)
{
ASSERT(str != NULL);
const char *p = str;
while (*p++);
return (p - str - 1);
}
/*
比较两个字符串
若a_中的字符与b_中的字符全部相同,则返回0
如果不同,则比较第一个不同的字符
如果a_>b_返回1,反之返回-1
*/
int8_t strcmp(const char *str1, const char *str2)
{
ASSERT(str1 != NULL && str2 != NULL);
// 比较两个字符串
while (*str1 != '\0' && *str2 != '\0')
{
if (*str1 != *str2)
{
return (*str1 < *str2) ? -1 : 1;
}
++str1;
++str2;
}
// 如果两个字符串走到末尾还是没有不同,比较它们的结束符
return (*str1 == *str2) ? 0 : (*str1 < *str2 ? -1 : 1);
}
/*
从左到右查找字符串str中首次出现字符ch的地址
const char *str,表示str指向的字符串内容不可改变
但是str指针值是可以改变的
*/
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;
/* 从头到尾遍历一次,若存在ch字符,last_char总是该字符最后一次出现在串中的地址(不是下标,是地址)*/
while (*str != 0)
{
if (*str == ch)
{
last_char = str;
}
str++;
}
return (char *)last_char;
}
/* 将字符串src_拼接到dst_后,将回拼接的串地址 */
char *strcat(char *dst_, const char *src_)
{
ASSERT(dst_ != NULL && src_ != NULL);
char *p = dst_;
while (*p++)
;
--p;
while ((*p++ = *src_++))
;
return dst_;
}
/* 在字符串str中查找指定字符ch出现的次数 */
uint32_t strchrs(const char *str, uint8_t ch)
{
ASSERT(str != NULL);
uint32_t cnt = 0;
const char *p = str;
while (*p != 0)
{
if (*p == ch)
cnt++;
++p;
}
return cnt;
}
这部分内容比较简单,具体实现细节不再赘述
代码编译
cpp
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
编译通过,没有问题
位图实现
位图简介
在计算机的内存管理系统 中,位图 (Bitmap)是一种常用的数据结构,用于高效地管理内存的分配和回收 。它通过使用一个位数组来表示内存中各个块(或页面)的状态,进而实现对内存资源的管理 。每个二进制位代表一个固定大小的内存块(如页、页框或块),0和1的状态分别表示该内存块是"空闲"的或"已分配"的。
- 位图的基本概念
在内存管理中,位图通常用于表示一个固定大小的内存块集合,每个内存块用一个二进制位来表示:
0 表示该内存块是"空闲"的,即该内存块没有被分配。
1 表示该内存块是"已分配"的,即该内存块正在被使用。
例如,如果内存被分成了8个块,并且它们的分配情况如下:
-
1 0 0 1 1 0 0 1
那么这个位图表示:
第1、4、5、8个内存块已分配。
第2、3、6、7个内存块是空闲的。
- 位图在内存管理中的应用
位图通常用于以下几种内存管理任务:
2.1 内存分配
当需要为一个进程分配内存时,内存管理系统可以使用位图来查找一个或多个空闲的内存块。通过遍历位图,找到连续的若干个0位(表示空闲块),然后将这些空闲块标记为已分配(将相应的位设置为1)。
2.2 内存回收
当进程释放内存时,内存管理系统可以通过修改位图将相应的位重新设置为0,表示这些内存块已空闲,准备重新分配给其他进程。
2.3 内存块分配策略
位图使得内存管理系统能够轻松地选择空闲内存块。常见的策略包括:
最佳适配:寻找最小的足够大空闲块。
最差适配:选择最大的空闲块。
首次适配:从位图中从头开始找到第一个符合要求的空闲块。
2.4 合并和拆分
在某些系统中,内存分配和回收过程中可能会导致内存碎片。位图可以帮助检测空闲块的合并,尤其是在连续块被释放后,系统可以将这些空闲块合并成更大的空闲区域。
位图的实现
首先还是定义位图相关的数据结构以及操作函数的相关定义
/lib/kernel/bitmap.h
cpp
#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);
/*确定位图的bit_idx位是否为1,即判断某块内存是否被分配*/
bool bitmap_scan_test(struct bitmap *btmp, uint32_t bit_idx);
/*在位图中寻找连续的cnt个0,以此来分配一块连续未被占用的内存*/
int bitmap_scan(struct bitmap *btmp, uint32_t cnt);
/*将位图的某一位设置为1或0*/
void bitmap_set(struct bitmap *btmp, uint32_t bit_idx, int8_t value);
#endif
接下来开始正式实现位图的相关操作
/lib/kernel/bitmap.c
1.位图的初始化
cpp
/*位图初始化*/
void bitmap_init(struct bitmap *btmp)
{
memset(btmp->bits, 0, btmp->btmp_bytes_len);
}
2.确定位图的bit_idx位是否为1
cpp
/*确定位图的bit_idx位是否为1*/
bool bitmap_scan_test(struct bitmap *btmp, uint32_t bit_idx)
{
//获取bit_idx在位图中第几个字节
uint32_t byte_idx = bit_idx / 8;
//获取bit_idx在位图中byte_idx字节处的第几个比特位
uint32_t bit_odd = bit_idx % 8;
// 小端字节序,故低位在右侧
return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd));
}
该函数的实现需要做以下几点说明
参数bit_idx是字节级别的,在位图中表示一个索引
计算机内部采用小端字节序,因此掩码应该左移
3.在位图中寻找连续的cnt个0,以此来分配一块连续未被占用的内存
cpp
/*
在位图中寻找连续的cnt个0,以此来分配一块连续未被占用的内存
*/
int bitmap_scan(struct bitmap *btmp, uint32_t cnt)
{
uint32_t idx_type = 0;
/*先逐字节比较,找到第一个含有比特0位的字节,0xff=1111 1111*/
while (btmp->bits[idx_type] == 0xff && idx_type < btmp->btmp_bytes_len)
++idx_type;
ASSERT(idx_type < btmp->btmp_bytes_len);
if (idx_type == btmp->btmp_bytes_len)
return -1;
/*在该字节内找到第一个为0的比特位*/
int idx_bit = 0;
while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_type])
++idx_bit;
/*空闲位在位图内的下标*/
int bit_idx_start = idx_type * 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;
/*寻找剩余连续的0*/
bit_idx_start = -1;
while (bit_left--)
{
if (!bitmap_scan_test(btmp, next_bit))
++count;
else
count = 0;
if (count == cnt)
{
bit_idx_start = next_bit - cnt + 1;
break;
}
++next_bit;
}
return bit_idx_start;
}
4.将位图的某一位设置为1或0
cpp
/*
将位图的某一位设置为1或0
*/
void bitmap_set(struct bitmap *btmp, uint32_t bit_idx, int8_t value)
{
ASSERT((value == 0) || (value == 1));
uint32_t idx_byte = bit_idx / 8;
uint32_t idx_odd = bit_idx % 8;
/* 一般都会用个0x1这样的数对字节中的位操作,
* 将1任意移动后再取反,或者先取反再移位,可用来对位置0操作。*/
if (value)
{ // 如果value为1
btmp->bits[idx_byte] |= (BITMAP_MASK << idx_odd);
}
else
{ // 若为0
btmp->bits[idx_byte] &= ~(BITMAP_MASK << idx_odd);
}
}
代码编译
cpp
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c