Linux系统分析课程总结

weixin_43382743 2022-07-03 16:51:22

学习收获与感想

从4月份开课后,报名了操纵系统大赛,选题为基于rust语言来完成一系列基于RISCV指令集的操作系统,在课程实验中慢慢理解并从0到1开始实现了一些操作系统(从输出HelloWorld,到简单的批处理系统,再到带task概念的批处理系统,再到带task带虚拟内存空间的批处理系统),虽然比较遗憾没能完成预期目标(完成rCore系列学习并添加一些新的功能与优化),但还是有众多收获(操作系统开发经验,内核debug等),打算再考试结束后继续完成预期目标。

课程实验情况及对操作系统的理解

重写rCore系统目前进度总结

学习并重新实现了第0章到第4章的内容(从输出HelloWorld,到简单的批处理系统,再到带task概念的批处理系统,再到带task带虚拟内存空间的批处理系统),总结主要围绕目前最新进度的实现展开。

TCB设计

#[derive(Clone, Copy)]
pub struct Task
{
    pub ctx: TaskContext,    
    pub status : TaskStatus,
    pub satp : usize,
    pub trapframe : PhyPage,
    pub kernelstack_top : PhyPage,
}

ctx为当前任务的任务上下文,status代表了当前任务的状态,satp为当前任务对应的页表再内存中的地址,trapframe为当前任务的中断上下文,kernelstack_top为当前任务对应的内核栈在内存中的地址。

上下文设计

中断上下文

在riscv中,当中断或异常发生后,在用户模式下运行的控制流会跳转到stvec中指定的位置处开始运行,同时将权限提升为管理模式,在stvec处的代码负责保存中断上下文并为进入内核做好准备,最后进入内核。

中断上下文结构:

#[derive(Debug, Clone, Copy)]
pub struct Context
{
    pub regs : [usize; 32],
    pub sepc : usize,
    pub sstatus : Sstatus,
    pub pgt : usize,
    pub kernel_stack : usize,
    pub handler : usize,
    pub trapframe : usize, //in user addr
}

regs为32个通用寄存器,sepc是进入中断之前用户程序的地址,sstatus为中断发生时程序状态字(类似于x86中的flag)。

pgt 为该任务对应的内核页表; kernel_stack为任务对应的内核栈; handler 为内核中中断异常处理代码的入口;trapframe为中断上下文在用户空间中的地址。下面是当中断异常发生时保存上下文并进入内核的riscv汇编:

.altmacro
.macro SAVE_REG n
    sd x\n, \n*8(t6)
.endm

.macro LOAD_REG n
    ld x\n, \n*8(a0)
.endm

    .align 12
    .section .text.trampoline
    .globl trampoline
    .globl usertrap
    .globl trapret
    .globl trampoline_end

trampoline:
usertrap:
csrrw t6, sscratch, t6

.set n, 0
.rept 31
    SAVE_REG %n
    .set n, n+1
.endr

csrr t0, sepc
csrr t1, sstatus

sd t0, 32*8(t6)
sd t1, 33*8(t6) //save sepc and sstatus

ld t0, 34*8(t6) // t0 points to kernel_pagetable

ld sp, 35*8(t6) // load the kernel_stack

ld t1, 36*8(t6) // load the traphander in t1

csrr t2, sscratch
sd t2, 31*8(t6)  //save t6

csrw sscratch, t6  //sscratch points trapframe again

csrw satp, t0
sfence.vma zero, zero

jr t1

//trapret(user_trapframe, user_pagetable) a0 points to trapframe
usertrap_end:

大致思路为:在正式进入内核前,先保存31个通用寄存器,然后保存进入内核时的sepc和sstatus, 取出保存在trapframe中的kernel_stack写入sp, 取出内核中断异常处理程序地址放在t1中,取出内核页表地址并写入satp, 最后跳转到t1中的地址继续执行。

任务上下文

下面是任务上下文的定义:

#[derive(Copy, Clone, Debug)]
pub struct TaskContext
{
    pub callee_save : [usize; 12],
    pub sp : usize,
    pub ra : usize,
}

callee_save是riscv中12个被调用者保存寄存器,因为其他寄存器如果有需要被保存的话,在调用切换函数前就已经保存在栈上了。sp是当前任务的内核栈,ra是调用切换函数前的返回地址。

在当前的设计中,内核空间与用户地址空间是分离的,内核存在一张页表中,而其他用户任务各自拥有一张自己地址空间的页表,内核页表被所有的用户任务共享,每一个用户任务在正式进入内核空间前会换上内核页表。

在离开内核空间的时候,可以获得当前被选中调度的进程的上下文来获得进程的页表,并在离开内核时写入satp寄存器中。

所以任务上下文除了在保存的一些寄存器之外,还有一个cur值,这个cur值指出当前运行在这个cpu上的任务在任务表中的下标,当需要获得当前运行在本cpu上的任务相关信息的时候,通过这个值可以找到该任务。这个值保存在一个全局结构中的一个域中。


