从Habré上的系列文章中阅读了编写Hello World内核的基本步骤之后,是时候开始认真开发最基本的工具了:堆分配器和调度程序。
重要事项 :
1.本系列初学者课程。 目的是形成一个整体的世界图景。 很长一段时间以来,我都无法理解塔南鲍姆理论,无法将其与实践联系起来。 对于那些拥有相同事物的人-我致力于这篇文章。
2.代码是最简单,最愚蠢的,但要尽可能清晰。 我的目标是给您一个了解,以便您可以编写自己的内核,比这要酷得多。
3.我会在准备好广泛使用后尽快发布该回购。 我写一小部分,调试并拍摄视频教程。 我没有成品。
老实说,我考虑了很长时间是否要开始撰写有关此类话题的文章和视频教程。 但是,由于对系统编程的热情和俄语中结构化信息的缺乏,促使我参加了这个实验。 因此,如果需要我的作品,我计划每月至少发表一次文章,每周不超过一次。
我梦想着写十年级的操作系统。 塔南鲍姆(Tanenbaum)的书拥有惊人的财产。 早晚接触它的任何人都开始梦想编写自己的操作系统。 但后来我什么也没写,因为我意识到不可能在正常情况下编译并链接干净的二进制文件和代码。 我不得不学习Linux,上大学,上班。 一切都将一事无成。 是的,只有梦想在十年后才实现。 现在,我回想起React上无聊的模子,回想起我花了数小时去拆装和调试程序,拆下包装工,弄破了破解盒的激情,晚上我读了很多学校小说中的文章...我知道我的生活可能会有所不同。 完全不同。 如果我有一个可以在一开始就帮助我的人。 但是时间已经过去了。 程序很难破解。 Linux内核已发展到令人难以置信的大小。 管理程序出现。 英特尔汇编器已经变得很大,以至于只看手册中的命令列表,对它的学习热情就消失了。 我们必须在网上赚钱。 是的 系统编程世界已经度过了黄金岁月。
因此,对于所有热情还没有耗尽的学生,我专门介绍了一个详细的教程,介绍如何从头开始编写具有自己的shell的最简单的多任务微内核。 我想警告所有尚未出现的人,尽管这是一件非常有趣的事情,但这是一次忘恩负义和无益的事情。 如您所知,青年也有自己的担忧。
走吧
本文包含在我的家庭工作室中拍摄
的视频教程 。 它直接专用于代码。
让我们开始堆管理器的开发。
extern void *kmalloc(size_t size); extern void kfree(void *addr);
要编写堆分配器,您需要存储空闲和繁忙地址的区域。 这最容易实现为方向列表。 有向列表本身的实现必须通过静态数组来完成。
struct slist_head_t { struct slist_head_t *prev; struct slist_head_t *next; bool is_valid; void *data; } attribute(packed); struct slist_definition_t { size_t base; u_int slots; size_t slot_size; struct slist_head_t *head; struct slist_head_t *tail; };
这样的有限列表的元素将是存储区域的记录。 此外,在相邻区域之间可以没有孔。 如果存在可用内存,则通过单独的列表项对其进行描述。
struct kheap_entry_t { struct slist_head_t list_head; size_t addr; size_t size; bool is_buzy; } attribute(packed); struct kheap_entry_t kheap_blocks[KHEAP_MAX_ENTRIES]; struct slist_definition_t kheap_list = { .head = null, .tail = null, .slot_size = sizeof(struct kheap_entry_t), .slots = KHEAP_MAX_ENTRIES, .base = (size_t)kheap_blocks };
slist_head_t是kheap_entry_t结构的开头,它使您可以轻松地将堆记录类型转换为列表项。
现在考虑最简单的堆分配器算法(kmalloc):
- 如果没有可用块,请选择所需大小(但不超过最大堆大小)的新块。
- 否则,我们将浏览所有空闲块。 如果找到空闲块,请查看其大小。 如果足够,请接受。 如果有剩余,则创建一个新的剩余大小的空白块。 否则,如果不足,则扩大堆边界(但不超过最大值)。
无内存算法将类似(kfree):
- 查找一个地址以要释放的地址开头的块。 检查他是否忙。 标记为免费。
- 如果右边或左边有一个自由的邻居,与他团结在一起。
下一篇文章将重点讨论堆算法的实现。
让我们编写最简单的调度程序。 该任务将如下所示:
struct task_t { struct clist_head_t list_head; u_short tid; struct gp_registers_t gp_registers; struct op_registers_t op_registers; struct flags_t flags; u_int time; bool reschedule; u_short status; int msg_count_in; struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; void *kstack; void *ustack; } attribute(packed); struct clist_definition_t task_list = { .head = null, .slot_size = sizeof(struct task_t) };
我们已经知道如何分配内存并因此将任务组织为环列表。 因此,相对于当前任务,以所需状态执行以下任务更为方便(如果执行了任务,我们将使用TASK_RUNNING状态)。 首先,我们假设所有任务都在内核保护环中执行(调试调度程序更容易),并且仅需一个堆栈即可完成任务。 将来我们会固定TSS。
我们整理了任务,现在是计划本身。 将计时器处理程序添加到IDT,并在PIC中启用所需IRQ线的中断。 通过中断计时器(并在内核初始化代码的末尾),我们将控制权转移到调度程序,并通过计时器中断传递返回地址和先前保存的寄存器的地址:
/* * Handle IRQ0 * void asm_ih_timer(unsigned long *addr) */ asm_ih_timer: cli pushal mov %esp,%ebp mov %ebp,%ebx pushl %ebx # ® add $32,%ebx pushl %ebx # &ret addr call ih_timer mov %ebp,%esp popal sti iretl
在调度程序中,我们检查此时是否执行了任何任务。 如果是这样,我们将增加其执行时间的计数器,并查看其是否超过配额。 如果没有,请冷静地返回。 如果超过,则需要重新计划任务。 我们保存其状态(通过中断计时器,保存的寄存器的地址和存储在堆栈中的返回地址将派上用场)。
current_task->op_registers.eip = *ret_addr; current_task->op_registers.cs = *(u16 *)((size_t)ret_addr + 4); *(u32 *)(¤t_task->flags) = *(u32 *)((size_t)ret_addr + 6) | 0x200; current_task->op_registers.u_esp = (size_t)ret_addr + 12; current_task->gp_registers.esp = current_task->op_registers.u_esp; memcpy(¤t_task->gp_registers, (void *)reg_addr, sizeof(struct gp_registers_t));
我们获取新任务的堆栈地址,并在那里为iret命令形成一个返回帧。 然后,我们调用汇编函数来切换上下文。
next_task->op_registers.u_esp -= 4; *(u32 *)(next_task->op_registers.u_esp) = (*(u16 *)(&next_task->flags)) | 0x200; next_task->op_registers.u_esp -= 4; *(u32 *)(next_task->op_registers.u_esp) = next_task->op_registers.cs; next_task->op_registers.u_esp -= 4; *(u32 *)(next_task->op_registers.u_esp) = next_task->op_registers.eip; next_task->gp_registers.esp = next_task->op_registers.u_esp; next_task->op_registers.u_esp -= sizeof(struct gp_registers_t); memcpy((void *)next_task->op_registers.u_esp, (void *)&next_task->gp_registers, sizeof(struct gp_registers_t)); current_task = next_task; asm_switch_context(next_task->op_registers.u_esp);
上下文切换本身看起来像这样:
# # Switch context # void asm_switch_context(u32 esp) # asm_switch_context: mov 4(%esp),%ebp # ebp = esp mov %ebp,%esp popal sti iretl
调度程序已准备就绪。
请参阅本文随附
的视频教程中的完整代码。 它不仅考虑了调度程序,还考虑了为那些已忘记的用户加载,编译和配置IDT的过程: