Operating System Implementation - Interrupt and Task Scheduling

Blog URL: www.shicoder.top
WeChat: 18223081347
Welcome to group chat: 452380935

This time we will improve the kernel, mainly including the loading of global descriptors, task scheduling, interrupts, etc.

Loading of global descriptors

Let's review, is loaderthere some code for global descriptors in

 prepare_protected_mode:
 ​
     cli; 关闭中断
     ; 打开A20线
     in al, 0x92
     or al, 0b10 ; 第1位置1
     out 0x92, al
     ; 加载GDT
     lgdt [gdt_ptr]
     ; 启动保护模式
     mov eax, cr0
     or eax, 1 ; 第0位置1
     mov cr0, eax
 ​
     ; 用跳转来刷新缓存,启用保护模式
     jmp dword code_selector:protect_mode
复制代码

When we are preparing to enter the protected mode, we will gdt_ptrload the pointed place into the gdtregister, so is it not necessary to enter the protected mode, that is, the kernel stage, of course not, and you think, we are loaderin, there are only 2 segment, one is the code segment, the other is the data segment, and there are a total of 8192 segments, then how to load other segments when they are used by the kernel, so we need to redefine an array in the kernel, first initialize 8192 (of course in my In this kernel, not so much is used, in fact, only 128 are initialized), then first assign the value of the register in the previous protected mode gdtto this array, and then load the address of the array into the gdtregister, then know this step, let's get started

 #define GDT_SIZE 128 // 本身有8192个,但是我们在这里用不到这么多
 ​
 // 全局描述符
 typedef struct descriptor_t /* 共 8 个字节 */
 {
     unsigned short limit_low;      // 段界限 0 ~ 15 位
     unsigned int base_low : 24;    // 基地址 0 ~ 23 位 16M
     unsigned char type : 4;        // 段类型
     unsigned char segment : 1;     // 1 表示代码段或数据段,0 表示系统段
     unsigned char DPL : 2;         // Descriptor Privilege Level 描述符特权等级 0 ~ 3
     unsigned char present : 1;     // 存在位,1 在内存中,0 在磁盘上
     unsigned char limit_high : 4;  // 段界限 16 ~ 19;
     unsigned char available : 1;   // 该安排的都安排了,送给操作系统吧
     unsigned char long_mode : 1;   // 64 位扩展标志
     unsigned char big : 1;         // 32 位 还是 16 位;
     unsigned char granularity : 1; // 粒度 4KB 或 1B
     unsigned char base_high;       // 基地址 24 ~ 31 位
 } _packed descriptor_t;
 ​
 // 段选择子
 typedef struct selector_t
 {
     u8 RPL : 2;
     u8 TI : 1;
     u16 index : 13;
 } selector_t;
 ​
 // 全局描述符表指针
 typedef struct pointer_t
 {
     u16 limit;
     u32 base;
 } _packed pointer_t;
 ​
 void gdt_init();
复制代码

Let’s define the structure first, because in the kernel, we can use C language to write, so the definition here is not loaderin assembly like that. Does it feel simpler? The most important thing is a gdt_initfunction, which is the step we just took. main implementation of

 descriptor_t gdt[GDT_SIZE]; // 内核全局描述符表
 pointer_t gdt_ptr;          // 内核全局描述符表指针
 ​
 // 初始化内核全局描述符表
 void gdt_init()
 {
     DEBUGK("init gdt!!!\n");
     // 在loader.asm中,已经有三个描述符了,因此GDTR寄存器有3个了
     asm volatile("sgdt gdt_ptr"); //  读取GDTR寄存器到gdt_ptr指向的地方
 ​
     memcpy(&gdt, (void *)gdt_ptr.base, gdt_ptr.limit + 1);
     // 此时gdt这个数组前3个有值,后面125个是0
     gdt_ptr.base = (u32)&gdt;
     gdt_ptr.limit = sizeof(gdt) - 1;
     asm volatile("lgdt gdt_ptr\n"); // 将gdt_ptr指向的值写入到GDTR寄存器 ,此时GDTR寄存器有128个全局描述符
 }
复制代码

It's still quite simple, that's all, note that our kernel only has an array of 128, and 8192 is not implemented, but normally, it linuxmust be 8192

Tasks and Scheduling

To put it simply, a task can be thought of as a process, then each process needs to have its own stack to store the information it needs to run. In this code writing, in order to simplify, the stack of a process occupies one page memory, and its structure is as follows

image-20220512205431042

