目录
章节任务介绍
任务简介
实现assert断言
断言介绍
断言实现
中断状态控制
获取中断状态
开中断控制函数
关中断控制函数
设置中断状态函数
断言实现
代码编译
实现字符串操作函数
位图实现
位图简介
位图的实现
代码编译
本章节所有代码托管在miniOS_32
章节任务介绍
任务简介
上一节,我们成功为我们的内核开启了中断机制,并使用时钟中断进行了测试
本节我们将开启操作系统内存管理的相关内容
本章的主要任务有:
实现ASSERT断言
实现字符串处理函数
实现管理物理内存的数据结构位图
实现assert断言
断言介绍
断言(Assertion) 可视为一种调试工具,主要用于检测程序在运行时是否满足某个条件。如果断言条件为 真,程序继续执行;如果为 假,程序通常会停止执行并抛出错误信息,帮助开发者发现潜在的问题。
断言语句通常具有以下结构:
assert( condition, "Error message if condition is false")
-
condition
是你期望为真的条件。 -
如果
condition
为True
,程序继续执行。 -
如果
condition
为False
,则会抛出一个异常或错误,并输出后面的错误信息。
断言实现
中断状态控制
一方面,当内核运行中出现问题时,多属于严重的错误,着实没必要再运行下去了。另一方面,断言在输出报错信息时,屏幕输出不应该被其他进程干扰,这样咱们才能专注于报错信息。
综上两点原因,ASSERT排查出错误后,最好在关中断的情况下打印报错信息。
内核运行时,为了通过时钟中断定时调度其他任务,大部分情况下中断是打开的,因此我们需要实现一些可以控制中断开关的函数
如下,在/kernel/interrupt.h中添加中断状态的数据结构,以及控制中断状态的函数声明
/*定义中断状态*/
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位的掩码
/*eflags寄存器中的if位的掩码*/
#define EFLAGS_IF 0x00000200 // 表示eflags寄存器中的if位为1// 将该宏与当前的if位(中断状态)进行与操作,即可获取当前的中断状态
同时利用IF位的掩码定义获取IF位的宏
/*
功能:获取eflags寄存器中的值
pushfl将eflags寄存器中的值压入栈顶,再使用popl将栈顶的值弹出到EFLAG_VAR所在内存中
该约束自然用表示内存的字母,但是内联汇编中没有专门表示约束内存的字母,所以只能用g
代表可以是任意寄存器,内存或立即数
*/
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl;pop %0" : "=g"(EFLAG_VAR))
接下来就可以实现获取中断状态的函数了,如下所示
/* 获取当前中断状态 */
enum intr_status intr_get_status()
{uint32_t eflags = 0;GET_EFLAGS(eflags);return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}
开中断控制函数
/* 开中断并返回开中断前的状态*/
enum intr_status intr_enable()
{enum intr_status old_status;if (INTR_ON == intr_get_status()){old_status = INTR_ON;return old_status;}else{old_status = INTR_OFF;asm volatile("sti"); // 开中断,sti指令将IF位置1return old_status;}
}
注,
sti
指令用于开中断
关中断控制函数
/* 关中断,并且返回关中断前的状态 */
enum intr_status intr_disable()
{enum intr_status old_status;if (INTR_ON == intr_get_status()){old_status = INTR_ON;asm volatile("cli" : : : "memory"); // 关中断,cli指令将IF位置0// cli指令不会直接影响内存。然而,从一个更大的上下文来看,禁用中断可能会影响系统状态,// 这个状态可能会被存储在内存中。所以改变位填 "memory" 是为了安全起见,确保编译器在生成代码时考虑到这一点。return old_status;}else{old_status = INTR_OFF;return old_status;}
}
设置中断状态函数
/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status status)
{return status & INTR_ON ? intr_enable() : intr_disable(); // enable与disable函数会返回旧中断状态
}
断言实现
我们首先在/kernel/debug.h
中定义ASSERT
的断言宏
/*
如果CONDITION条件为真则什么也不做
否则就输出相关断言信息
*/
#define ASSERT(CONDITION) \if (CONDITION) \{ \} \else \{ \PANIC(#CONDITION); \} // 加#后,传入的参数变成字符串
其实现逻辑很简单,如果
CONDITION
条件为真,则什么也不做,否则就调用PANIC
宏,将判断为假的条件作为字符串参数传入,然后打印错误信息
其中PANIC
宏的实现如下
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
中,如下所示
/* 打印文件名,行号,函数名,条件,并使程序悬停 */
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) ;
}
代码编译
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
#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
#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;
}
这部分内容比较简单,具体实现细节不再赘述
代码编译
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
#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.位图的初始化
/*位图初始化*/
void bitmap_init(struct bitmap *btmp)
{memset(btmp->bits, 0, btmp->btmp_bytes_len);
}
2.确定位图的bit_idx位是否为1
/*确定位图的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,以此来分配一块连续未被占用的内存
/*
在位图中寻找连续的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;elsecount = 0;if (count == cnt){bit_idx_start = next_bit - cnt + 1;break;}++next_bit;}return bit_idx_start;
}
4.将位图的某一位设置为1或0
/*
将位图的某一位设置为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为1btmp->bits[idx_byte] |= (BITMAP_MASK << idx_odd);}else{ // 若为0btmp->bits[idx_byte] &= ~(BITMAP_MASK << idx_odd);}
}
代码编译
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