第09章下 任务调度

Posted perfy576

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第09章下 任务调度相关的知识,希望对你有一定的参考价值。

1 任务表

1.1 双向链表

进程中的就绪队列,锁的等待队列都需要带链表这一数据结构。

然后加上,和链表相关的一些操作。代码直接在最后贴上

2 任务初始化

2.1 扩展TaskStruct

需要在TaskStruct中加入:

  1. 该任务每次换上cpu后,能够执行的cpu嘀嗒数。可能是时钟中断间隔的倍数
  2. 该任务已经执行的嘀嗒数。不是每次发生中断都会进行任务调度的
  3. 链表节点,用于构建任务表

2.2 任务表

需要维护两个链表:

  1. 已就绪链表,将来的任务调度器从该链表中取出一个任务放上cpu执行
  2. 全部任务表,所有的任务都将放在这个表中

正在运行的任务,不在已就绪链表中,只在全部任务表中。(全部代码太长,直接在后面贴了)

// 就绪队列
struct List thread_ready_list;
// 所有任务队列
struct List thread_all_list;
// 当前,或是在中断中马上要切换到的任务
static struct ListElem *thread_tag;

2.2 任务创建

为了任务调度,需要给PCB结构加上和任务调度相关的数据:

  1. 一个任务每次换上cpu能够执行的嘀嗒数(是发生时钟中断的次数),这个值将等于任务的优先级
  2. 一个链表的节点,用于将该节点放入任务表中

因此扩充为:

// 进程的PCB控制块
struct TaskStruct
{
    uint32_t *self_kstack;
    enum TaskStatus status;
    uint8_t priority;
    char name[6];
    // 该任务从本次换上cpu后,还能执行的嘀嗒数,每次换上cpu后,都设置为 priority
    // 然后在时钟中断中,每次都--,当为0,的时候就进行调度
    uint8_t ticks; 
    // 该任务从开始运行,一共执行的嘀嗒数
    uint32_t elapsed_ticks;

    // 链表的节点元素,用于添加在队列中
    struct ListElem general_tag;
    // 同上,但是这个元素只能添加在全部任务表中
    struct ListElem all_list_tag;

    // 这是个魔术,完全使我们自定义的
    // 用于检查是否越界
    uint32_t stack_magic;
    // 还有一些字段,以后陆续添加
};

然后,thread_start()init_thread()需要初始化这些数据thread_create()不变。

// 全局变量用来记录第一个执行流的PCB结构
// 只是用于第一次初始化第一个执行流的PCB结构
struct TaskStruct *main_thread;

// 3. 初始化需要执行的任务
void thread_create(struct TaskStruct *pthread, ThreadFunc func, void *func_arg)
{
    // 当中断发生的时候,在idm.asm代码中会压入一系列的数据,
    // 这些数据在栈中,然后我们以压入数据结束时刻的栈的esp指针为首地址的,定义了一个
    // 结构提,该结构体就是 IntrStack ,
    // 也就是说在此时内核栈不为空,所以要跳过这些数据
    pthread->self_kstack -= sizeof(struct IntrStack);

    // 然后接下来偏移 ThreadStack 大小,
    // 此时esp就指向了 ThreadStack 的首指针
    // 然后填充该结构体
    pthread->self_kstack -= sizeof(struct ThreadStack);

    // 先将指针转换为 ThreadStack 结构体的指针,然后下面开始填充
    struct ThreadStack *kthread_stack = (struct ThreadStack *)pthread->self_kstack;

    // 本质上是调用 kernel_thread() 时候call之前栈中的数据,当然eip是call自己压入的
    // 这主要是为了 ret 时候,弹栈,能够使用
    // 填充的地址,pop到cs ip寄存器中,改变执行流
    kthread_stack->eip = kernel_thread;
    kthread_stack->func = func;
    kthread_stack->func_arg = func_arg;
    kthread_stack->ebp = 0;
    kthread_stack->ebx = 0;
    kthread_stack->esi = 0;
    kthread_stack->edi = 0;
}

// 2. 初始化PCB结构 TaskStruct
void init_thread(struct TaskStruct *pthread, char *name, int prio)
{
    // 清零,都是在获取页以后清零,在回收页的时候不会清零
    memset(pthread, 0, sizeof(*pthread));

    // 设置任务的名字
    strcpy(pthread->name, name);

    // 每一个新建的任务都是就绪态,然后放入就绪队列中
    // 但是 第一个执行流特殊,第一个执行流为自己创建PCB结构的时候
    // 他已经在运行了,所以是设置为TASK_RUNNING
    if (pthread == main_thread)
    {
        pthread->status = TASK_RUNNING;
    }
    else
    {
        pthread->status = TASK_READY;
    }

    // 设置该任务的栈为栈顶,后面会在 thread_create 中跳过cpu压栈和
    // 中断历程的压栈数据
    pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);
    // 设置任务的优先级
    pthread->priority = prio;
    // 每个每次换上cpu能够执行的滴答数等于其优先级
    pthread->ticks = prio;
    // 该任务从开始运行,到某个时刻一共运行的嘀嗒数
    pthread->elapsed_ticks = 0;

    // 因为 stack_magic 字段后面到该页的结束都是该任务的内核栈
    // 因此为了防止栈溢出以后破坏PCB结构,因此需要这魔数
    pthread->stack_magic = 0x19870916;
}

// 任务初始化流程,先分配一页,然后初始化该页,再初始化要执行的函数
struct TaskStruct *thread_start(char *name, int prio, ThreadFunc func, void *func_arg)
{
    // 1. 首先是去获取一页,作为存储PCB结构的页
    struct TaskStruct *thread = get_kernel_pages(1);