因此任务调度就是将此时的栈切换为下一个进程的栈,那么切换肯定要知道切换之后要保存哪些东西,这个是由ABI来规定的,一个进程有自己的寄存器值,ABI规定,比如进程a要切换到进程b,那么进程a要自己保存下面三个

  • eax
  • ecx
  • edx

进程b要替进程a保存以下5个

  • ebx
  • esi
  • edi
  • ebp
  • esp

知道上面的理论,我们就可以进行切换了

创建进程

我们上面说到一个进程需要一个栈,那么我们就给这个栈创建一个结构体

 typedef struct task_t
 {
     u32 *stack; // 内核栈
 } task_t;
复制代码

此时就是按照上面那个图,设置一些值

 #define PAGE_SIZE 0x1000 // 4KB 表示一页 每一页里面存放进程的信息和进程的栈信息
 ​
 task_t *a = (task_t *)0x1000; // 进程a的栈的初始地址,然后每个进程的栈有1页
 u32 thread_a()
 {
     while (true)
     {
         printk("A");
         schedule();
     }
 }
 ​
 static void task_create(task_t *task, target_t target)
 {
     // 此时stack为这个进程的栈的最高地址
     u32 stack = (u32)task + PAGE_SIZE;
     // 进程的栈的最高地址往下一点,就是存放task_frame_t
     stack -= sizeof(task_frame_t);
     task_frame_t *frame = (task_frame_t *)stack;
     frame->ebx = 0x11111111;
     frame->esi = 0x22222222;
     frame->edi = 0x33333333;
     frame->ebp = 0x44444444;
     frame->eip = (void *)target;
 ​
     task->stack = (u32 *)stack;
 }
 ​
 task_create(a, thread_a);
复制代码

进程调度

其中最重要的函数schedule中的task_switch由于需要对寄存器进行操作,因此采用汇编实现

 void schedule()
 {
     // 第一次进入时候,current是main进程,后续才是ababa这样一直切换
     task_t *current = running_task();
     task_t *next = current == a ? b : a;
     task_switch(next);
 }
复制代码
 task_switch:
     push ebp
     mov ebp, esp
 ​
     push ebx
     push esi
     push edi
 ​
     mov eax, esp;
     and eax, 0xfffff000; current
 ​
     mov [eax], esp
 ;=======上面是保存切换前的环境,下面是恢复即将要切换的线程环境,其实最重要的一点就是
 ; esp的值,esp决定了此时在哪个进程的栈中
     mov eax, [ebp + 8]; next
     mov esp, [eax]
 ​
     pop edi
     pop esi
     pop ebx
     pop ebp
 ​
     ret
复制代码

差不多到此时,栈的切换完成,一旦ret,就会到进程a的代码

中断

上面可以看出我们是使用schedule来自己进行切换,而正常情况会出现抢占式的切换,就比如自己遇到一些情况,比如打印机需要纸,就会自动切换进程,这样就要使用中断来切换,中断就是一个函数

中断向量表

由于中断就是一个函数,因此有一个表来存放这个函数的地址,到时候调用中断时候,去表里面查询调用的函数序号就知道具体调用什么函数,在实模式下,处理器要求将它们的入口点集中存放到内存中从物理地址 0x000 开始,到 0x3ff 结束,共 1 KB 的空间内,一共256个中断向量,中断向量是指向中断函数的指针。一个向量包括4个字节,前2个字节为段内偏移,后2个字节是段地址,调用方式为int num,下面我们来试一下实模式下的中断,我们将boot.asm改成下面,把跳转到loader的部分先注释掉

 ; 将该代码放在0x7c00 因为由007内核加载器.md文件可知,MBR加载区域就是从0x7c00开始
 [org 0x7c00]
 ​
 ;设置屏幕模式为文本模式,清除屏幕
 mov ax, 3
 int 0x10
 ​
 ;初始化段寄存器
 mov ax, 0
 mov ds, ax
 mov es, ax
 mov ss, ax
 mov sp, 0x7c00
 ​
 ;====================测试中断
 mov word [0x54 * 4], interrupt
 mov word [0x54 * 4 + 2],0
 int 0x54
 ;====================
 jmp $
 ​
 interrupt:
     mov si, string
     call print
     iret
 ​
 string:
     db ".",0
 ​
 ; print 函数需要三条语句
 ; mov ah 0x0e   mov al 字符串  int 0x10
 print:
     mov ah, 0x0e
 .next:
     mov al, [si]
     ; si相当于是指针,不断向后移动,知道遇到booting字符串最后的0
     cmp al, 0
     jz .done
     int 0x10
     inc si
     jmp .next
 .done:
     ret
