2 Star 6 Fork 3

稀风/KOS

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
多进程并行执行.md 14.54 KB
一键复制 编辑 原始数据 按行查看 历史
稀风 提交于 2023-01-28 10:58 . 代码优化

引言

  • 我们的目标是仅仅就是创建一个任务,然后运行这个任务吗?
  • 显然不是,我们的目标是要多个任务同时运行
  • 问题:创建两个任务:TASK A 和 TASK B,执行 TASK A,那么,什么时候执行 TASK B 呢,又由谁来进行任务切换执行呢?

任务切换过程

  • TASK A 执行过程中,如果不被打断,那么 TASK B 就永远不会执行,需要在 TASK A 运行期间无条件的将其打断,如何打断呢?利用时钟中断打断任务的执行,而这个时钟中断我们在上一个章节中已经实现了

  • 打断了之后又要做什么工作吗?很显然,打断之后还必须保存打断前 TASK A 的运行状态,我们将其称之为保存上下文

  • 接下来就是要切换到 TASK B 执行,如何做到呢?答案就是恢复 TASK B 的上下文

    多任务切换

  • 继续看一下更加详细的过程

    多任务切换详细过程

目标

  • 使用时钟中断打断任务(每个任务执行固定的时间片)
  • 中断发生后立即进入中断服务程序
  • 在中断服务程序中完成上下文的保存并切换任务
  • 接下来就来分步实现代码

任务与时钟中断同时实现

  • 前面我们已经单独实现了 TASK A 与时钟中断,接下来就让它们两个同时实现在同一个程序中

  • 就在上一章节的代码基础上改动吧,上一章节已经实现了时钟中断,我们再加上任务 TASK A 的相关代码就可以了,见 main.c

  • 看起来一切都好,然而,当我们运行起来后,发现只有 TASK A 任务在执行,中断貌似并没有执行,什么原因呢?

  • 哈哈哈,深入查找原因,发现在创建任务 TaskCreat 函数中,eflags 被赋值为 0x3002(IPOL=3), 其中 bit9 IF 位并未被置 1, IF 为 0 表示 CPU 不使能外部中断,把 IF 位置 1

    task->reg.eflags = 0x3202;  // IOPL=3: 允许任务(特权级 3)进行 /O 操作; IF=1: 允许外部中断
  • 编译运行,感觉应该没问题了,然而实际上问题好像更严重了,这次不光中断没有执行,就连 TASK A 任务好像也没有执行,懵逼树上懵逼果,怪事练练

  • Ctrl + C 退出程序,使用 “reg” 命令,发现 esp 寄存器的值为 0x1b9357e4, 莫名其妙的一个值,并不是任务栈,也不是内核栈,这说明是栈出问题

  • 继续思考,当中断发生时,程序由特权级 3 跳转到特权级 0,CPU 使用栈由使用任务栈转到使用内核栈,而内核栈的是由 TSS 决定的,于是我们查看 loader.asm 中的 TSS,发现 esp0 的值为 0,栈顶怎么能是 0 呢,我们将内核栈顶设置为 0x7c00(BOOT_START_ADDR)

    TSS_SEGMENT:
        dd    0
        dd    BOOT_START_ADDR     ; esp0
        dd    DATA32_FLAT_SELECTOR; ss0
        dd    0                   ; esp1
        dd    0                   ; ss1
        dd    0                   ; esp2
        dd    0                   ; ss2
        times 4 * 18 dd 0
        dw    0
        dw    $ - TSS_SEGMENT + 2
        db    0xFF   
    TSS_LEN    equ    $ - TSS_SEGMENT
  • 再次编译运行,哈哈,这次终于一切 OK,看一下效果

    任务与时钟中断同时实现

深入思考

  • 上面的中断服务程序和任务看起来都成功执行了,似乎并没什么错误,那么,整个过程就一定是正确的吗?
  • 中断服务程序仅完成了逻辑功能,但在中断发生时并没有保存上下文,在中断发生后也没有恢复上下文

中断服务程序的重新设计

  • 中断发生时,立即保存上下文(寄存器)
  • 逻辑功能实现
  • 中断返回时恢复上下文