    // 2. 然后初始化该PCB结构
    init_thread(thread, name, prio);

    // 3. 初始化需要执行的任务
    thread_create(thread, func, func_arg);

    // 最后添加到全部任务队列和已就绪任务队列
    ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
    list_append(&thread_ready_list, &thread->general_tag);

    ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
    list_append(&thread_all_list, &thread->all_list_tag);

    return thread;
}

2.3 初始化第一个执行流

从开机到内核加载,一直只有一个执行流,该执行流进行了各种初始化。该执行流没有PCB结构。而其他任务创建的时候会使用thread_start()初始化后拥有PCB。为了能够让第一个执行流进行调度,需要为第一个执行流初始化,创建PCB结构。

第一个执行流的栈设置在0xc0009f00。而栈是PCB所在页高位向下扩展,因此第一个执行流的PCB在0xc0009e00,不需要去获取物理页了,同时也不需要去初始化执行流任务,所以它所需要的只是去创建PCB结构:

// 获取当前正在运行的pcb结构的首地址
// 其实就是将 esp 抹掉低4位,就可以了
// 因为都是在一页中,抹掉低4位,就能够获取该页的首地址,也就是PCB的首地址了
struct TaskStruct *running_thread()
{
    uint32_t esp;
    asm("mov %%esp, %0":"=g"(esp));
    return (struct TaskStruct *)(esp & 0xfffff000);
}

// 为第一个执行流创建PCB结构
static void make_main_thread(void)
{
    // 第一个执行流的栈是0xc0009f00,因此所在的页就是0xc0009e00
    main_thread = running_thread();
    init_thread(main_thread, "main", 31);
    ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
    list_append(&thread_all_list, &main_thread->all_list_tag);
}

2.4 任务初始化

任务初始化,除了为第一个执行流创建PCB还需要,初始化全局任务表,和已就绪任务表:

void thread_init()
{
    put_str("thread_init start\n");
    list_init(&thread_ready_list);
    list_init(&thread_all_list);
    make_main_thread();
    put_str("thread_init done\n");
}

3 任务调度

有了上面的准备,那么就可以做任务调度了

3.1 任务调度原理

任务调度是基于时钟中断的,每次时钟中断的中断函数,都去切换要执行的任务。

而"切换"的含义:每次中断发生,cpu将ss,esp,eflags,cs,eip,error_code(可能有)压入栈中,然后去执行对应的中断处理函数,而我么你编写的中断处理函数又会将段寄存器,通用寄存器压入栈中,紧接着去执行真正的中断处理函数(真正的意思是为每种中断编写的中断处理函数)。当中真正的中断处理函数执行完毕以后,就需要弹栈了:从esp开始线上,将之前压入的值弹出,直到cpu将它压入的eip,cs等弹出去,以及ss,esp弹出去,那么此次中断结束了。而如果我们在真正的中断处理函数中,将esp的值替换掉,替换为其他任务的esp值,换句话说,也就是替换其他任务的栈,(内核态的栈基址ss没有更换,只要控制esp,弹出的数据就可以了),也就是说:另一个任务的寄存器映像被加载到了寄存器中。

3.2 switch函数

根据上面的的,我们就可以编写一个替换esp的汇编代码

[bits 32]
section .text
; 两个参数依次是 当前任务,和要执行任务的 TaskStruct 结构的首指针
global switch_to
switch_to:
    ; 根据ABI规定,压入四个主调用这使用的寄存器
    push esi
    push edi
    push ebx
    push ebp

    ; TaskStruct 首指针,其实是指向他的self_kstack字段
    ; 该字段,其实正好记录的是该任务的 esp 
    ; 因此先将当前任务的esp保存
    mov eax, [esp + 20]      
    mov [eax], esp              

    ; 然后将要替换任务的esp加载上
    mov eax, [esp + 24]      
    mov esp, [eax]       

    ; 弹栈
    pop ebp
    pop ebx
    pop edi
    pop esi
    ret             

这段代码完成了新旧esp的替换。

然后这里分两种情况:

3.2.1 任务第一次执行

当任务第一次执行的时候:那么此时的栈是这样的(假设中断处理函数直接压入两个TaskStruct结构的首指针,然后call switch_to ):

此时,新任务的栈是:

因此,弹栈后,直接开始执行kernel_thread(),换句话说,也就是此时其实相当于还是在中断处理中,也就是eflagsif位是0,这是中断一开始的时候cpu主动关闭的。

因此我们需要在kernel_thread()打开中断:

// 这是一个包裹函数
// ThreadStack 结构体本质上是 kernel_thread call 之前的压栈数据
static void kernel_thread(ThreadFunc *func, void *func_arg)
{
    // 在中断处理函数中关中断,然后切换完成后开中断
    intr_enable();
    func(func_arg);
}

另外,我们之前在idt.asm中代码是这样的:

; 省略...
; 暴露给外部的接口,没有参数
global idt_entry_table
idt_entry_table:
    ; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
    %macro VECTOR 2   
        section .text
        ; %1 表示第一个参数,直接替换
        intr%1entry:
            %2
        
            push ds
            push es
            push fs
            push gs
            pushad  ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

            push %1 ;压栈中断号,作为 idt_handle 的参数

            call [idt_table+%1*4]
            add esp,4   ;跳过参数

            ; 手动模式下,需要主动的向主片和从片发送EOI信号
            mov al,0x20
            out 0xa0,al
            out 0x20,al
            
            popad   
            pop gs
            pop fs
            pop es
            pop ds
            add esp,4 ;跳过 error_code
            
            iretd
        section .data
            dd intr%1entry
    %endmacro 
    ; 省略...

