操作系统实现-中断及任务调度

博客网址:www.shicoder.top
微信:18223081347
欢迎加群聊天 :452380935

这一次我们来对内核进行完善,主要包括全局描述符的加载、任务调度、中断等

全局描述符的加载

我们回顾下,是不是在 loader中有关于全局描述符的一些代码

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

我们在准备进入保护模式的时候,将 gdt_ptr指向的地方,加载到 gdt寄存器中,那么难道进入保护模式,也就是内核阶段,难道就不用了吗,当然不是,而且你想,我们在 loader中,只有2个段,一个是代码段,一个数据段,而一共有8192个段,那么其他段在内核使用的时候,怎么加载呢,因此我们需要在内核中重新定义一个数组,先初始化8192个(当然在我这个内核中,用不到这么多,其实只初始化了128个),然后先将之前保护模式下 gdt寄存器的值赋值到这个数组中,再将数组的地址加载到 gdt寄存器中,那么知道了这个步骤,我们就开始吧

#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();

先将结构定义一下,因为在内核中,我们可以使用c语言编写,因此这里的定义不像 loader那样用汇编,是不是感觉也看的简单点啦,最主要是一个 gdt_init函数,它就是我们刚刚那个步骤的主要实现

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个全局描述符
}

还是挺简单的嘛,就这么一点,注意,我们的内核只有一个128的数组,并没实现8192,但正常来说, linux肯定是8192

任务及调度

简单来说,一个任务可以想成一个进程,那么每个进程都需要有自己的一个栈来保存自己运行时候所需要的信息,在这次的代码编写中,为了简化,一个进程的栈占一页内存,同时其结构如下

操作系统实现-中断及任务调度

因此任务调度就是将此时的栈切换为下一个进程的栈,那么切换肯定要知道切换之后要保存哪些东西,这个是由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

其实就是我们在使用 int num调用一个中断时候,先将我们要调用的函数注册进来,前面说到,一共256个,每个4字节,因此比如上面那个是 int 0x54,则将 interrupt函数注册到 0x54 * 4的地方

操作系统实现-中断及任务调度

可以看到成功打印出 .

但是在保护模式下,由于我们很少使用段地址和段内偏移,因此很少使用上面的方式,但还是将这种思想保留了下来,下面我们来说下保护模式下的中断把

中断描述符表

在实模式下的中断向量表,在保护模式下变为了中断描述符表,实模式下的中断向量,在保护模式下变为了中断描述符,我们先来说中断描述符把

我们知道中断向量其实就是指向函数的地址,但是中断描述符因为空间变大了,因此添加了很多其他的东西,先看下它的结构体把

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;

其中的 offset1offset2可以想成指向的函数的地址,当然这里分割为高15位地址和低15位地址

同样,将所有的中断描述符聚合成一个表,当然就是中断描述符表,同理有一个特殊的寄存器指向这个中断描述符表,它就是 IDT register,同样有两个指令

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

下面我们就来实现我们系统中的中断描述符表把

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 $

我们来说下它的流程把,首先是在 kernel的主函数 kernel_init函数中使用 interrupt_init创建了一个大小为128个中断描述符表,且初始化了每个中断描述符,每个描述符指向的函数地址都是 interrupt_handler,这个函数就是打印 default interrupt,然后注意,当 kernel_init函数返回时候,就到了 _start函数,里面使用了 int 0x80,其实这个时候不需要指定哪个数,因为128个中断描述符都是一样的, int 0x69也一样,小于128就行

操作系统实现-中断及任务调度

结果出来啦