保存上下文

  • 任务切换的整个流程我们也已经有了一定的了解,各部分代码也基本上都实现过了,就剩下最后一个功能,保存上下文

  • 进程的初步实现 中,我们已经学习过了如何恢复上下文,那么保存上下文就是跟恢复上下文反着操作而已

    保存上下文

  • 首先我们要做的工作就是在中断发生时,让栈顶指针 esp 指针指向 reg 数据结构的末尾,不然中断发生时,① 中的 5 个寄存器会被入栈到内核栈中,这显然不是我们想要的

  • 如何才能实现中断发生时,栈顶指针 esp 指向 reg 数据结构的末尾呢?

  • 方法:把 reg 数据结构的末尾地址值放到 TSS 中 esp0 处即可

  • 问题又来了, TSS 在 loader 中实现,现在在内核中,找不到 TSS 位置

  • 解决方案:共享内存,在 “loader.asm” 把 TSS 位置信息放入共享内存中,在内核 “share.h” 宏定义其位置就可以了

    ; loader.asm 中
    PutDataToShare:
        ...
        ; 将 TSS 基地址放到共享内存 TSS_ENTRY_ADDR 中
        mov dword [TSS_ENTRY_ADDR], TSS_SEGMENT
        ; 将 TSS 大小放到共享内存 TSS_SIZE_ADDR 中
        mov dword [TSS_SIZE_ADDR], TSS_LEN
        ...
        ret
    // share.h 中
    #define TSS_ENTRY_ADDR          (SHARE_START_ADDR + 24)
    #define TSS_SIZE_ADDR           (SHARE_START_ADDR + 28)
  • TSS 数据结构我们还没有定义,在 “desc.h” 中定义一下吧

    typedef struct TSS
    {
        U32         previous;               // 上一个任务链接(TSS 选择符)
        U32         esp0;                   // 内核栈顶
        U32         ss0;                    // 内核栈基址
        U32         unused[22];             // 不使用
        U16         reserved;               // 保留
        U16         iomb;                   // I/O 位图基地址
    } TSS;
  • 利用指针很容易就实现对 TSS 中 esp0 进行修改

    TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR);                                   // 找到 TSS
    tss->esp0 = (U32)(&taskA.reg) + sizeof(taskA.reg); // 修改 TSS.esp0 = reg 的末尾

增加保存与恢复上下文代码实现

  • 任务切换的所有部分知识都已经了解了,接下来就来真正的实现代码吧

  • 主要相关代码:interrupt.asmmain.cdesc.h

  • 在任务切换 0x20 号中断服务程序如下:

    Int0x20_Entry:
        ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存
        pushad                      ; 保存通用寄存器
        push ds
        push es
        push fs 
        push gs
    
        mov esp, KERNEL_STACK       ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 
    
        call Int0x20Handle          ; 中断逻辑功能
    
        mov esp, [gRegAddr]         ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置
        ; 恢复上下文 
        pop gs
        pop fs
        pop es
        pop ds   
        popad                       ; 恢复通用寄存器
        iret
  • 由于中断前我们就已经将 TSS.esp0 修改位任务 TASK A 的上下文 reg 的末尾,CPU 进入中断时会自动将 ss esp eflags cs eip 这 5 个寄存器入栈,即上面图中 ①

  • 接下来的 ② ③ 保存上下文工作就好理解了

  • 再往后是 “mov esp, KERNEL_STACK” 执行语句,它是放在 “call Int0x20Handle” 之前的,因为此时 esp 经过上面的步骤后已经指向了 reg 数据结构的起始位置,接下来再进行入栈操作的话就会破坏 reg 上面的内存区数据,所以必须重新设置一下 esp 的值(KERNEL_STACK 值为 0x7c00)

  • “call Int0x20Handle” 这条语句没什么说的,这是中断逻辑功能部分

  • 再往下就是要恢复中断上下文,想要恢复上下文,关于恢复上下文我们在 进程的初步实现 中已经做过详细介绍了

  • 想要恢复上下文,首先就是找到 reg 的起始位置,然而目前情况是好像并不能找到 reg 的起始位置

  • 于是就利用全局变量来记住 reg 的起始位置

    gRegAddr = (U32)&taskA.reg;
  • 在有了上面的赋值操作后, “mov esp, [gRegAddr]” 这个语句就很好理解了吧,就是使得 esp 指向 reg 的起始位置

  • 再往下的语句就完全是我们前面实现过的恢复上下文了

  • 没图没真相,必须看一下运行效果图,虽然效果跟没有保存上下文和恢复上下文的一样

    增加保存与恢复上下文

再加一个任务 TASK B

  • 多任务并行执行,最少也得实现两个任务吧,代码见:main.c

  • 参照 TASK A 的代码,复制一个 TASK B,为了区分 TASK A,我们让 TASK B 循环打印 26 个字母

    TASK taskB = {0};     // 任务对象
    U08 taskB_stack[512];   // 任务私有栈
    void TaskBFunc(void)    // 任务执行函数
    {
        static U32 count = 0;
        while(1)
        {
            count++;
            if(count % 1000 == 0)
            {
                static U32 j = 0;
                SetCursorPos(0, 6);
                printk("TASK B: %c\n", (j++%26)+'A');   
            }  
        }
    }
  • 调用任务创建函数进行 TASK B 的初始化

    TaskCreat(&taskB, TaskBFunc, taskB_stack, 512, "Task B");   // 创建任务 TASK B
  • 任务切换是借助时钟中断实现的,所以任务切换代码肯定要写在时钟中断服务程序的逻辑函数 Int0x20Handle 中

    volatile TASK* gTask = NULL;
    void Int0x20Handle(void)
    {
        static U32 count = 0;
    
        if(count++ % 5 == 4)
        {
            gTask = (gTask == &taskA) ? &taskB : &taskA;
            TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR);               // 找到 TSS
            tss->esp0 = (U32)(&gTask->reg) + sizeof(gTask->reg);        // TSS.esp0 指向任务上下文数据结构 reg 的末尾
            gRegAddr = (U32)(&gTask->reg);                              // gRegAddr 指向任务上下文数据结构 reg 的起始位置
        }
        write_m_EOI();  
    }
  • 下面这条语句的作用是让 TSS.esp0 指向将要跳转的任务的上下文末尾,其作用是在下一次中断服务程序 Int0x20_Entry 进入时保存上下文

    tss->esp0 = (U32)(&gTask->reg) + sizeof(gTask->reg);
  • 下面这条语句的作用是使 gRegAddr 指向任务上下文数据结构 reg 的起始位置,当 Int0x20Handle 函数执行结束后,程序将执行 Int0x20_Entry 的下半部分,即恢复任务上下文,而恢复上下文的前提条件就是 esp 指向任务上下文数据结构 reg 的起始位置

    gRegAddr = (U32)(&gTask->reg); 
  • 上图

    新增一个任务 TASK B

