rCore_Lab5

本章任务

本章要完成的操作系统的核心目标是: 让开发者能够控制程序的运行

伤齿龙

最聪明的恐龙...

一些基本概念

进程与任务的不同点

第三/四章提到的 任务 是这里提到的 进程 的初级阶段,任务还没进化到拥有更强大的动态变化功能:进程可以在运行的过程中,创建 子进程 、 用新的 程序 内容覆盖已有的 程序 内容。这种动态变化的功能可让程序在运行过程中动态使用更多的物理或虚拟的 资源

进程、线程与协程

进程,线程和协程是操作系统中经常出现的名词,它们都是操作系统中的抽象概念,有联系和共同的地方,但也有区别。计算机的核心是 CPU,它承担了基本上所有的计算任务;而操作系统是计算机的管理者,它可以以进程,线程和协程为基本的管理和调度单位来使用 CPU 执行具体的程序逻辑。

随着计算机的发展,对计算机系统性能的要求越来越高,而进程之间的切换开销相对较大,于是计算机科学家就提出了线程。线程是程序执行中一个单一的顺序控制流程,线程是进程的一部分,一个进程可以包含一个或多个线程。各个线程之间共享进程的地址空间,但线程要有自己独立的栈(用于函数访问,局部变量等)和独立的控制流。且线程是处理器调度和分派的基本单位。对于线程的调度和管理,可以在操作系统层面完成,也可以在用户态的线程库中完成。用户态线程也称为绿色线程(GreenThread)。如果是在用户态的线程库中完成,操作系统是“看不到”这样的线程的,也就谈不上对这样线程的管理了。

协程(Coroutines,也称纤程(Fiber)),也是程序执行中一个单一的顺序控制流程,建立在线程之上(即一个线程上可以有多个协程),但又是比线程更加轻量级的处理器调度对象。协程一般是由用户态的协程管理库来进行管理和调度,这样操作系统是看不到协程的。而且多个协程共享同一线程的栈,这样协程在时间和空间的管理开销上,相对于线程又有很大的改善。在具体实现上,协程可以在用户态运行时库这一层面通过函数调用来实现;也可在语言级支持协程,比如 Rust 借鉴自其他语言的的 asyncawait 关键字等,通过编译器和运行时库二者配合来简化程序员编程的负担并提高整体的性能。

进程模型与重要系统调用

fork系统调用

/// 功能:当前进程 fork 出来一个子进程。
/// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID 。
/// syscall ID:220
pub fn sys_fork() -> isize;

进程 A 调用 fork 系统调用之后,内核会创建一个新进程 B,这个进程 B 和调用 fork 的进程A在它们分别返回用户态那一瞬间几乎处于相同的状态:这意味着它们包含的用户态的代码段、堆栈段及其他数据段的内容完全相同,但是它们是被放在两个独立的地址空间中的。因此新进程的地址空间需要从原有进程的地址空间完整拷贝一份。两个进程通用寄存器也几乎完全相同。例如, pc 相同意味着两个进程会从同一位置的一条相同指令(我们知道其上一条指令一定是用于系统调用的 ecall 指令)开始向下执行, sp 相同则意味着两个进程的用户栈在各自的地址空间中的位置相同。其余的寄存器相同则确保了二者回到了相同的控制流状态。

但是唯有用来保存 fork 系统调用返回值的 a0 寄存器(这是 RISC-V 64 的函数调用规范规定的函数返回值所用的寄存器)的值是不同的。这区分了两个进程:原进程的返回值为它新创建进程的 PID ,而新创建进程的返回值为 0

waitpid系统调用

/// 功能:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值。
/// 参数:pid 表示要等待的子进程的进程 ID,如果为 -1 的话表示等待任意一个子进程;
/// exit_code 表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。
/// 返回值:如果要等待的子进程不存在则返回 -1;否则如果要等待的子进程均未结束则返回 -2;
/// 否则返回结束的子进程的进程 ID。
/// syscall ID:260
pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize;

一般情况下一个进程要负责通过 waitpid 系统调用来等待它 fork 出来的子进程结束并回收掉它们占据的资源,这也是父子进程间的一种同步手段。

但这并不是必须的。如果一个进程先于它的子进程结束,在它退出的时候,它的所有子进程将成为进程树的根节点——用户初始进程的子进程,同时这些子进程的父进程也会转成用户初始进程。这之后,这些子进程的资源就由用户初始进程负责回收了,这也是用户初始进程很重要的一个用途。后面我们会介绍用户初始进程是如何实现的。

exec系统调用

我们还需要引入 exec 系统调用来执行不同的可执行文件

/// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。
/// 参数:path 给出了要加载的可执行文件的名字;
/// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。
/// syscall ID:221
pub fn sys_exec(path: &str) -> isize;
// user/src/exec.rs

pub fn sys_exec(path: &str) -> isize {
    syscall(SYSCALL_EXEC, [path.as_ptr() as usize, 0, 0])
}

这样,利用 forkexec 的组合,我们很容易在一个进程内 fork 出一个子进程并执行一个特定的可执行文件。

rust知识点

fn main(){
    let x = 0xd;
    match x {
        (0xa | 0xd) => {
            println!("yeyeye~~~");
        }    
        _ =>{
            println!("nonono");
        }
    }
    println!("{:x}",0xa | 0xd);

}

这里的 0xa | 0xd 并不是与的意思,在match的意思是: 如果x = 0xa 或0xd,则 {}

一些问题

0、fork中子进程是怎么获取父进程的寄存器的?或者说trapContext?

这要从 trap_cx_ppn的获取来看:

image-20240118164428920

这里获取的trapContext的ppn其实就是父进程的TrapContext的ppn,因为目前处于父进程的cr3

image-20240118164555966

可以尝试打印这个,就会发现与猜想一致

1、父进程介绍后,os一边把父进程的所有子进程挂到INITPROC程序上,一边又把子进程clear

image-20240117120935230

那么子进程到底还存不存在?存在

2、父进程创建子进程后,页表到底一不一样?

从代码中我看不一样啊,不一样

问答题

1、如何查看Linux操作系统中的进程?

ps -aux

2、简单描述一下进程的地址空间中有哪些数据和代码

.text段中包含代码

.data段中包含已初始化的全局变量和静态变量

.rodata段中包含用于存储只读数据,如常量字符串和其他只读值。

.bss(Block Started by Symbol)段用于存储未初始化的全局变量和静态变量。

heap段保存堆

stack段保存栈

3、进程控制块保存哪些内容?

(1)、pid

(2)、trapContext与其物理页号(包括返回地址、寄存器、用户栈之类的)

(3)、base_size ,进程数据大小

(4)、进程状态

(5)、子进程、父进程

(6)、token

(7)、栈指针

4、进程上下文切换需要保存哪些内容?

(1)、一些寄存器

(2)、返回地址

(3)、堆栈指针

(4)、页全局目录

5、fork 为什么需要在父进程和子进程提供不同的返回值?

根据不同的返回值来区分是子进程还是父进程

6、fork + exec 的一个比较大的问题是 fork 之后的内存页/文件等资源完全没有使用就废弃了,针对这一点,有什么改进策略?

COW

7、其实使用了6的策略之后,fork + exec 所带来的无效资源的问题已经基本被解决了,但是近年来fork 还是在被不断的批判,那么到底是什么正在”杀死”fork?可以参考 论文

fork 和其他的操作不正交,也就是 os 每增加一个功能,都要改 fork, 这导致新功能开发困难,设计受限.有些和硬件相关的甚至根本无法支持 fork.

fork 得到的父子进程可能产生共享资源的冲突;

子进程继承父进程,如果父进程处理不当,子进程可以找到父进程的安全漏洞进而威胁父进程;

还有比如 fork 必须要虚存, SAS 无法支持等等.

8、请阅读下列代码,并分析程序的输出,假定不发生运行错误,不考虑行缓冲,不考虑中断:

int main(){
    int val = 2;

    printf("%d", 0);
    int pid = fork();
    if (pid == 0) {
        val++;
        printf("%d", val);
    } else {
        val--;
        printf("%d", val);
        wait(NULL);
    }
    val++;
    printf("%d", val);
    return 0;
}
如果 fork() 之后主程序先运行,则结果如何?如果 fork() 之后 child 先运行,则结果如何?

主进程先运行 : 01342

子进程先运行 : 03412

9、为什么子进程退出后需要父进程对它进行 wait,它才能被完全回收?

不到啊

当一个进程通过exit系统调用退出之后,它所占用的资源并不能够立即全部回收,需要由该进程的父进程通过wait收集该进程的返回状态并回收掉它所占据的全部资源,防止子进程变为僵尸进程造成内存泄漏。同时父进程通过wait可以获取子进程执行结果,判断运行是否达到预期,进行管理。

10、有哪些可能的时机导致进程切换?

(1)、时钟中断

(2)、进程被创建

(3)、进程终止执行

(4)、进程主动放弃CPU(yield / sleep)

11、请描述在本章操作系统中实现本章提出的某一种调度算法(RR调度除外)的简要实现步骤。

可降低优先级的MLFQ:将manager的进程就绪队列变为数个,初始进程进入第一队列,调度器每次选择第一队列的队首进程执行,当一个进程用完时间片而未执行完,就在将它重新添加至就绪队列时添加到下一队列,直到进程位于底部队列。