struct InnerTaskManager
{
    pub cur: usize,
    pub tasks: [Task; APP_MAX_COUNT],
    pub kernel_ctx: TaskContext,
}

调度策略

在目前的实现中,对于任务的调度,使用的是时间片轮转调度算法。在内核一开始启动的时候,会选择一个任务开始运行,在过一段时间后发生时钟中断时,会选择另外一个任务开始运行,直到所有任务都退出后,内核才退出。

下面是内核开始任务调度的代码,get_next()方法返回下一个被调度上CPU的任务的下标,这里使用一个函数进行解耦合,方便后面有需要可以置换调度算法。

  pub fn run_task(&self) -> !
    {
        loop
        {
            let next_task = self.get_next();
            if let core::option::Option::Some(next_task_index) = next_task
            {
                println!("get task {}", next_task_index);
                let mut tasks = self.tasks.access();
                let saved = &mut tasks.kernel_ctx  as *mut TaskContext;
                let loaded = &(tasks.tasks[next_task_index].ctx)  as *const TaskContext;
                drop(tasks);
                unsafe
                {
                    switch(saved, loaded);
                }
            }
            else
            {
                panic!("no more task\n");
            }
        }
    }

下面是函数的具体实现:

   fn get_next(&self) -> Option<usize>
    {
        let mut inner_tasks = self.tasks.access();
        let mut cur = inner_tasks.cur;
        for _ in 0..self.total
        {
            if let TaskStatus::Ready = inner_tasks.tasks[cur].status
            {
                inner_tasks.cur = cur;
                return Some(cur);
            }
            cur = (cur + 1) % self.total;
        }
        None
    }

 它先获得当前正在运行的任务的下标值,然后开始遍历任务表,如果找得到一个任务的状态为ready的,则返回该下标,找不到返回None。

#[no_mangle]
pub fn trap_handler()
{
    ......
    
    match scause.cause() 
    {
        ......

        Trap::Interrupt(Interrupt::SupervisorTimer) =>
        {
            set_next_timer_intr_in_ms(10);
            println!("timer interupt happen");
            suspend();
        }
        
        ......

    }
    before_trapret();
}

当定时器中断发生时,会调用trap_handler函数,在对定时器中断的处理中,先将下一次定时器的中断时间设置在10ms后,然后调用suspend()函数来暂停当前任务执行,并切换下一个任务。在suspend函数中会调用suspend_cur_task函数来完成这一操作。

    pub fn suspend_cur_task(&self)
    {
        let mut tasks = self.tasks.access();
        let cur = tasks.cur;

        tasks.tasks[cur].status = TaskStatus::Ready;
        tasks.cur = (cur + 1) % self.total;
        let saved = &mut tasks.tasks[cur].ctx as *mut TaskContext;
        let loaded = &tasks.kernel_ctx as *const TaskContext;
        drop(tasks);

        unsafe
        {
            switch(saved, loaded)
        }
    }

 它将当前任务上下文地址作为被保存上下文地址, 把内核调度控制流的上下文地址作为被调度上cpu的上下文,并调用调度函数进行上下文切换。最后内核调度控制流进入get_next中选出下一次被调度上CPU的进程。

 

虚拟内存空间

内核目前采用单一内核页表与多个用户内核页表的设计,用户程序进入内核时必须换上内核页表,离开时换上当前的用户内核页表。这样设计的一个缺点每次进入内核都需要有一次切换页表的开销,包括清空TLB和cache,开销相对较大。

用户程序的链接脚本如下:

OUTPUT_ARCH(riscv)
ENTRY(_start)

BASE_ADDRESS = 0x00010000;

SECTIONS
{
    . = BASE_ADDRESS;
    .text : {
        *(.text.entry)
        *(.text .text.*)
    }
    . = ALIGN(4K);

    .rodata : {
        *(.rodata .rodata.*)
        *(.srodata .srodata.*)
    }
    . = ALIGN(4K);

    .data : {
        *(.data .data.*)
        *(.sdata .sdata.*)
    }
    . = ALIGN(4K);

    .bss : {
        sbss = .;
        *(.bss .bss.*)
        *(.sbss .sbss.*)
        ebss = .;
    }
    . = ALIGN(4K);

    /DISCARD/ : {
        *(.eh_frame)
        *(.debug*)
    }
}

用户程序的地址从0X10000开始,依次放入代码段 只读数据段 全局数据段 和 bss段, 每个段以4KB,也就是一个物理页面大小进行对齐。

内核空间为直接映射,也就是在构造页表时,对于虚拟地址X,对应的物理地址也为X,不发生其他变化。

 

项目地址:ZYJ-33/my_os_proj: writing an os using rust (github.com)

参考资料:庖丁解牛Linux操作系统分析https://gitee.com/mengning997/linuxkernel

                  MIT 6.S081操作系统 6.S081 / Fall 2021 (mit.edu)

                  清华大学rCore系列 rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档 (rcore-os.github.io)

 

学号尾号:590

...全文
133 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