SimpleOS开发(8)改进进程调度

gewenbin
gewenbin
gewenbin
188
文章
15
评论
2022年6月26日14:06:31 评论 533

1. 系统接口名称修改

为了方便使用系统接口以及辨识系统接口,需要将系统接口的命名进行规范。这里的系统接口是指通用功能接口,比如开关中断、申请释放内存等等,simpleos采用简单的"os_"前缀来命名系统接口,如下所示:

void os_clean_dcache_all(void);
void os_clean_dcache_range_pa(unsigned long start, unsigned long end);
void os_clean_dcache_range(unsigned long start, unsigned long end);
void os_invalidate_dcache_all(void);
void os_invalidate_dcache_range_pa(unsigned long start, unsigned long end);
void os_invalidate_dcache_range(unsigned long start, unsigned long end);
void os_flush_dcache_all(void);
void os_flush_dcache_range_pa(unsigned long start, unsigned long end);
void os_flush_dcache_range(unsigned long start, unsigned long end);
void os_dcache_enable(void);
void os_dcache_disable(void);
int  os_dcache_status(void);

将系统的其他功能模块提供的接口都如此修改。

2. 进程调度完善

上一节实现了进程调度和切换,但是只是个雏形,比如只能切换两个进程,进程切换只是通过"ret"函数返回来跳转到新地址执行,这在初期测试时比较有用,但是后期有U模式的进程,就不能通过"ret"来跳转了,所以需要对进程调度和切换机制进一步完善。

2.1 修改进程控制块

由于后面需要遍历进程控制块,所以需要在控制块中添加一个指针成员用来指向下一个进程控制块,另外添加进程占用cpu的时间片成员,用于后续配合tick中断调度。

struct proc_block {
    void *kstack;               // 进程内核栈

    struct proc_block *next;    // 下一个进程控制块地址
    enum  proc_state state;     // 进程状态
    int   pid;                  // 进程ID

    u32   size;                 // 进程占用内存大小
    u32   tick;                 // 进程时间片

    pagetable_t pgtable;         // 进程页表
    char name[32];               // 进程名字
};

在初始化的时候将这些进程控制块组成单向链表:

// 初始化进程控制块
void os_proc_init(void)
{
    int i = 0;
    struct proc_block *p, *next;
  
    for (p = proc_table, next = p + 1; p < &proc_table[CONFIG_SYS_PROCESS_NUM]; i++, p++, next++) {
        // 申请内核栈空间
        p->kstack = os_kalloc();
        if (!p->kstack) {
         panic("proc kstack alloc");
        }

        // 初始化单向循环链表
        if (p != &proc_table[CONFIG_SYS_PROCESS_NUM - 1]) {
            p->next = next;
        } else {
            p->next = proc_table;
        }
    }
}

2.2 修改进程调度算法

新的进程调度以tick作为判断条件,如果当前进程的时间片不为0则继续运行当前进程,如果当前进程时间片用完则选择下一个准备就绪并且时间片不为0的进程运行:

// 寻找新进程运行,必须在关中断环境下调用
static struct proc_block *proc_find_new(void)
{
    struct proc_block *new = proc_current->next;
    struct proc_block *old = proc_current;

    if (old->tick) { 
        // 当前进程时间片未用完,则继续运行
        new = old;
    } else {
        // 当前进程时间片未用完,寻找新的准备就绪的进程
        old->tick  = PROC_TICK_DEFAULT;
        old->state = PROC_RUNNABLE;

        for(; new != old; new = new->next) {
            if(new->state == PROC_RUNNABLE) {
                new->state = PROC_RUNNING;
                break;
            }
        }
    }

    return new;
}

如果当前进程时间用完会再次初始化时间片为默认值并且将进程状态修改为RUNABBLE,以让进程后续依然有机会被调度运行。如果调度的新进程不是当前进程的话需要执行进程上下文切换:

// 尝试进程调度,禁止在中断中掉用
void os_sched(void)
{
    unsigned long flag;

    flag = os_int_disable();
    proc_ready = proc_find_new();
    // 新进程不是当前进程就执行上下文切换
    if (proc_ready != proc_current) {
        // 每次执行完 sret 后, SPP 位被自动清除,这里需要重新设置
        // SPP 设置为1,这样sret执行后处理器还是处于 S 模式
        csr_set(sstatus, MSTATUS_SPP);
        os_proc_ctx_switch();
    }
    os_int_enable(flag);
}

注意这里在进程切换之前将sstatus中的SPP位置1了,正常该位是处理器在发生异常时硬件自动将当前的处理器模式记录在该位。但是目前还未支持U模式进程,进程都还运行在S模式,在进程切换后还需要处于S模式,所以需要手动将该位置1表示进程切换后处理器还是处于S模式。后续在支持U模式进程后就不需要手动去设置了,目前为了测试必须要这么设置。

2.3 修改进程切换机制