复制代码

关键是这三行

 mov word [0x54 * 4], interrupt
 mov word [0x54 * 4 + 2],0
 int 0x54
复制代码

In fact, when we use int numto call an interrupt, we first register the function we want to call. As mentioned earlier, there are a total of 256, each 4 bytes, so for example, the above is int 0x54where the interruptfunction is registered 0x54 * 4.

image-20220513095830248

You can see that the print is successful.

However, in protected mode, since we rarely use segment addresses and offsets within segments, the above method is rarely used, but this idea is still retained. Let's talk about interrupts in protected mode.

Interrupt Descriptor Table

The interrupt vector table in real mode becomes an interrupt descriptor table in protected mode, and the interrupt vector in real mode becomes an interrupt descriptor in protected mode. Let's talk about the interrupt descriptor first.

We know that the interrupt vector actually points to the address of the function, but because the space of the interrupt descriptor becomes larger, a lot of other things have been added. Let's take a look at its structure first.

 typedef struct gate_t
 {
     u16 offset0;    // 段内偏移 0 ~ 15 位
     u16 selector;   // 代码段选择子
     u8 reserved;    // 保留不用
     u8 type : 4;    // 任务门/中断门/陷阱门
     u8 segment : 1; // segment = 0 表示系统段
     u8 DPL : 2;     // 使用 int 指令访问的最低权限
     u8 present : 1; // 是否有效
     u16 offset1;    // 段内偏移 16 ~ 31 位
 } _packed gate_t;
复制代码

The offset1sum offset2can be thought of as the address of the function pointed to, of course, it is divided into the upper 15-bit address and the lower 15-bit address

Similarly, all interrupt descriptors are aggregated into one table, which is of course the interrupt descriptor table. Similarly, there is a special register pointing to this interrupt descriptor table, which is IDT register, there are also two instructions

 lidt [idt_ptr]; 加载 idt 将idt_ptr指向的地方保存到IDT register
 sidt [idt_ptr]; 保存 idt 将IDT register存放的值放在idt_ptr中
复制代码

Let's implement the interrupt descriptor table in our system.

 global interrupt_handler
 ​
 interrupt_handler:
     xchg bx, bx
 ​
     push message
     call printk
     add esp, 4
 ​
     xchg bx, bx
     iret
 ​
 section .data
 ​
 message:
     db "default interrupt", 10, 0
复制代码
 gate_t idt[IDT_SIZE];
 pointer_t idt_ptr; // 本身这个变量是针对全局描述符表,因为中断描述符表的指针一样,所以公用
 ​
 extern void interrupt_handler();
 ​
 void interrupt_init()
 {
     for (size_t i = 0; i < IDT_SIZE; i++)
     {
         gate_t *gate = &idt[i];
         gate->offset0 = (u32)interrupt_handler & 0xffff;
         gate->offset1 = ((u32)interrupt_handler >> 16) & 0xffff;
         gate->selector = 1 << 3; // 代码段
         gate->reserved = 0;      // 保留不用
         gate->type = 0b1110;     // 中断门
         gate->segment = 0;       // 系统段
         gate->DPL = 0;           // 内核态
         gate->present = 1;       // 有效
     }
     idt_ptr.base = (u32)idt;
     idt_ptr.limit = sizeof(idt) - 1;
     // BMB;
     asm volatile("lidt idt_ptr\n");
 }
复制代码
 void kernel_init()
 {
     console_init();
     gdt_init();
     interrupt_init();
     return;
 }
复制代码
 _start:
     call kernel_init
     ; main.c返回
     int 0x80; 调用 0x80 中断函数 系统调用,因此在初始化中,将256整个中断描述符表的每一项中断描述符都指向interrupt_handler,所以随便调用哪个都可以
     jmp $
复制代码

Let's talk about its process. First of all, it is used in kernelthe main function kernel_initfunction to interrupt_initcreate an interrupt descriptor table with a size of 128, and initialize each interrupt descriptor. The function address pointed to by each descriptor is interrupt_handler, This function is to print default interrupt, and then note that when the kernel_initfunction returns, it will come to the _startfunction, which will be used in it int 0x80. In fact, there is no need to specify which number at this time, because 128 interrupt descriptors are the same, int 0x69and the same, less than 128.

image-20220513101813959

The result is out

\

Guess you like

Origin juejin.im/post/7097558120959836174