注意,主动的向主片和从片发送EOI信号是在调用玩真正的处理函数以后才发送,也就是说,在真正的中断处理函数未执行完以前,即使我们在kernel_thread()中,打开中断intr_enable(),8259A也不会给我们发送新的中断,因为之前设置的是手动模式,8259A认为之前的中断处理函数还未执行完。因此,需要将发送EOI信号,提call [idt_table+%1*4]之前。也就是说在调用真正的信号处理函数之前就发送EOI信号,但是此时eflagsif位为0,不会响应中断。

因此新代码就是:

; 省略

            mov al,0x20
            out 0xa0,al
            out 0x20,al

            call [idt_table+%1*4]
; 省略

这样,即使我们在中断处理函数中改变了执行流,去执行kernel_thread(),只要事先向8259A发送了EOI信号,那8259A也会认为中断处理函数已经执行完了。但其实,我们骗了它。

当然在调用真正的信号处理函数这段时间,因为eflagsif位为0,因此即使8259A发来了新的中断,cpu也不会相应的。

3.2.2 任务不是第一次执行

相比上一个,好理解的多。

因为,切换到不是第一次执行的任务,它的栈是这样的:

那么就会沿这原路返回,完成中断的后半部分,就是弹栈的那一部分。

3.3 任务调度函数

主要是处理当前任务:是否再次加入就绪队列。然后选取一个已就绪任务。

然后调用switch()

// 任务调度函数
void schedule()
{
    ASSERT(intr_get_status() == INTR_OFF);
    struct TaskStruct *cur = running_thread();
    
    // 如果一个任务被阻塞,那么在进入 schedule 之前就会被设置为 TASK_BLOCKED
    // 因此如果在 schedule 中是 TASK_RUNNING ,那么证明时间片用完了
    if (cur->status == TASK_RUNNING)
    { 
        // 时间片用完了,那么直接加入到就绪队列中,然后重新设置嘀嗒数(时间片)
        ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
        list_append(&thread_ready_list, &cur->general_tag);
        cur->ticks = cur->priority; 
        // 并设置为 TASK_READY 表示随时可以执行
        cur->status = TASK_READY;
    }
    else
    {
        // 如果需要等待资源,那么需要另外处理
    }
    ASSERT(!list_empty(&thread_ready_list));

    // 取一个任务,thread_tag 是要换上cpu的那个任务的PCB的general_tag字段的指针
    thread_tag = NULL;
    thread_tag = list_pop(&thread_ready_list);
    // 根据其 thread_tag 计算该任务的PCB
    struct TaskStruct *next = elem2entry(struct TaskStruct, general_tag, thread_tag);
    // 该任务设置为 TASK_RUNNING 
    next->status = TASK_RUNNING;
    // 调用 switch_to ,去切换esp
    switch_to(cur, next);
}

3.4 修改时钟中断的中断处理函数

这里初始化了8253,是一个专门的时钟中断的硬件,这部分太麻烦,所以跳过了8253的初始化,最后直接贴代码。知道就行。

和时钟中断相关的代码被放在./device/timer.c

// 时钟中断处理函数
static void intr_timer_handler(void)
{
  struct TaskStruct *cur_thread = running_thread();

  // 检查是 内核栈是否溢出
  ASSERT(cur_thread->stack_magic == 0x19870916); 
  // 该任务从开始执行到现在所执行的嘀嗒数
  cur_thread->elapsed_ticks++;
  
  // 计算机从开机到现在所执行的滴答数 
  ticks++;                    

  // 如果时间片(嘀嗒数)用完,那么需要切换。否则嘀嗒数--,
  // 退出中断处理函数
  if (cur_thread->ticks == 0)
  { 
    schedule();
  }
  else
  { 
    cur_thread->ticks--;
  }
}

另外,需要在interrupt.c中,加一个函数,用于为指定的中断,设置一个新的中断处理函数,同时在interput.h中暴露接口。

// interrupt.c
// 为指定的中断,设置一个新的中断处理函数
void register_handler(uint8_t vector_no, void* function) {
   idt_table[vector_no] = function; 
}

最终的timer_init()

// 初始化 timer
void timer_init()
{
  put_str("timer_init start\n");
  //  初始化8253
  frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
  // 为 0x20 时钟中断,设置新的中断处理函数
  register_handler(0x20, intr_timer_handler);
  put_str("timer_init done\n");
}

4 代码

目录结构:

