mapleFU / mwishCore

The kernel of mwish following the uCore
7 stars 0 forks source link

Process/Thread/Schedule 内容学习 #9

Open mapleFU opened 3 years ago

mapleFU commented 3 years ago

有些问题我现在感觉还是没有理的很清楚,比如内核栈的数量(2.4 之后好像是 部分在 per cpu,比如 IRQ 的;然后剩下的是每个 task_struct 一个 stack ),中断的开关,线程的栈在哪,那我们先理一下好了

mapleFU commented 3 years ago

rCore 里面线程代码如下:

pub struct Thread {
    /// 线程 ID
    pub id: ThreadID,
    /// 线程的栈
    pub stack: Range<VirtualAddress>,
    /// 所属的进程
    pub process: Arc<Process>,
    /// 用 `Mutex` 包装一些可变的变量
    pub inner: Mutex<ThreadInner>,
}

/// 线程中需要可变的部分
pub struct ThreadInner {
    /// 线程执行上下文
    ///
    /// 当且仅当线程被暂停执行时,`context` 为 `Some`
    pub context: Option<Context>,
    /// 是否进入休眠
    pub sleeping: bool,
    /// 是否已经结束
    pub dead: bool,
}

线程被停下的时候,内容会被丢到 context 里面,这个相当于 TCB 的内容了。进程这里表现为一个 MemorySet。是不是很合理!

程序抽象出了一个 Processor 来表示进程现在的运行状况,包括:

  1. 正在执行的线程
  2. 调度器
  3. 休眠的线程

然后创建了一个全局的调度器。注意这个调度器的逻辑和中断是有关的,要利用起来还需要考虑关中断。

mapleFU commented 3 years ago

这个程序,最开始的时候,是这样的,直接在内核栈上处理所有的程序,比如 Panic 什么的。但是现在肯定不行了。

原本的逻辑,处理 interrupt 的时候,在内核栈上保留之前的 sp, 然后把所有内容压栈,然后处理. 现在丢到 a0 上,然后 __interrupt__restore 的时候处理。

关于中断有:

sstatus 标志位的具体意义 spp:中断前系统处于内核态(1)还是用户态(0) sie:内核态是否允许中断。对用户态而言,无论 sie 取何值都开启中断 spie:中断前是否开中断(用户态中断时可能 sie 为 0) 硬件处理流程

在中断发生时,系统要切换到内核态。此时,切换前的状态会被保存在 spp 位中(1 表示切换前处于内核态)。同时,切换前是否开中断会被保存在 spie 位中,而 sie 位会被置 0,表示关闭中断。 在中断结束,执行 sret 指令时,会根据 spp 位的值决定 sret 执行后是处于内核态还是用户态。与此同时,spie 位的值会被写入 sie 位,而 spie 位置 1。这样,特权状态和中断状态就全部恢复了。

还比较重要的是 Thread::new 的代码:

    pub fn new(
        process: Arc<Process>,
        entry_point: usize,
        arguments: Option<&[usize]>,
    ) -> MemoryResult<Arc<Thread>> {
        // 让所属进程分配并映射一段空间,作为线程的栈
        let stack = process.alloc_page_range(STACK_SIZE, Flags::READABLE | Flags::WRITABLE)?;

        // 构建线程的 Context
        let context = Context::new(stack.end.into(), entry_point, arguments, process.is_user);

        // 打包成线程
        let thread = Arc::new(Thread {
            id: unsafe {
                THREAD_COUNTER += 1;
                THREAD_COUNTER
            },
            stack,
            process,
            inner: Mutex::new(ThreadInner {
                context: Some(context),
                sleeping: false,
                dead: false,
            }),
        });

        Ok(thread)
    }

这里索要了 STACK_SIZE 的内存来处理。

mapleFU commented 3 years ago

parkprepare 是一对对操作:

    /// 准备执行一个线程
    ///
    /// 激活对应进程的页表,并返回其 Context
    pub fn prepare(&self) -> *mut Context {
        // 激活页表
        self.process.inner().memory_set.activate();
        // 取出 Context
        let parked_frame = self.inner().context.take().unwrap();
        // 将 Context 放至内核栈顶
        unsafe { KERNEL_STACK.push_context(parked_frame) }
    }

    /// 发生时钟中断后暂停线程,保存状态
    pub fn park(&self, context: Context) {
        // 检查目前线程内的 context 应当为 None
        assert!(self.inner().context.is_none());
        // 将 Context 保存到线程中
        self.inner().context.replace(context);
    }

就是相当于拿/取 PCB 和页表相关的配置

mapleFU commented 3 years ago

这里开了一个空的 KERNEL_STACK, 每个线程 prepare 的时候,会有:

    /// 准备执行一个线程
    ///
    /// 激活对应进程的页表,并返回其 Context
    pub fn prepare(&self) -> *mut Context {
        // 激活页表
        self.process.inner().memory_set.activate();
        // 取出 Context
        let parked_frame = self.inner().context.take().unwrap();
        // 将 Context 放至内核栈顶
        unsafe { KERNEL_STACK.push_context(parked_frame) }
    }

然后最外层:

    // 获取第一个线程的 Context
    let context = PROCESSOR.lock().prepare_next_thread();
    // 启动第一个线程
    unsafe { __restore(context as usize) };

实际上,这里逻辑链路是这样的:一开始拿到第一个线程,然后拿到 prepare, 这里会调用 KERNEL_STACK.push_context .

# 离开中断
# 此时内核栈顶被推入了一个 Context,而 a0 指向它
# 接下来从 Context 中恢复所有寄存器,并将 Context 出栈(用 sscratch 记录内核栈地址)
# 最后跳转至恢复的 sepc 的位置
__restore:
    # 从 a0 中读取 sp
    # 思考:a0 是在哪里被赋值的?(有两种情况)
    mv      sp, a0
    # 恢复 CSR
    LOAD    t0, 32
    LOAD    t1, 33
    csrw    sstatus, t0
    csrw    sepc, t1
    # 将内核栈地址写入 sscratch
    addi    t0, sp, 34*8
    csrw    sscratch, t0

第一次写入的时候,内核栈 prepare 返回一个指向它的指针,然后 addi t0, sp, 34*8 完成初始化。