进程切换主要就是保存上下文和恢复上下文。要保存哪些上下文是有讲究的,保存上下文就是为了让进程在下次被调度时能够再次恢复之前的寄存器状态执行,进程的上下文构建主要在三个时间点:

  • 创建进程时。创建进程时需要提前在进程内核栈中准备好初始化好的上下文,这样进程再被第一次调度运行时可以正常执行。
  • 进程上下文中调度别的进程运行时。在非中断调度时,某个进程可能由于阻塞或者主动放弃cpu需要调度新进程执行,这时需要将当前的进程上下文保存在内核栈中。
  • 发生中断时。中断的时候需要将当前进程上下文保存在内核栈中,中断中可能会调度新进程运行。

这三种情况下进程保存的上下文需要是一致的,这样在恢复时不需要做特殊处理。由于切换是该为"sret"执行实现,所以需要在上下文中添加一个成员来保存"sepc"的值:

struct proc_context {
    u64 ra; // 返回地址
    u64 sp; // 栈地址
    u64 gp;
    u64 tp;
    u64 t0;
    u64 t1;
    u64 t2;
    u64 s0;
    u64 s1;
    u64 a0;
    u64 a1;
    u64 a2;
    u64 a3;
    u64 a4;
    u64 a5;
    u64 a6;
    u64 a7;
    u64 s2;
    u64 s3;
    u64 s4;
    u64 s5;
    u64 s6;
    u64 s7;
    u64 s8;
    u64 s9;
    u64 s10;
    u64 s11;
    u64 t3;
    u64 t4;
    u64 t5;
    u64 t6;
    u64 sepc;
};

在创建进程时,主要就是初始化上下文中的sp和sepc值:

// 创建进程
int os_proc_create(void(*proc)(void))
{
    struct proc_block *p;
    struct proc_context *ctx;
    unsigned long flag;

    p = proc_alloc();
    if (!p) {
        pr_err("proc_create fail!\n");
        return -1;
    }

    flag = os_int_disable();
    // 构造进程上下文
    ctx = (struct proc_context *)(((u64)p->kstack + PAGE_SIZE) - sizeof(struct proc_context));
    ctx->sp   = (u64)ctx;
    ctx->sepc = (u64)proc;
    p->state  = PROC_RUNNABLE;
    p->kstack = (void *)ctx->sp;
    os_int_enable(flag);

    return 0;
}

2.3.1 在中断上下文中切换

系统在两个地方需要调用在中断上下文切换的接口:

  • 运行系统第一个进程
  • 真的是在中断中需要切换

这两个时间点的共同点就是切换时不需要保存当前进程的上下文,因为运行第一个进程时上席文是在创建进程时准备好的,中断中切换时原有的进程上下文已经在中断入口处理时保存了。此接口逻辑比较简单,大致为:

  • 修改当前进程为新进程控制块地址
  • 获取新进程内核栈地址,其中保存了进程被中断时的上下文
  • 恢复中断返回地址
  • 恢复新进程上下文
# 进程上下文切换,中断中使用,中断需要关闭状态
# void os_proc_int_ctx_sw(void);
# 从栈中恢复寄存器值
.globl os_proc_int_ctx_sw
os_proc_int_ctx_sw:
    # 修改当前进程为新进程控制块地址
    la a0, proc_current  # a0 = &proc_current
    la a1, proc_ready    # a1 = &proc_ready
    ld a2, 0(a1)         # a2 = proc_ready
    sd a2, 0(a0)         # proc_current = a2

    # 获取新进程内核栈地址,其中保存了进程被中断时的上下文
    ld a1, 0(a0)        # a1 = proc_current
    ld sp, 0(a1)        # sp = proc_current->kstack;

    # 恢复中断返回地址
    ld a0, 248(sp)
    csrw sepc, a0

    # 恢复新进程上下文
    ld ra, 0(sp)
    ld sp, 8(sp)
    ld gp, 16(sp)
    ld tp, 24(sp)
    ld t0, 32(sp)
    ld t1, 40(sp)
    ld t2, 48(sp)
    ld s0, 56(sp)
    ld s1, 64(sp)
    ld a0, 72(sp)
    ld a1, 80(sp)
    ld a2, 88(sp)
    ld a3, 96(sp)
    ld a4, 104(sp)
    ld a5, 112(sp)
    ld a6, 120(sp)
    ld a7, 128(sp)
    ld s2, 136(sp)
    ld s3, 144(sp)
    ld s4, 152(sp)
    ld s5, 160(sp)
    ld s6, 168(sp)
    ld s7, 176(sp)
    ld s8, 184(sp)
    ld s9, 192(sp)
    ld s10, 200(sp)
    ld s11, 208(sp)
    ld t3, 216(sp)
    ld t4, 224(sp)
    ld t5, 232(sp)
    ld t6, 240(sp)
    addi sp, sp, 256

    sret

2.3.2 在进程上下文中切换