12、非抢占式的调度算法,以及抢占式的调度算法,他们的优点各是什么?

非抢占式: 进程执行连续

抢占式:任务级响应时间最优,能满足紧迫任务的需要

13、假设我们简单的将进程分为两种:前台交互(要求短时延)、后台计算(计算量大)。下列进程/或进程组分别是前台还是后台?a) make 编译 linux; b) vim 光标移动; c) firefox 下载影片; d) 某游戏处理玩家点击鼠标开枪; e) 播放交响乐歌曲; f) 转码一个电影视频。除此以外,想想你日常应用程序的运行,它们哪些是前台,哪些是后台的?

前台:b,d,e

后台:a,c,f

14、RR 算法的时间片长短对系统性能指标有什么影响?

太长的话,每个任务在时间片内完成,平均周转时间长

太短的话,进程切换频繁,进程切换上下文总开销过大

15、MLFQ 算法并不公平,恶意的用户程序可以愚弄 MLFQ 算法,大幅挤占其他进程的时间。(MLFQ 的规则:“如果一个进程,时间片用完了它还在执行用户计算,那么 MLFQ 下调它的优先级”)你能举出一个例子,使得你的用户程序能够挤占其他进程的时间吗?

MLFQ 多级反馈队列

每次连续执行只进行大半个时间片长度即通过执行一个IO操作等让出cpu,这样优先级不会下降,仍能很快得到下一次调度。

实验题

https://github.com/TL-SN/rCore/tree/lab5

对于第二个实验,我没有找到很好的测例去检验它,而且我在rust方面遇到了一些问题

linux函数

spawn

fn sys_spawn(path: *const u8) -> isize
syscall ID: 400

功能:新建子进程,使其执行目标程序。
s
说明:成功返回子进程id,否则返回 -1。

可能的错误:
无效的文件名。

进程池满/内存不足等资源错误。

TIPS:虽然测例很简单,但提醒读者 spawn 不必 像 fork 一样复制父进程的地址空间。

rust关于一些奇奇怪怪的报错

1、RefMut与借用检查器

processor.current.unwrap().inner_exclusive_access().stride;这一行报错

const BIGSTRIDE:isize = 100000;
///
pub fn run_tasks() {
    // 实验二、实现stride调度算法
    

    loop {
        let mut processor = PROCESSOR.exclusive_access();
        if let Some(task) = fetch_task() {                      // 循环从队列中取出task控制块
            let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();        // 获取 当前的TaskContext
            // access coming task TCB exclusively
            let mut task_inner = task.inner_exclusive_access();
            let next_task_cx_ptr = &task_inner.task_cx as *const TaskContext;
            task_inner.task_status = TaskStatus::Running;                                      // 取出next TaskContext,并赋status
            task_inner.stride += BIGSTRIDE/task_inner.priority;
            
            let pri1 = processor.current.unwrap().inner_exclusive_access().stride;
            println!("{:x}",pri1);


            drop(task_inner);
            // release coming task TCB manually
            processor.current = Some(task); 
            // release processor manually
            drop(processor);
            unsafe {
                __switch(idle_task_cx_ptr, next_task_cx_ptr);
            }
        }
    }
}

RefMut当您尝试将值移出可变引用(在本例中为)之外时,通常会出现错误消息“cannot move out of dereference of ” processor.current.unwrap(),但 Rust 的借用检查器会阻止这种情况以确保内存安全。

要解决此问题,您可以使用RefMut::as_mut()方法获取对current字段的可变引用processor而不移动它。以下是修改代码的方法:

loop {
    let mut processor = PROCESSOR.exclusive_access();
    if let Some(task) = fetch_task() {
        let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();
        let mut task_inner = task.inner_exclusive_access();
        let next_task_cx_ptr = &task_inner.task_cx as *const TaskContext;
        task_inner.task_status = TaskStatus::Running;
        task_inner.stride += BIGSTRIDE / task_inner.priority;

        let pri1 = processor.current.as_mut().unwrap().inner_exclusive_access().stride;
        println!("{:x}", pri1);

        drop(task_inner);
        processor.current = Some(task);
        drop(processor);
        unsafe {
            __switch(idle_task_cx_ptr, next_task_cx_ptr);
        }
    }
}

在此修改后的代码中,我们用于as_mut()获取可变引用processor.current而不移动它,然后调用unwrap()该引用来访问其内部内容。这应该可以解决该错误。

但实际上,能通过编译,但通不过运行

posted @ 2024-01-18 23:03  TLSN  阅读(67)  评论(0编辑  收藏  举报