1. 进程相关数据结构定义
首先定义进程的状态,这里参考xv6将进程定义为6个状态:
- PROC_UNUSED:进程控制块未使用处于空闲状态。
- PROC_USED:进程控制块已经被使用状态。
- PROC_SLEEPING:进程处于睡眠状态。
- PROC_RUNNABLE:进程已经就绪可以被调度执行状态。
- PROC_RUNNING:进程正在运行状态。
- PROC_ZOMBIE:进程退出状态。
接着定义进程上下文:
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; };
可以看出只是简单的将除了x0寄存器之外的通用寄存器定义为进程的上下文,最后定义进程控制块:
struct proc_block { enum proc_state state; // 进程状态 int pid; // 进程ID void *kstack; // 进程内核栈 u32 size; // 进程占用内存大小 pagetable_t pgtable; // 进程页表 struct proc_context context; // 进程上下文 char name[32]; // 进程名字 };
每个进程拥有自己的页表,这个页表会在后期开发真正的用户进程时用到。同时每个进程会有自己的id和内核栈,内核栈大小固定为4KB。
2. 进程操作接口
2.1 用于进程管理相关数据
进程控制块池,默认最多有64个进程:
static struct proc_block proc_table[CONFIG_SYS_PROCESS_NUM];
进程id分配器,这里只是简单的将int型变量每次加1,id从1开始分配:
static int nextpid = 1;
当前进程控制块指针,永远指向当前运行的进程的控制块:
static struct proc_block *proc_current;
2.2 初始化进程控制块
控制块初始化很简单,只是为每个控制块事先分配好其内核栈:
// 初始化进程控制块 void proc_init(void) { struct proc_block *p; for (p = proc_table; p < &proc_table[CONFIG_SYS_PROCESS_NUM]; p++) { p->kstack = kalloc(); if (!p->kstack) { panic("proc kstack alloc"); } } }
每个进程内核栈大小为4KB。
2.3 创建新进程
创建新进程时首先要申请一个空闲的进程控制块,然后设置这个控制块:
// 创建进程 int proc_create(void(*proc)(void)) { struct proc_block *p; p = proc_alloc(); if (!p) { pr_err("proc_create fail!\n"); return -1; } int_disable(); p->context.sp = (u64)p->kstack + PAGE_SIZE; p->context.ra = (u64)proc; p->state = PROC_RUNNABLE; int_enable(); return 0; }
RISCV用的时满减栈,所以需要需要将初始化时的栈地址加上4KB,同时设置ra返回地址为传入的函数地址。proc_alloc函数实现从进程控制块池中寻找出一个空闲的进程控制块:
// 申请空闲进程控制块 static struct proc_block* proc_alloc(void) { struct proc_block *p; // 尝试寻找空闲的进程控制块 int_disable(); for(p = proc_table; p < &proc_table[CONFIG_SYS_PROCESS_NUM]; p++) { if(p->state == PROC_UNUSED) goto found; } int_enable(); // 未找到空闲的进程控制块 return 0; // 找到空闲进程控制块后进行初步初始化 found: p->pid = nextpid++; p->state = PROC_USED; memset(&p->context, 0, sizeof(p->context)); int_enable(); return p; }
找到控制块后设置控制块的进程id和状态,最后将进程上下文都清零作为进程初始状态。
2.4 进程调度
进程调度负责选择下一个要运行的进程并切换上下文让新进程运行:
// 尝试进程调度,禁止在中断中掉用 void sched(void) { struct proc_block *p; struct proc_block *old = proc_current; int_disable(); for(p = proc_table; p < &proc_table[CONFIG_SYS_PROCESS_NUM]; p++) { // 依次寻找准备就绪的进程,并选择第一个准备好的进程 if((p->state == PROC_RUNNABLE) && (p != proc_current)) { p->state = PROC_RUNNING; proc_current = p; break; } } // 上下文切换 proc_ctx_switch(&old->context, &p->context); int_enable(); }
目前的调度算法很简单,遍历所有进程,寻找一个准备就绪并且不是当前进程的新进程,然后上下文切换执行。目前这个调度算法其实只能调度两个线程轮流执行,而且每个进程必须主动放弃cpu才能调度另一个进程执行,目前先实现简单点,后续再完善。
2.5 放弃cpu执行
一个进程主动放弃当前cpu,然后调用sched选择另一个进程调度执行:
// 主动放弃当前cpu,选择新进程运行 void yield(void) { struct proc_block *p = proc_current; int_disable(); p->state = PROC_RUNNABLE; int_enable(); sched(); }
在放弃当前cpu时需要改变当前进程的状态为就绪状态以在后续调度可以被调度执行。
2.6 进程上下文切换
进程上下文切换主要就是保存当前上下文在老的进程context中并从新进程的context中恢复上下文:
.globl proc_ctx_switch proc_ctx_switch: sd ra, 0(a0) sd sp, 8(a0) sd gp, 16(a0) sd tp, 24(a0) sd t0, 32(a0) sd t1, 40(a0) sd t2, 48(a0) sd s0, 56(a0) sd s1, 64(a0) sd a0, 72(a0) sd a1, 80(a0) sd a2, 88(a0) sd a3, 96(a0) sd a4, 104(a0) sd a5, 112(a0) sd a6, 120(a0) sd a7, 128(a0) sd s2, 136(a0) sd s3, 144(a0) sd s4, 152(a0) sd s5, 160(a0) sd s6, 168(a0) sd s7, 176(a0) sd s8, 184(a0) sd s9, 192(a0) sd s10, 200(a0) sd s11, 208(a0) sd t3, 216(a0) sd t4, 224(a0) sd t5, 232(a0) sd t6, 240(a0) ld ra, 0(a1) ld sp, 8(a1) ld gp, 16(a1) ld tp, 24(a1) ld t0, 32(a1) ld t1, 40(a1) ld t2, 48(a1) ld s0, 56(a1) ld s1, 64(a1) ld a0, 72(a1) # ld a1, 80(a1) a1的值不能被改变 ld a2, 88(a1) ld a3, 96(a1) ld a4, 104(a1) ld a5, 112(a1) ld a6, 120(a1) ld a7, 128(a1) ld s2, 136(a1) ld s3, 144(a1) ld s4, 152(a1) ld s5, 160(a1) ld s6, 168(a1) ld s7, 176(a1) ld s8, 184(a1) ld s9, 192(a1) ld s10, 200(a1) ld s11, 208(a1) ld t3, 216(a1) ld t4, 224(a1) ld t5, 232(a1) ld t6, 240(a1) ret
注意恢复的时候由于a1寄存器中是新进程的context地址,其值后面还要用不能改变,所以就不需要恢复a1寄存器的值。让执行ret后,cpu就会跳转到ra代表的地址出运行。
2.6 运行第一个进程
第一个进程目前很简单的选择进程控制块中的第一个进程直接恢复上下文运行:
// 运行内核,启动第一个进程 void os_start(void) { struct proc_block *p = &proc_table[0]; proc_current = p; proc_ctx_restore(&p->context); }
proc_ctx_restore相比proc_ctx_switch只是少了保存的部分:
.globl proc_ctx_restore proc_ctx_restore: ld ra, 0(a0) ld sp, 8(a0) ld gp, 16(a0) ld tp, 24(a0) ld t0, 32(a0) ld t1, 40(a0) ld t2, 48(a0) ld s0, 56(a0) ld s1, 64(a0) # ld a0, 72(a0) a0的值不能被改变 ld a1, 80(a0) ld a2, 88(a0) ld a3, 96(a0) ld a4, 104(a0) ld a5, 112(a0) ld a6, 120(a0) ld a7, 128(a0) ld s2, 136(a0) ld s3, 144(a0) ld s4, 152(a0) ld s5, 160(a0) ld s6, 168(a0) ld s7, 176(a0) ld s8, 184(a0) ld s9, 192(a0) ld s10, 200(a0) ld s11, 208(a0) ld t3, 216(a0) ld t4, 224(a0) ld t5, 232(a0) ld t6, 240(a0) ret
3. 测试
测试很简单,就是创建两个进程打印一条信息后放弃cpu调度另一个进程运行:
static void process_test1(void) { while(1){ pr_test("process run, pid = %d!\n", proc_current->pid); yield(); } } static void process_test2(void) { while(1){ pr_test("process run, pid = %d!\n", proc_current->pid); yield(); } } void proc_test(void) { proc_create(process_test1); proc_create(process_test2); }
运行结果如下所示:
评论