代码优化

  • main 函数里面有点乱,简单优化一下
  • 重新在 “task.c” 文件中定义变量,替代 gTask 和 gRegAddr,并在 “task.h” 文件中使用 extern 声明这两个变量
    volatile TASK* current_task = NULL;       // 当前任务指针,永远指向当前任务; current_task 代替 gTask
    volatile U32 current_reg;                   // 当前任务的上下文起始位置; current_reg 代替 gRegAddr
  • 把 main 函数里面的杂乱语句整理放到一个函数中,该函数接口设计如下
    • 函数名称: E_RET TaskStart(TASK* task0)
    • 输入参数: TASK* task0 --任务指针
    • 输出参数: 无
    • 函数返回: E_OK:成功; E_ERR:失败
    • 其它说明:想要启动所有任务,只要启动第一个任务就可以了,其它任务将由任务调度启动
  • 函数具体实现:
    E_RET TaskStart(TASK* task0)
    {
        // 检查参数合法性
        if(NULL == task0)
            return E_ERR;
    
        current_task = task0;                                   // 当前任务指针指向 task0
        TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR);           // 找到 TSS
        tss->esp0 = (U32)(&task0->reg) + sizeof(task0->reg);    // TSS.esp0 指向 task0 的上下文数据结构 reg 的末尾   
        current_reg = (U32)&task0->reg;                         // current_reg 指向任务上下文数据结构 reg 的起始位置
        
        asm volatile("sti");                                    // 开中断
        SWITCH_TO(task0);                                       // 切换到 TASK A 执行
    
        return E_OK;
    }
  • 因为变量名更改,所以 Int0x20Handle 函数和 Int0x20_Entry 中都要记得修改。
  • 改动后的 main.c

解决打印异常

  • 程序长时间运行后,出现了下面异常现象,打印出现了异常

    进程切换时打印异常

  • 本以为是进程切换代码有问题,查了好久,最终确定进程切换代码并没有问题。打印是需要硬件支持的,一个进程正在打印时被切换到了另一进程,此时另一个进程也需要打印,两个打印同时进行,硬件并不支持这种的情况

  • 优化代码,在打印相关需要硬件参与的代码前加上关中断操作,在打印结束后开中断,保证硬件执行过程不被中断打断

  • 在相关硬件操作前后加上下面代码,用了 eflags_c 是为了记住硬件操作前的状态,执行完毕后要恢复到之前的状态

    // 获取 eflags 寄存器的值放到 eflags_c 变量中
    asm volatile("pushf;popl %%eax":"=a"(eflags_c));
    
    // 关闭外部中断
    asm volatile("cli");
    
    // 硬件操作,省略...
    
    // 若 bit9(IF 位) 为 1,则开启外部中断
    if(EFLAGS_IF(eflags_c))
        asm volatile("sti");
  • 打印函数只优化了 printk,其它打印相关函数并没有优化,主要是因为懒,那么此时 “print.h” 中相关打印函数声明就去掉了,只保留 printk 这一个接口函数以供使用,统一了也挺好,省的乱七八糟的

  • 另外,在设置光标位置 SetCursorPos 中端口操作后加了一定的延时,给硬件一定的处理时间

  • 稍不注意还有遗漏,在 “main.c” 中, SetCursorPos 和 printk 同时使用,也有极低概率出现 SetCursorPos 刚设置完光标位置,又被切换到其它任务的 printk 处执行,此时也会出现打印异常情况,最好是在整个设置光标和打印期间都关闭外部中断

    asm volatile("cli");
    SetCursorPos(0, 4);
    printk("change: %d\n", count);
    asm volatile("sti");
  • 跟硬件有关的问题查起来都很难,没啥道理可言,这个调好了换一个硬件又有其它的问题,就不深入纠结了,就此 over

  • 优化后的代码:print.cprint.hmain.c

  • 还有更好的优化方式,那就是给一个较大的打印数据循环缓冲区 + 硬件 DMA,只不过这种方式太麻烦了,我懒得搞,现在将就用吧,别频繁打印,慎用打印

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/thin-wind/KOS.git
git@gitee.com:thin-wind/KOS.git
thin-wind
KOS
KOS
main

搜索帮助