Original: https://www.cnblogs.com/shilinkun/p/16271169.html
Author: 小坤学习园
Title: 操作系统实现-中断及任务调度

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/607197/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 剑指offer计划23( 数学简单)—java

    1.1、题目1 剑指 Offer 39. 数组中出现次数超过一半的数字 1.2、解法 万能哈希表,数学方法看到我要吐血。。。 1.3、代码 class Solution { pub…

    Linux 2023年6月11日
    0121
  • 基于 vite 创建 vue3 全家桶项目(vite + vue3 + tsx + pinia)

    vite 最近非常火,它是 vue 作者尤大神发布前端构建工具,底层基于 Rollup,无论是启动速度还是热加载速度都非常快。vite 随 vue3 正式版一起发布,刚开始的时候与…

    Linux 2023年6月7日
    0164
  • netstat 命令查看端口状态详解

    转载请注明出处: netstat 可以查看服务器当前端口列表及指定端口的连接状态等; -t : 指明显示TCP端口,t是TCP的首字母。 -u : 指明显示UDP端口,u是UDP的…

    Linux 2023年6月14日
    0104
  • 在docker中使用主机串口通讯

    在进行软件docker化的过程时,很大的一个阻碍就是软件与各种外围硬件设备的交互,网口通信的设备能够很容易地接入容器,但是串口设备则要复杂一些。本文讨论在windows和linux…

    Linux 2023年6月6日
    0126
  • Liunx-LVM创建与扩容

    LVM是 Logical Volume Manager(逻辑卷管理)的简写,它是Linux环境下对磁盘分区进行管理的一种机制,它由Heinz Mauelshagen在Linux 2…

    Linux 2023年6月8日
    0121
  • redis的三种集群方式

    博客园 :当前访问的博文已被密码保护 请输入阅读密码: Original: https://www.cnblogs.com/heqiyoujing/p/14494037.htmlA…

    Linux 2023年5月28日
    0121
  • Linux嵌套目录权限的比较探究

    在/tmp目录下新建一个嵌套目录,名字分别为test_0、test_1、test_2。在test_2目录下新建普通文件,名为tryme。设置test_0和test_2的权限为777…

    Linux 2023年6月7日
    0110
  • 【EventOS Nano】EventOS Nano初步了解

    EventOS Nano是什么? EventOS Nano是一个面向单片机、事件驱动的、分布式的、可跨平台开发的嵌入式开发平台。主要有两大技术特色: 事件驱动和 超轻量 Event…

    Linux 2023年6月13日
    0119
  • centos7 删除grub2 开头的所有文件后故障修复

    环境:centos7 mini故障原因:删除根目录,/boot以及/boot/efi下递归查找到的所有grub2 开头的文件现象:无法正常开机,开机显示下图 解法(该解法过程中照片…

    Linux 2023年6月6日
    0162
  • Linux Ubuntu 下载&安装 MySQL

    1. 下载安装 下载&安装:一句搞定 sudo apt update sudo apt install mysql-server 查看版本信息 mysql –versio…

    Linux 2023年6月14日
    0124
  • python装饰器(新年第一写)

    祭奠碌碌无为的2018,想想其实也不算碌碌无为,至少我还搞懂了装饰器,写了一堆有用没用的玩意 原来觉得装饰器挺难的,直到2018年的最后几天,突然就明白了,难道这就是传说中的开天聪…

    Linux 2023年6月6日
    0125
  • Redis-实现SpringBoot集成Redis多数据源

    背景​ 有些时候在一个项目里,由于业务问题,可能仅仅操作一个Redis数据源已经不能满足,比如某个运营系统,对接着多个不同的服务,处理数据时又不想通过远程调用,那只能增加一个数据源…

    Linux 2023年5月28日
    0107
  • Linux下如何切割与合并大文件

    我们传输一个大文件时,有时网络比较慢,需要花费很长时间才能传输完成,或者传输的过程中,网络不稳定,有可能导致此次传输失败,针对这种情况,我们可以把大文件切分成小文件,再逐个的传输到…

    Linux 2023年6月13日
    0151
  • 部署office在线预览服务器(Office Web Apps Server)

    引言为方便在web端方便的使用office。 简介 Office Online Server (OOS,下文简写为OOS ) 提供基于浏览器的 Word、PowerPoint、Ex…

    Linux 2023年6月14日
    0169
  • Spring Boot yaml配置文件解析

    1、Spring Boot 配置文件类型和作用 2、yaml 配置文件简介 3、yaml 基础语法 3.1、配置【基本】数据类型 3.2、配置【Object、Map】数据类型 3….

    Linux 2023年6月8日
    0121
  • python 练习题:小明的成绩从去年的72分提升到了今年的85分,请计算小明成绩提升的百分点

    python;gutter:true; -<em>- coding: utf-8 -</em>- 小明的成绩从去年的72分提升到了今年的85分,请计算小明成…

    Linux 2023年6月8日
    0110
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球