└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 06a.tar.gz
├── 07a.tar.gz
├── 07b.tar.gz
├── 07c.tar.gz
├── 08.tar.gz
├── 09a.tar.gz
├── 09b
│?? ├── boot
│?? │?? ├── include
│?? │?? │?? └── boot.inc
│?? │?? ├── loader.asm
│?? │?? └── mbr.asm
│?? ├── build
│?? │?? ├── bitmap.o
│?? │?? ├── debug.o
│?? │?? ├── idt.o
│?? │?? ├── init.o
│?? │?? ├── interrupt.o
│?? │?? ├── kernel.bin
│?? │?? ├── kernel.o
│?? │?? ├── list.o
│?? │?? ├── loader.bin
│?? │?? ├── mbr.bin
│?? │?? ├── memory.o
│?? │?? ├── print.o
│?? │?? ├── string.o
│?? │?? ├── switch.o
│?? │?? ├── thread.o
│?? │?? └── timer.o
│?? ├── device
│?? │?? ├── timer.c
│?? │?? └── timer.h
│?? ├── kernel
│?? │?? ├── debug.c
│?? │?? ├── debug.h
│?? │?? ├── global.h
│?? │?? ├── idt.asm
│?? │?? ├── init.c
│?? │?? ├── init.h
│?? │?? ├── interrupt.c
│?? │?? ├── interrupt.h
│?? │?? ├── main.c
│?? │?? ├── memory.c
│?? │?? └── memory.h
│?? ├── lib
│?? │?? ├── kernel
│?? │?? │?? ├── bitmap.c
│?? │?? │?? ├── bitmap.h
│?? │?? │?? ├── io.h
│?? │?? │?? ├── list.c
│?? │?? │?? ├── list.h
│?? │?? │?? ├── print.asm
│?? │?? │?? ├── print.h
│?? │?? │?? ├── string.c
│?? │?? │?? └── string.h
│?? │?? └── libint.h
│?? ├── makefile
│?? ├── start.sh
│?? └── thread
│?? ├── switch.asm
│?? ├── thread.c
│?? └── thread.h
└── hd60m.img

4.0 bochs任意断点

bochs的断点只能在调试的时候打。但是如果开了?magic_break: enabled=1选项,则可以在编写代码的时候打上断点。然后在调试程序的时候,就可以不用再打断点了。

因此配置文件改为:

# bochs 能够使用的内存,单位MB
megs:32

# 真是机器的Bios和VGA BIOS
romimage:file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage:file=/usr/share/bochs/VGABIOS-lgpl-latest


# 启动盘
boot:disk

# 日志输出
log:/tmp/bochs.log

# 开关功能
mouse:enabled=0
keyboard:keymap=/usr/share/bochs/keymaps/x11-pc-us.map  

# 硬盘设置
ata0-master: type=disk, path="hd60m.img", mode=flat, cylinders=121, heads=16, spt=63

magic_break: enabled=1

就新加了最后一行。

然后在代码中,需要打断点的地方,加上汇编代码

__asm__("xchg %%bx,%%bx"::); 

就可以实现在任何地方实现断点。

我们将这条指令封装在debug.h

#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H

void panic(char *filename, int line, const char *func, const char *cond);

#define PANIC(cond) panic(__FILE__, __LINE__, __func__, cond)

#ifdef NDEBUG
      #define ASSERT(cond) ((void)0)
      #define BREAK() ((void)0); 
      
