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

博客网址: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)

大家都在看

  • Linux 系统IO响应缓慢系统hang住

    应急处理:reboot 解决方法: sysctl -w vm.dirty_ratio=10 sysctl -w vm.dirty_background_ratio=5 sysctl…

    Linux 2023年6月13日
    0112
  • Linux备份之远程同步—rsync

    一、备份 1.1 什么是备份? 备份就是把重要的数据或者文件复制一份保存到另一个地方,实现不同主机之间的数据同步 1.2 为什么做备份? 数据在公司中是很重要的!!!备份就是为了恢…

    Linux 2023年5月27日
    0114
  • SpringSecurity

    1、环境搭建 1、导包,使用maven搭建项目 2、关闭thymeleaf缓存 3、导入静态资源和相应页面代码 4、建立controller层 2、权限与认证 运用了 Aop 切面…

    Linux 2023年6月14日
    098
  • xshell/bash/zsh 等终端鼠标滚轮乱码问题(转)

    终端上滚动鼠标,有可能不是预期的滚屏,而是出现一些乱码字符 解决方法:输入 reset命令 回车即可 注意: clear或者 ctrl+l是清屏命令,在此情况下无效。 转自: xs…

    Linux 2023年5月28日
    0185
  • PyTorch介绍-优化模型参数

    既然已经有模型和数据了,是时候在数据上优化模型参数来训练、验证和测试它了。模型训练是一个迭代过程;在每一次迭代( epoch),模型会作出一个预测,计算其预测误差( loss),收…

    Linux 2023年6月14日
    0113
  • [20210930]bbed读取数据块7 fffext.sh.txt

    [20210930]bbed读取数据块7 fffext.sh.txt –//一般bash shell脚本很少考虑执行效率,仅仅考虑利用它快速解决工作中遇到的问题. &#…

    Linux 2023年6月13日
    094
  • 思科CISCO ASA 5521 防火墙 Ipsec 配置详解

    版本信息: Cisco Adaptive Security Appliance Software Version 9.9(2) Firepower Extensible Opera…

    Linux 2023年6月6日
    0105
  • Redis之延迟监控

    *参考官方文档 *启用 redis 延迟监控 CONFIG SET latency-monitor-threshold 100 单位:毫秒,100表示一百毫秒。如果将 latenc…

    Linux 2023年5月28日
    0102
  • Redis优化总结

    注意在redis.conf中的小聚合数据类型的特殊编码设置(http://carlosfu.iteye.com/blog/2254572) hash-max-zipmap-entr…

    Linux 2023年5月28日
    090
  • DNS 查询原理详解

    你可能会问,难道 DNS 服务器(比如 1.1.1.1)保存了世界上所有域名(包括二级域名、三级域名)的 IP 地址? 当然不是。DNS 是一个分布式系统,1.1.1.1 只是用户…

    Linux 2023年6月8日
    073
  • 多用户共享文件

    假设有三个用户:Tom Jerry Bob.其中,tom和Jerry都属于market部,Bob属于tech部,请在/usr目录下创建两个用户共享的目录market和public,…

    Linux 2023年6月13日
    0107
  • apache tomcat 目录session应用信息漏洞

    Tomcat 是一款开源的 Web 应用服务器软件。Tomcat 属于轻量级应用服务器,在中小型系统和并发访问用户不多的场合下被普遍使用,是开发和调试 JSP 程序的首选。 漏洞描…

    Linux 2023年6月7日
    0149
  • python爬虫_入门_翻页

    写出来的爬虫,肯定不能只在一个页面爬,只要要爬几个页面,甚至一个网站,这时候就需要用到翻页了 其实翻页很简单,还是这个页面http://bbs.fengniao.com/forum…

    Linux 2023年6月6日
    091
  • IEEE浮点数标准

    IEEE浮点数标准 阅读笔记:Computer System : A Programmmer’s Perspective 基本概念 IEEE浮点数标准采用 [V=(-1…

    Linux 2023年6月8日
    0129
  • Linux系统僵尸进程详解

    大安好,我是良许。 本文我们将来讨论一下什么是僵尸进程,僵尸进程是怎么产生的,如何杀死一个僵尸进程。 Linux中的进程是什么? 讲到进程,我们要先了解一下另一个概念: &…

    Linux 2023年6月14日
    0163
  • Linux 0.11源码阅读笔记-总结

    Linux 0.11主要包含文件管理和进程管理两个部分。进程管理包括内存管理、进程管理、进程间通信模块。文件管理包含磁盘文件系统,打开文件内存数据。磁盘文件系统包括空闲磁盘块管理,…

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