前情提要
因为马上要涉及到一个非常重要的部分,内存管理,所以这里我们编写一个简单的C库,用于支持我们后续的C语言开发
一、Assert断言
assert其实如果大家对C语言比较熟悉的话并不陌生,这个函数被称为断言,也就是程序员断定这个函数内的等式成立,如果不成立的话就停止在这里。
我们使用assert是为了看程序运行到某一句是否会出错,我们看一下他的实现代码
// os/src/lib/kernel/assert.h#ifndef __LIB_KERNEL_ASSERT_H
#define __LIB_KERNEL_ASSERT_H
void panic_spin(char* filename, int line, const char* func, const char* condition);/*************************** __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_ASSERT_H*/
// os/src/lib/kernel/assert.c/* 打印文件名,行号,函数名,条件并使程序悬停 */
void panic_spin(char* filename, int line, const char* func, const char* condition) {intr_disable();put_str("###################assert error########################\n");put_str("filename:");put_str(filename);put_str("\n");put_str("line:");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");put_str("#######################################################\n");while(1);
}
其中,PANIC
这个宏定义需要关注一下,里面有四个参数
__FILE__
:这是一个预定义的宏,在编译时会被当前源文件的文件名所替代。
__LINE__
:同样是一个预定义的宏,在编译时会被当前源文件的行号所替代。
__func__
:也是一个预定义的宏,会被当前函数的名称所替代。
__VA_ARGS__
:表示可变参数列表,允许宏在调用时接受不定数量的参数在这里,它用于接收传递给 PANIC 宏的额外参数。
由于只是调试时使用,所以可以使用一个宏定义,当不需要调试时,定义宏 NDEBUG
就可以取消所有 ASSERT
的作用。
在调用PANIC
时,#CONDITION
是将 ASSERT
中的判断条件作为字符串传递。
二、string字符串
很明显,这个库是用来处理字符串的。这个库相对而言就比较简单,这里我们只列出我们实现了那些功能,不再一一实现。想要看实现细节的小伙伴可以看github源码,
// os/src/lib/string.h
#ifndef __LIB_STRING_H
#define __LIB_STRING_H#include "stdin.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);
/* 连续比较以地址a_和地址b_开头的size个字节,若相等则返回0,若a_大于b_返回+1,否则返回-1 */
int memcmp(const void* a_, const void* b_, uint32_t size);
/* 将字符串从src_复制到dst_ */
char* strcpy(char* dst_, const char* src_);
/* 返回字符串长度 */
uint32_t strlen(const char* str);
/* 比较两个字符串,若a_中的字符大于b_中的字符返回1,相等时返回0,否则返回-1. */
int8_t strcmp (const char *a, const char *b);
/* 从前往后查找字符串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
三、list链表
链表是一种常见的数据结构,它由一系列节点组成,每个节点包含两部分:数据和指向下一个节点的指针。链表中的节点按顺序排列,通过指针将它们连接起来,形成一个链式结构。
链表可以分为单向链表和双向链表:
- 单向链表:每个节点包含一个数据元素和一个指向下一个节点的指针。
- 双向链表:每个节点包含一个数据元素,一个指向下一个节点的指针,以及一个指向前一个节点的指针。
链表的基本操作包括:
- 插入:在链表中插入一个新节点。
- 删除:从链表中删除指定节点。
- 搜索:在链表中查找特定数据元素。
- 遍历:遍历整个链表,访问每个节点的数据。
链表的优点包括:
- 相对于数组,链表的长度可以动态变化,不需要提前指定大小。
- 插入和删除节点的时间复杂度为 O(1),只需要重新连接指针即可。
链表的缺点包括:
- 无法像数组那样通过索引直接访问元素,需要从头开始遍历到目标位置。
- 链表需要额外的空间来存储指针信息。
3.1、实现的接口
可以先看一下我们实现的接口
// os/src/lib/list.h/* 链表节点结构 */
struct list_elem {struct list_elem* prev; // 前躯结点struct list_elem* next; // 后继结点
};/* 链表结构,用来实现队列 */
struct list {struct list_elem head; // 定义头节点struct list_elem tail; // 定义尾节点,这两个是哨兵节点
};/* 自定义函数类型function,用于在list_traversal中做回调函数 */
typedef bool (function)(struct list_elem*, int arg);/* 初始化双向链表list */
void list_init (struct list*);
/* 把链表元素elem插入在元素before之前 */
void list_insert_before(struct list_elem* before, struct list_elem* elem);
/* 添加元素到列表队首,类似栈push操作 */
void list_push(struct list* plist, struct list_elem* elem);
/* 追加元素到链表队尾,类似队列的先进先出操作 */
void list_append(struct list* plist, struct list_elem* elem);
/* 链表中删除元素pelem */
void list_remove(struct list_elem* pelem);
/* 将链表第一个元素弹出并返回,类似栈的pop操作 */
struct list_elem* list_pop(struct list* plist);
/* 判断链表是否为空,空时返回true,否则返回false */
bool list_empty(struct list* plist);
/* 返回链表长度 */
uint32_t list_len(struct list* plist);
/* 判断是否有符合函数func(list_elem,arg)的节点,有则返回地址,没有则返回空 */
struct list_elem* list_traversal(struct list* plist, function func, int arg);
/* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */
bool elem_find(struct list* plist, struct list_elem* obj_elem);
再看一下具体的实现,具体的实现其实也比较简单。
void list_init(struct list *list) {list->head.prev = NULL;list->head.next = &list->tail;list->tail.prev = &list->head;list->tail.next = NULL;
}void list_insert_before(struct list_elem *before, struct list_elem *elem) {/* 将before前驱元素的后继元素更新为elem, 暂时使before脱离链表*/before->prev->next = elem;/* 更新elem自己的前驱结点为before的前驱,* 更新elem自己的后继结点为before, 于是before又回到链表 */elem->prev = before->prev;elem->next = before;/* 更新before的前驱结点为elem */before->prev = elem;
}void list_push(struct list *plist, struct list_elem *elem) {list_insert_before(plist->head.next, elem); // 在队头插入elem
}void list_append(struct list *plist, struct list_elem *elem) {list_insert_before(&plist->tail, elem); // 在队尾的前面插入
}void list_remove(struct list_elem *pelem) {pelem->prev->next = pelem->next;pelem->next->prev = pelem->prev;
}struct list_elem *list_pop(struct list *plist) {struct list_elem *elem = plist->head.next;list_remove(elem);return elem;
}struct list_elem *list_traversal(struct list *plist, function func, int arg) {struct list_elem *elem = plist->head.next;if (list_empty(plist)) return NULL; // 队列为空则直接返回while (elem != &plist->tail) {if (func(elem, arg)) { // func返回ture则认为符合条件return elem;} // 若回调函数func返回true,则继续遍历elem = elem->next;}return NULL;
}uint32_t list_len(struct list *plist) {struct list_elem *elem = plist->head.next;uint32_t length = 0;while (elem != &plist->tail) {length++;elem = elem->next;}return length;
}bool list_empty(struct list *plist) {return (plist->head.next == &plist->tail ? true : false);
}bool elem_find(struct list *plist, struct list_elem *obj_elem) {struct list_elem *elem = plist->head.next;while (elem != &plist->tail) {if (elem == obj_elem) {return true;}elem = elem->next;}return false;
}
具体的实现比较简单,而且由于我们实现了哨兵结点,所以其实编程上更为简单一些。
3.2、具体使用
可以具体怎么用这个链表呢?这就涉及到比较难的两个宏定义了
#define offset(struct_type,member) (int)(&((struct_type*)0)->member)
#define elem2entry(struct_type, struct_member_name, elem_ptr) \(struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))
首先看第一个,这个宏定义计算了结构体中某个成员相对于结构体起始位置的偏移量。
offset
宏接受两个参数:struct_type
表示结构体类型,member
表示结构体中的成员名称。(struct_type*)0
创建了一个指向地址为 0 的结构体类型的指针。&((struct_type*)0)->member
取得了结构体中member
成员的地址,并通过取地址运算符&
返回该地址。(int)
将地址转换为整数类型,得到了该成员相对于结构体起始位置的偏移量。
再看第二个,这个宏定义根据给定的结构体成员指针,求出整个结构体的起始地址。
elem2entry
宏接受三个参数:struct_type
表示结构体类型,struct_member_name
表示结构体中的成员名称,elem_ptr
表示指向成员的指针。offset(struct_type, struct_member_name)
调用前面定义的offset
宏,计算出结构体中struct_member_name
成员相对于结构体起始位置的偏移量。(int)elem_ptr
将指向成员的指针转换为整数类型,表示该成员的地址。(int)elem_ptr - offset(struct_type, struct_member_name)
计算出整个结构体的起始地址,即减去成员偏移量。即这个结构体的起始地址。(struct_type*)
将计算出的整个结构体的起始地址转换为指向该结构体的指针,最终返回该指针。
这两个宏的作用我们举个例子
由于我们是在64位环境下的例子,所以地址是64位的。
// 创建一个教师结构体,其中有链表的结构
struct teacher {int age;int height;struct list_elem teacher_list_elem;
};int main() {struct list* teacher_list = (struct list*)malloc(sizeof(struct list));list_init(teacher_list);struct teacher* teacher1 = (struct teacher*)malloc(sizeof(struct teacher));teacher1->age = 10;teacher1->height = 100;list_append(teacher_list, &(teacher1->teacher_list_elem));struct teacher* teacher2 = (struct teacher*)malloc(sizeof(struct teacher));teacher2->age = 50;teacher2->height = 500;list_append(teacher_list, &(teacher2->teacher_list_elem));struct list_elem* pos = teacher_list->head.next;// 可以看到各个元素在结构体起始地址的偏移量printf("%lld\n",offset(struct teacher, age));printf("%lld\n",offset(struct teacher, height));printf("%lld\n",offset(struct teacher, teacher_list_elem));// 打印头结点,尾节点,两个教师结点的地址printf("head addriss is %lld\n",(long long int)&teacher_list->head);printf("tail addriss is %lld\n",(long long int)&teacher_list->tail);printf("teacher1 addriss is %lld\n",(long long int)teacher1);printf("teacher2 addriss is %lld\n",(long long int)teacher2);// 打印链表长度printf("list len is %d\n",list_len(teacher_list));while(pos != &teacher_list->tail) {// 循环遍历链表printf("address=%lld ",(long long int)elem2entry(struct teacher, teacher_list_elem, pos));// 获得带有链表节点的结构体的地址printf("age=%d height=%d\n",(elem2entry(struct teacher, teacher_list_elem, pos))->age,\(elem2entry(struct teacher, teacher_list_elem, pos))->height);// 遍历下一个pos = pos->next;}return 0;
}
看一下结果
完全正确,在给一个结构图加深印象
可以看到,链表的前向结点和后向结点都指向了上一个结点中链表的部分,所以我们想要拿到结构体的地址需要一点小手段,也就是 elem2entry
宏。
四、bitmap位图
位图(Bitmap)是一种数据结构,用于表示一组二进制位的集合,每个位通常对应某种状态或者标记。位图常用于解决空间高效利用和快速查询的问题。
在计算机中,位图通常表示为一个由连续的比特位组成的数组或者其他数据结构。例如,一个简单的位图可以用一个整数数组来表示,其中每个整数可以存储多个位的信息。位图可以被用来表示一组开关的状态(开/关)、一组标记的存在与否、一组IP地址的分配情况等等。它的使用场景非常广泛。
位图数据结构通常支持以下操作:
- 设置某一位的值(置位)。
- 清除某一位的值(清零)。
- 查询某一位的值。
位图的优点包括:
- 空间效率高:位图通常比使用其他数据结构来表示相同信息所需的空间更少,因为它们可以紧凑地存储大量的布尔值信息。
- 快速查询:可以通过位运算来快速查询某个位置的值,不需要进行复杂的遍历操作。
位图的缺点有:
- 位图的大小通常是固定的,因此当需要存储的元素数量不确定时,可能会浪费一些空间。
- 插入、删除等操作相对复杂,可能需要进行大量的位移和逻辑运算。
看一下其具体的结构
4.1、实现的接口
先看一下我们实现的接口
#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个位,返回其起始位下标 */
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);
然后看一下具体的实现
void bitmap_init(struct bitmap* btmp) {memset(btmp->bits, 0, btmp->btmp_bytes_len);
}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));
}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;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为0count++;} else {count = 0;}if (count == cnt) { // 若找到连续的cnt个空位bit_idx_start = next_bit - cnt + 1;break;}next_bit++; }return bit_idx_start;
}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) {btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);} else {btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);}
}
bitmap我们这里实现的较为简单。索引直接采用的从左到右的暴力索引。
结束语
这节实现了我们后面编程需要用到的一些数据结构。下一节我们将实现一个内存池,以后我们想要内存就需要从内存池申请了。不在需要手动管理。
老规矩,代码地址 https://github.com/lyajpunov/os.git