#else
      #define ASSERT(cond)      \
            if (cond)           \
            {                   \
            }                   \
            else                \
            {                   \
                  PANIC(#cond); \
            }
      #define BREAK() __asm__("xchg %%bx,%%bx"::); 

#endif /*__NDEBUG */

#endif /*__KERNEL_DEBUG_H*/

4.1 switch.asm(新)

[bits 32]
section .text
; 两个参数依次是 当前任务,和要执行任务的 TaskStruct 结构的首指针
global switch_to
switch_to:
    ; 根据ABI规定,压入四个主调用这使用的寄存器
    push esi
    push edi
    push ebx
    push ebp

    ; TaskStruct 首指针,其实是指向他的self_kstack字段
    ; 该字段,其实正好记录的是该任务的 esp 
    ; 因此先将当前任务的esp保存
    mov eax, [esp + 20]      
    mov [eax], esp              

    ; 然后将要替换任务的esp加载上
    mov eax, [esp + 24]      
    mov esp, [eax]       

    ; 弹栈
    pop ebp
    pop ebx
    pop edi
    pop esi
    ret             

4.2 thread.h/thread.c

#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H

#include "libint.h"
#include "list.h"

// 被线程执行的函数
typedef void ThreadFunc(void *);

// 几种任务的状态
enum TaskStatus
{
    TASK_RUNNING,
    TASK_READY,
    TASK_BLOCKED,
    TASK_WAITING,
    TASK_HANGING,
    TASK_DIED,
};

// 由时钟中断保存的任务上下文.
struct IntrStack
{
    // 下面的代码是我们idm.asm代码压入的东西
    // 这个是在idt.asm汇编的时候,push %1 压入的
    // push %1 最后压入,所以在第一个
    // 中断向量号,在这里面应该都是时钟中断
    uint32_t vec;
    uint32_t edi;
    uint32_t esi;
    uint32_t ebp;
    // 这里esp没看懂
    // 解释是:虽然pushad,会把esp也压入,但是esp因为压栈一直在变化
    // 所以会被popad忽略
    uint32_t esp_dummy;
    uint32_t ebx;
    uint32_t edx;
    uint32_t ecx;
    uint32_t eax;
    uint32_t gs;
    uint32_t fs;
    uint32_t es;
    uint32_t ds;

    // 下面是cpu从地特权先搞特权是压入
    // 是cpu自己压入的。
    uint32_t err_code;
    void (*eip)(void);
    uint32_t cs;
    uint32_t eflags;
    void *esp;
    uint32_t ss;
};

// 线程第一次执行时候需要的数据
// 例如函数地址,函数参数
struct ThreadStack
{
    // 这四个寄存器是因为ABI的缘故,需要压入
    // 由我们写的汇编代码 pop
    uint32_t ebp;
    uint32_t ebx;
    uint32_t edi;
    uint32_t esi;

    // 下面就是 ret 后 eip的值 pop到eip中,是近跳转,所以没cs
    // 剩下的都是参数
    void (*eip)(ThreadFunc *func, void *arg);

    // 这些都是为  kernel_thread(ThreadFunc *func,void* func_arg)
    // 准备的参数
    void(*unused_retaddr);
    ThreadFunc *func; // 这是个参数,以后讲。
    void *func_arg;
};

// 进程的PCB控制块
struct TaskStruct
{
    uint32_t *self_kstack;
    enum TaskStatus status;
    uint8_t priority;
    char name[6];
    // 该任务从本次换上cpu后,还能执行的嘀嗒数,每次换上cpu后,都设置为 priority
    // 然后在时钟中断中,每次都--,当为0,的时候就进行调度
    uint8_t ticks; 
    // 该任务从开始运行,一共执行的嘀嗒数
    uint32_t elapsed_ticks;

    // 链表的节点元素,用于添加在队列中
    struct ListElem general_tag;
    // 同上,但是这个元素只能添加在全部任务表中
    struct ListElem all_list_tag;

    // 这是个魔术,完全使我们自定义的
    // 用于检查是否越界
    uint32_t stack_magic;
    // 还有一些字段,以后陆续添加
};

void thread_create(struct TaskStruct *pthread, ThreadFunc function, void *func_arg);
void init_thread(struct TaskStruct *pthread, char *name, int prio);
struct TaskStruct *thread_start(char *name, int prio, ThreadFunc function, void *func_arg);
void thread_init(void);
struct TaskStruct *running_thread(void);
void schedule(void);
void thread_init(void);
#endif
#include "thread.h"
#include "string.h"
#include "print.h"
#include "memory.h"
#include "interrupt.h"
#include "debug.h"

#define PG_SIZE 4096

extern void switch_to(struct TaskStruct *cur, struct TaskStruct *next);

// 全局变量用来记录第一个执行流的PCB结构
// 只是用于第一次初始化第一个执行流的PCB结构
struct TaskStruct *main_thread;

// 就绪队列
struct List thread_ready_list;
// 所有任务队列
struct List thread_all_list;
// 当前,或是在中断中马上要切换到的任务
static struct ListElem *thread_tag;

// 这是一个包裹函数
// ThreadStack 结构体本质上是 kernel_thread call 之前的压栈数据
static void kernel_thread(ThreadFunc *func, void *func_arg)
{
    // 在中断处理函数中关中断,然后切换完成后开中断
    intr_enable();
    func(func_arg);
}

// 获取当前正在运行的pcb结构的首地址
// 其实就是将 esp 抹掉低4位,就可以了
// 因为都是在一页中,抹掉低4位,就能够获取该页的首地址,也就是PCB的首地址了
struct TaskStruct *running_thread()
{
    uint32_t esp;
    asm("mov %%esp, %0":"=g"(esp));
    return (struct TaskStruct *)(esp & 0xfffff000);
}

// 3. 初始化需要执行的任务
void thread_create(struct TaskStruct *pthread, ThreadFunc func, void *func_arg)
{
    // 当中断发生的时候,在idm.asm代码中会压入一系列的数据,
    // 这些数据在栈中,然后我们以压入数据结束时刻的栈的esp指针为首地址的,定义了一个
    // 结构提,该结构体就是 IntrStack ,
    // 也就是说在此时内核栈不为空,所以要跳过这些数据
    pthread->self_kstack -= sizeof(struct IntrStack);

    // 然后接下来偏移 ThreadStack 大小,
    // 此时esp就指向了 ThreadStack 的首指针
    // 然后填充该结构体
    pthread->self_kstack -= sizeof(struct ThreadStack);

    // 先将指针转换为 ThreadStack 结构体的指针,然后下面开始填充
    struct ThreadStack *kthread_stack = (struct ThreadStack *)pthread->self_kstack;

    // 本质上是调用 kernel_thread() 时候call之前栈中的数据,当然eip是call自己压入的
    // 这主要是为了 ret 时候,弹栈,能够使用
    // 填充的地址,pop到cs ip寄存器中,改变执行流
    kthread_stack->eip = kernel_thread;
    kthread_stack->func = func;
    kthread_stack->func_arg = func_arg;
    kthread_stack->ebp = 0;
    kthread_stack->ebx = 0;
    kthread_stack->esi = 0;
    kthread_stack->edi = 0;
}

// 2. 初始化PCB结构 TaskStruct
void init_thread(struct TaskStruct *pthread, char *name, int prio)
{
    // 清零,都是在获取页以后清零,在回收页的时候不会清零
    memset(pthread, 0, sizeof(*pthread));

    // 设置任务的名字
    strcpy(pthread->name, name);

    // 每一个新建的任务都是就绪态,然后放入就绪队列中
    // 但是 第一个执行流特殊,第一个执行流为自己创建PCB结构的时候
    // 他已经在运行了,所以是设置为TASK_RUNNING
    if (pthread == main_thread)
    {
        pthread->status = TASK_RUNNING;
    }
    else
    {
        pthread->status = TASK_READY;
    }

    // 设置该任务的栈为栈顶,后面会在 thread_create 中跳过cpu压栈和
    // 中断历程的压栈数据
    pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);
    // 设置任务的优先级
    pthread->priority = prio;
    // 每个每次换上cpu能够执行的滴答数等于其优先级
    pthread->ticks = prio;
    // 该任务从开始运行,到某个时刻一共运行的嘀嗒数
    pthread->elapsed_ticks = 0;

    // 因为 stack_magic 字段后面到该页的结束都是该任务的内核栈
    // 因此为了防止栈溢出以后破坏PCB结构,因此需要这魔数
    pthread->stack_magic = 0x19870916;
}