在进程上下文中切换时大致分为两个部分:保存和恢复。恢复部分和在中断上下文中切换时处理是一样的,保存部分主要分为:

  • 保存通用寄存器
  • 保存返回地址到栈中sepc位置,由于这时并不是在中断中,所以栈中sepc位置需要保存的是ra寄存器的值,ra代表的是函数调用后的返回地址
  • 保存新栈地址到当前进程控制块的kstack中
os_proc_ctx_switch:
    # 保存当前进程上下文
    addi sp, sp, -256
    sd ra, 0(sp)
    sd sp, 8(sp)
    sd gp, 16(sp)
    sd tp, 24(sp)
    sd t0, 32(sp)
    sd t1, 40(sp)
    sd t2, 48(sp)
    sd s0, 56(sp)
    sd s1, 64(sp)
    sd a0, 72(sp)
    sd a1, 80(sp)
    sd a2, 88(sp)
    sd a3, 96(sp)
    sd a4, 104(sp)
    sd a5, 112(sp)
    sd a6, 120(sp)
    sd a7, 128(sp)
    sd s2, 136(sp)
    sd s3, 144(sp)
    sd s4, 152(sp)
    sd s5, 160(sp)
    sd s6, 168(sp)
    sd s7, 176(sp)
    sd s8, 184(sp)
    sd s9, 192(sp)
    sd s10, 200(sp)
    sd s11, 208(sp)
    sd t3, 216(sp)
    sd t4, 224(sp)
    sd t5, 232(sp)
    sd t6, 240(sp)

    # 保存返回地址
    sd ra, 248(sp)

    # 记录当前进程内核栈地址到进程的kstack成员中
    la a0, proc_current  # a0 = &proc_current
    ld a1, 0(a0)         # a1 = proc_current
    sd sp, 0(a1)         # proc_current->kstack = sp;

    # 修改当前进程为新进程控制块地址
    la a0, proc_current  # a0 = &proc_current
    la a1, proc_ready    # a1 = &proc_ready
    ld a2, 0(a1)         # a2 = proc_ready
    sd a2, 0(a0)         # proc_current = a2

    # 获取新进程内核栈地址,其中保存了进程的上下文
    ld a1, 0(a0)        # a1 = proc_current
    ld sp, 0(a1)        # sp = proc_current->kstack;

    # 恢复返回地址
    ld a0, 248(sp)
    csrw sepc, a0

    # 恢复新进程上下文
    ld ra, 0(sp)
    ld sp, 8(sp)
    ld gp, 16(sp)
    ld tp, 24(sp)
    ld t0, 32(sp)
    ld t1, 40(sp)
    ld t2, 48(sp)
    ld s0, 56(sp)
    ld s1, 64(sp)
    ld a0, 72(sp)
    ld a1, 80(sp)
    ld a2, 88(sp)
    ld a3, 96(sp)
    ld a4, 104(sp)
    ld a5, 112(sp)
    ld a6, 120(sp)
    ld a7, 128(sp)
    ld s2, 136(sp)
    ld s3, 144(sp)
    ld s4, 152(sp)
    ld s5, 160(sp)
    ld s6, 168(sp)
    ld s7, 176(sp)
    ld s8, 184(sp)
    ld s9, 192(sp)
    ld s10, 200(sp)
    ld s11, 208(sp)
    ld t3, 216(sp)
    ld t4, 224(sp)
    ld t5, 232(sp)
    ld t6, 240(sp)
    addi sp, sp, 256
    
    sret

3. 测试

系统启动中运行第一个进程其实就是调用从中断中恢复上下文的接口来执行的:

// 运行内核,启动第一个进程
void os_start(void)
{
    struct proc_block *p = &proc_table[0];

    proc_ready = p;
    proc_ready->state = PROC_RUNNING;

    // SPP 设置为1,这样sret执行后处理器还是处于 S 模式
    csr_set(sstatus, MSTATUS_SPP);

    os_proc_int_ctx_sw();
}

创建三个进程,每个进程打印进程id后主动放弃cpu调度下一个进程运行:

static void process_test(void)
{
    while(1){
        pr_test("process run, pid = %d!\n", proc_current->pid);
        // 当未使用tick中断时,可以打开下面代码单独测试调度器
        os_yield();
    }
}

void proc_test(void)
{
    os_proc_create(process_test);
    os_proc_create(process_test);
    os_proc_create(process_test);
}

os_yield实现比较简单,将当前进程的时间片设置为0然后尝试调度:

// 主动放弃当前cpu,选择新进程运行
void os_yield(void)
{
    struct proc_block *p = proc_current;
    unsigned long flag;
    
    flag = os_int_disable();
    p->tick  = 0;
    os_int_enable(flag);

    os_sched();
}

一切正常的话可以在串口终端上看到3个进程轮流打印进程id:

SimpleOS开发(8)改进进程调度

gewenbin
  • 本文由 发表于 2022年6月26日14:06:31
  • 转载请务必保留本文链接:http://www.databusworld.cn/10797.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: