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

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

大家都在看

  • redis配置systemctl

    [Unit]Description=redisAfter=network.target [Service]Type=forkingPIDFile=/var/run/redis_63…

    Linux 2023年5月28日
    0106
  • 教你搞懂Jenkins安装部署!

    前言:请各大网友尊重本人原创知识分享,谨记本人博客: 南国以南i Jenkins介绍 Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作…

    Linux 2023年6月14日
    0123
  • 不自由的自由职业

    大家好,我是良许,前码农,现在自由职业者。 有关注我朋友圈的小伙伴都知道,就在上周,我刚刚结束了长达 35 天的「假期」。 此言一出,立刻掀起了评论区留言狂潮,大家纷纷问我,你特么…

    Linux 2023年6月14日
    081
  • Ubuntu16.04修改IP

    ssh登录到服务。编辑网卡配置文件。 vim /etc/network/interfaces 先关闭DPCP,将 iface eth0 inet dhcp前面加上#号。 设置IP地…

    Linux 2023年6月6日
    0103
  • Linux显示IP和主机名

    (1) ifconfig:Linux ifconfig命令用于显示或设置网络设备。 语法: ifconfig [网络设备][down up -allmulti -arp -prom…

    Linux 2023年6月8日
    0105
  • [20220301]oracle如何定位使用library cache mutex.txt

    [20220301]oracle如何定位使用library cache mutex.txt –//这个问题实际上困扰我很久,我开始以为library cache buc…

    Linux 2023年6月13日
    090
  • git 的使用

    git 的使用 1、介绍 代码版本管理、协同开发 对文件(代码)进行版本管理 完成协同开发 项目,帮助程序员整合代码 i)帮助开发者合并开发的代码 ii)如果出现冲突代码的合并,会…

    Linux 2023年6月14日
    090
  • 【转】谈谈 JVM 内部锁升级过程

    一、加锁发生了什么 //System.out.println&#x90FD;&#x52A0;&#x4E86;&#x9501; public void…

    Linux 2023年6月16日
    0116
  • k8s/kubeadm 生产环境高可用集群部署

    kubeadm 生产环境集群部署 基本环境配置 kubeadm 安装方式自 1.14 版本以后,安装方法几乎没有任何变化,此文档可以尝试安装最新的 k8s 集群, centos 采…

    Linux 2023年6月14日
    094
  • Python 多线程

    import threading import time def userTest(aa,bb): print(aa) time.sleep(3) print(bb) if __n…

    Linux 2023年6月6日
    076
  • dockerfile

    基础结构 指令 from label maintainer run cmd export env add copy entrypoint volume user workdir o…

    Linux 2023年6月7日
    085
  • Nginx 配置文件说明

    bash;gutter:true;</p> <h1>定义Nginx运行的用户和用户组</h1> <p>user www www;&l…

    Linux 2023年6月8日
    093
  • win11下配置vue3版本

    安装node.js PS:全局需要使用管理员权限打开CMD** 下载nodejs的地址 选择左边就好 下载安装后,选择自己需要安装的盘符,即可,不再叙述。 打开CMD查看node是…

    Linux 2023年6月14日
    0102
  • 写给初学者的Linux errno 错误码机制

    不同于Java的异常处理机制, 当你使用C更多的接触到是基于错误码的异常机制, 简单来说就是当调用的函数发生异常时, 程序不会跳转到一个统一处理异常的地方, 取而代之的是返回一个整…

    Linux 2023年5月27日
    080
  • openssh升级

    bash;gutter:false; ssh -V</p> <p>cp -a /etc/ssh/sshd_config /etc/ssh/sshd_conf…

    Linux 2023年6月7日
    082
  • 尤娜故事-迷雾-springboot扮酷小技巧

    前情回顾 从前,有一个简单的通道系统叫尤娜…… 尤娜系统的第一次飞行中换引擎的架构垂直拆分改造 四种常用的微服务架构拆分方式 尤娜,我去面试了 正文 我回到…

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