// 任务初始化流程,先分配一页,然后初始化该页,再初始化要执行的函数
struct TaskStruct *thread_start(char *name, int prio, ThreadFunc func, void *func_arg)
{
    // 1. 首先是去获取一页,作为存储PCB结构的页
    struct TaskStruct *thread = get_kernel_pages(1);

    // 2. 然后初始化该PCB结构
    init_thread(thread, name, prio);

    // 3. 初始化需要执行的任务
    thread_create(thread, func, func_arg);

    // 最后添加到全部任务队列和已就绪任务队列
    ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
    list_append(&thread_ready_list, &thread->general_tag);

    ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
    list_append(&thread_all_list, &thread->all_list_tag);

    return thread;
}

// 为第一个执行流创建PCB结构
static void make_main_thread(void)
{
    // 第一个执行流的栈是0xc0009f00,因此所在的页就是0xc0009e00
    main_thread = running_thread();
    init_thread(main_thread, "main", 31);
    ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
    list_append(&thread_all_list, &main_thread->all_list_tag);
}

void thread_init()
{
    put_str("thread_init start\n");
    list_init(&thread_ready_list);
    list_init(&thread_all_list);
    make_main_thread();
    put_str("thread_init done\n");
}

// 任务调度函数
void schedule()
{
    ASSERT(intr_get_status() == INTR_OFF);
    struct TaskStruct *cur = running_thread();
    
    // 如果一个任务被阻塞,那么在进入 schedule 之前就会被设置为 TASK_BLOCKED
    // 因此如果在 schedule 中是 TASK_RUNNING ,那么证明时间片用完了
    if (cur->status == TASK_RUNNING)
    { 
        // 时间片用完了,那么直接加入到就绪队列中,然后重新设置嘀嗒数(时间片)
        ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
        list_append(&thread_ready_list, &cur->general_tag);
        cur->ticks = cur->priority; 
        // 并设置为 TASK_READY 表示随时可以执行
        cur->status = TASK_READY;
    }
    else
    {
        // 如果需要等待资源,那么需要另外处理
    }
    ASSERT(!list_empty(&thread_ready_list));

    // 取一个任务,thread_tag 是要换上cpu的那个任务的PCB的general_tag字段的指针
    thread_tag = NULL;
    thread_tag = list_pop(&thread_ready_list);
    // 根据其 thread_tag 计算该任务的PCB
    struct TaskStruct *next = elem2entry(struct TaskStruct, general_tag, thread_tag);
    // 该任务设置为 TASK_RUNNING 
    next->status = TASK_RUNNING;
    // 调用 switch_to ,去切换esp
    switch_to(cur, next);
}

4.4 timer.h/timer.h

#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
#include "libint.h"
void timer_init(void);
#endif
#include "timer.h"
#include "io.h"
#include "print.h"
#include "interrupt.h"
#include "thread.h"
#include "debug.h"

#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43

// ticks是内核自中断开启以来总共的嘀嗒数
uint32_t ticks;

static void frequency_set(uint8_t counter_port,
                          uint8_t counter_no,
                          uint8_t rwl,
                          uint8_t counter_mode,
                          uint16_t counter_value)
{
  outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
  outb(counter_port, (uint8_t)counter_value);
  outb(counter_port, (uint8_t)counter_value >> 8);
}

// 时钟中断处理函数
static void intr_timer_handler(void)
{
  struct TaskStruct *cur_thread = running_thread();

  // 检查是 内核栈是否溢出
  ASSERT(cur_thread->stack_magic == 0x19870916); 
  // 该任务从开始执行到现在所执行的嘀嗒数
  cur_thread->elapsed_ticks++;
  
  // 计算机从开机到现在所执行的滴答数 
  ticks++;                    

  // 如果时间片(嘀嗒数)用完,那么需要切换。否则嘀嗒数--,
  // 退出中断处理函数
  if (cur_thread->ticks == 0)
  { 
    schedule();
  }
  else
  { 
    cur_thread->ticks--;
  }
}

// 初始化 timer
void timer_init()
{
  put_str("timer_init start\n");
  //  初始化8253
  frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
  // 为 0x20 时钟中断,设置新的中断处理函数
  register_handler(0x20, intr_timer_handler);
  put_str("timer_init done\n");
}

4.5 init.h/init.c

#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "memory.h"
#include "timer.h"
#include "thread.h"


/*负责初始化所有模块 */
void init_all() {
   put_str("init_all\n");
   idt_init();   //初始化中断
   mem_init();
   thread_init();
   timer_init();  
}

4.6 idt.asm

[bits 32]
; %define用于定义文本替换标号,类似于C语言里面常用的宏替换。
; equ用于 对标号赋值,equ可放在程序中间,而%define则只能用于程序开头。
%define ERROR_CODE nop 
%define ZERO push 0

; 引用外部函数
extern put_str
extern idt_table


section .data
; 中断处理程序中打印的字符串
intr_str db "interrupt occur!",0xa,0

; 暴露给外部的接口,没有参数
global idt_entry_table
idt_entry_table:
    ; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
    %macro VECTOR 2   
        section .text
        ; %1 表示第一个参数,直接替换
        intr%1entry:
            %2
        
            push ds
            push es
            push fs
            push gs
            pushad  ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

            push %1 ;压栈中断号,作为 idt_handle 的参数

         ; 手动模式下,需要主动的向主片和从片发送EOI信号
            mov al,0x20
            out 0xa0,al
            out 0x20,al

            call [idt_table+%1*4]
            add esp,4   ;跳过参数
            
            popad   
            pop gs
            pop fs
            pop es
            pop ds
            add esp,4 ;跳过 error_code
            
            iretd
        section .data
            dd intr%1entry
    %endmacro 
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO 
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO 
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE 
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO 
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO 
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO 
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE 
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO 
VECTOR 0x20,ZERO

4.7 list.h/list.c

#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "global.h"

#define offset( struct_type, member ) ( int )( &( ( struct_type* )0 )->member )
#define elem2entry( struct_type, struct_member_name, elem_ptr ) \
    ( struct_type* )( ( int )elem_ptr - offset( struct_type, struct_member_name ) )

/**********   定义链表结点成员结构   ***********
 *结点中不需要数据成元,只要求前驱和后继结点指针*/
struct ListElem
{
    struct ListElem* prev;  // 前躯结点
    struct ListElem* next;  // 后继结点
};

/* 链表结构,用来实现队列 */
struct List
{
    /* head是队首,是固定不变的,不是第1个元素,第1个元素为head.next */
    struct ListElem head;
    /* tail是队尾,同样是固定不变的 */
    struct ListElem tail;
};

/* 自定义函数类型function,用于在list_traversal中做回调函数 */
typedef bool( Function )( struct ListElem*, int arg );

void             list_init( struct List* );
void             list_insert_before( struct ListElem* before, struct ListElem* elem );
void             list_push( struct List* plist, struct ListElem* elem );
void             list_iterate( struct List* plist );
void             list_append( struct List* plist, struct ListElem* elem );
void             list_remove( struct ListElem* pelem );
struct ListElem* list_pop( struct List* plist );
bool             list_empty( struct List* plist );
uint32_t         list_len( struct List* plist );
struct ListElem* list_traversal( struct List* plist, Function func, int arg );
bool             elem_find( struct List* plist, struct ListElem* obj_elem );
#endif
#include "list.h"
#include "interrupt.h"

/* 初始化双向链表list */
void list_init( struct List* list )
{
    list->head.prev = NULL;
    list->head.next = &list->tail;
    list->tail.prev = &list->head;
    list->tail.next = NULL;
}

/* 把链表元素elem插入在元素before之前 */
void list_insert_before( struct ListElem* before, struct ListElem* elem )
{
    enum IntrStatus old_status = intr_disable();

    /* 将before前驱元素的后继元素更新为elem, 暂时使before脱离链表*/
    before->prev->next = elem;

    /* 更新elem自己的前驱结点为before的前驱,
     * 更新elem自己的后继结点为before, 于是before又回到链表 */
    elem->prev = before->prev;
    elem->next = before;

    /* 更新before的前驱结点为elem */
    before->prev = elem;

    intr_set_status( old_status );
}

/* 添加元素到列表队首,类似栈push操作 */
void list_push( struct List* plist, struct ListElem* elem )
{
    list_insert_before( plist->head.next, elem );  // 在队头插入elem
}

/* 追加元素到链表队尾,类似队列的先进先出操作 */
void list_append( struct List* plist, struct ListElem* elem )
{
    list_insert_before( &plist->tail, elem );  // 在队尾的前面插入
}

/* 使元素pelem脱离链表 */
void list_remove( struct ListElem* pelem )
{
    enum IntrStatus old_status = intr_disable();

    pelem->prev->next = pelem->next;
    pelem->next->prev = pelem->prev;

    intr_set_status( old_status );
}

/* 将链表第一个元素弹出并返回,类似栈的pop操作 */
struct ListElem* list_pop( struct List* plist )
{
    struct ListElem* elem = plist->head.next;
    list_remove( elem );
    return elem;
}

/* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */
bool elem_find( struct List* plist, struct ListElem* obj_elem )
{
    struct ListElem* elem = plist->head.next;
    while ( elem != &plist->tail )
    {
        if ( elem == obj_elem )
        {
            return true;
        }
        elem = elem->next;
    }
    return false;
}

/* 把列表plist中的每个元素elem和arg传给回调函数func,
 * arg给func用来判断elem是否符合条件.
 * 本函数的功能是遍历列表内所有元素,逐个判断是否有符合条件的元素。
 * 找到符合条件的元素返回元素指针,否则返回NULL. */
struct ListElem* list_traversal( struct List* plist, Function func, int arg )
{
    struct ListElem* elem = plist->head.next;
    /* 如果队列为空,就必然没有符合条件的结点,故直接返回NULL */
    if ( list_empty( plist ) )
    {
        return NULL;
    }

    while ( elem != &plist->tail )
    {
        if ( func( elem, arg ) )
        {  // func返回ture则认为该元素在回调函数中符合条件,命中,故停止继续遍历
            return elem;
        }  // 若回调函数func返回true,则继续遍历
        elem = elem->next;
    }
    return NULL;
}

/* 返回链表长度 */
uint32_t list_len( struct List* plist )
{
    struct ListElem* elem   = plist->head.next;
    uint32_t         length = 0;
    while ( elem != &plist->tail )
    {
        length++;
        elem = elem->next;
    }
    return length;
}

/* 判断链表是否为空,空时返回true,否则返回false */
bool list_empty( struct List* plist )
{  // 判断队列是否为空
    return ( plist->head.next == &plist->tail ? true : false );
}

4.8 main.c

#include "print.h"
#include "init.h"
#include "debug.h"
#include "interrupt.h"
#include "memory.h"
#include "thread.h"

// 这里一定要先声明,后面定义
// 不然会出错,我也不知道为啥,应该是因为改变了地址?
// 就是在ld中
void k_thread_a(void *);
void k_thread_b(void *);
int main(int argc, char const *argv[])
{
    set_cursor(880);
    put_char('k');
    put_char('e');
    put_char('r');
    put_char('n');
    put_char('e');
    put_char('l');
    put_char('\n');
    put_char('\r');
    put_char('1');
    put_char('2');
    put_char('\b');
    put_char('3');
    put_str("\n put_char\n");

    init_all();

    put_str("interrupt on\n");

    void *addr = get_kernel_pages(3);
    put_str("\n get_kernel_page start vaddr is ");
    put_int((uint32_t)addr);
    put_str("\n");

    // 改变执行流
    thread_start("k_thread_b", 8, k_thread_b, "argB ");
    thread_start("k_thread_a", 31, k_thread_a, "argA1 ");

    intr_enable(); // 打开中断,使时钟中断起作用

    while (1)
    {
        put_str("main ");
    }

    return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void *arg)
{
    /* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
    char *para = arg;

    while (1)
    {
        put_str(para);
    }
}

/* 在线程中运行的函数 */
void k_thread_b(void *arg)
{
    /* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
    char *para = arg;

    while (1)
    {
        put_str(para);
        // put_str("b ");
    }
}

4.8 makefile


BUILD_DIR=./build
AS=nasm
NASM_ELF=-f elf
INCLUDE=-I./lib -I ./lib/kernel -I./kernel -I./thread/ -I./device
ASINCLUDE=-I./boot/include/
CFLAGS=-m32 -fno-builtin
LDFLAGS=-Ttext 0xc0001500 -m elf_i386 -e main
CC=gcc

# 注意这里: $(BUILD_DIR)/kernel.o 一定要放在第一个上,因此,这个变量是为连接器准备的.如果不放在第一项,保证错
OBJ=$(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/idt.o $(BUILD_DIR)/debug.o \
    $(BUILD_DIR)/interrupt.o  $(BUILD_DIR)/init.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/string.o  \
    $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o $(BUILD_DIR)/switch.o \
    $(BUILD_DIR)/timer.o
    
# 最终要生成的文件
all: $(BUILD_DIR)/kernel.bin $(BUILD_DIR)/loader.bin $(BUILD_DIR)/mbr.bin

# 编译 并刻录 loader.bin
$(BUILD_DIR)/loader.bin: ./boot/loader.asm
    $(AS) -o [email protected] $^ $(ASINCLUDE)
    dd if=$(BUILD_DIR)/loader.bin of=./hd60m.img bs=512 count=4 seek=2 conv=notrunc


# 编译 并刻录 mbr.bin 
$(BUILD_DIR)/mbr.bin: ./boot/mbr.asm
    $(AS) -o [email protected] $^ $(ASINCLUDE)
    dd if=$(BUILD_DIR)/mbr.bin of=./hd60m.img bs=512 count=1 conv=notrunc

# 编译 print.asm
$(BUILD_DIR)/print.o:./lib/kernel/print.asm
    $(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE) 

# 编译 idt.asm
$(BUILD_DIR)/idt.o:./kernel/idt.asm
    $(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE) 

# 编译 interrupt.c
$(BUILD_DIR)/interrupt.o:./kernel/interrupt.c 
    $(CC) -o [email protected]  $(CFLAGS) -fno-stack-protector  -c $^ $(INCLUDE) 

# 编译 debug.c
$(BUILD_DIR)/debug.o:./kernel/debug.c
    $(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)

# 编译 init.c
$(BUILD_DIR)/init.o:./kernel/init.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 kernel/memory.c
$(BUILD_DIR)/memory.o:./kernel/memory.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 lib/kernel/string.c
$(BUILD_DIR)/string.o:./lib/kernel/string.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 lib/kernel/bitmap.c
$(BUILD_DIR)/bitmap.o:./lib/kernel/bitmap.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 lib/kernel/list.c
$(BUILD_DIR)/list.o:./lib/kernel/list.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 thread/thread.c
$(BUILD_DIR)/thread.o:./thread/thread.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 switch.asm
$(BUILD_DIR)/switch.o:./thread/switch.asm
    $(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE) 
    
# 编译 main.c
$(BUILD_DIR)/timer.o:./device/timer.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)

# 编译 main.c
$(BUILD_DIR)/kernel.o:./kernel/main.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 最终链接
$(BUILD_DIR)/kernel.bin:$(OBJ)
    $(LD) -Ttext 0xc0001500 -m elf_i386 -e main -o ./build/kernel.bin $(OBJ)
    dd if=./build/kernel.bin of=./hd60m.img bs=512 count=40 seek=9 conv=notrunc

clean:
    rm -rf ./build/*

6 运行

为了验证新任务第一次运行实际上是属于中断处理函数部分,因此我们在kernel_thread()中,加一下的任意断点

static void kernel_thread(ThreadFunc *func, void *func_arg)
{

    BREAK();
    // 在中断处理函数中关中断,然后切换完成后开中断
    intr_enable();
    BREAK();
 
    func(func_arg);
}

技术分享图片

技术分享图片

技术分享图片

技术分享图片

以上是关于第09章下 任务调度的主要内容,如果未能解决你的问题,请参考以下文章

第08章下 内存管理系统

第09章上 内核线程

STM32H7第14章 ThreadX调度锁,任务锁和中断锁(调度阀值)

STM32F429第14章 ThreadX调度锁,任务锁和中断锁(调度阀值)

STM32H7第13章 任务调度—抢占式,时间片和合作式

STM32F429第13章 任务调度—抢占式,时间片和合作式