在开始我们操作系统的线程之旅前,建议先阅读上一章的基础工作:Rust 写操作系统之 Hello world。
本文的源码地址:
https://gitee.com/rust4fun/myos/blob/master/ch2-task/src/tasks_1.rs
源码说明:第一章写完之后就不会再动了,第二章节是拷贝第一章节的代码基础上做一些改动。每个章节的代码都单独一个目录,本章节代码也是单独目录ch2-task。
线程是什么
我开始写代码之前,我们得先了解一下线程是什么。参考网上的答案:线程(Thread)是计算机中执行的最小单位,它是进程(Process)内的一个执行流程。线程与进程类似,但是线程之间共享相同的地址空间和其他资源,因此它们之间的切换比进程更高效。线程可以被看作是进程内的一个独立的执行单元,它可以并发执行,与其他线程共享同一进程的资源。在操作系统中,线程由操作系统内核进行管理和调度。线程可以执行任何计算机程序中的任务,并且可以与其他线程同步,共享数据等。
从上面线程定义可以看出,如果把我们的操作系统 myos 当作一个进程的话,线程间的资源又是共享的,那线程又跟函数有什么区别呢。话不多说,我们来动手试试看吧。
函数 vs 线程
首先在第一章代码的基础上,新建一个 src/task.rs 文件并定义几个 task 函数。代码如下:
/*********************************************
* 线程数据结构,保存线程的运行状态
* 当前先定义个函数指针,表示这个线程需要执行的任务
**********************************************/
struct TaskControlBlock {
pub func : fn(),
}
/* 定义线程创建方法 */
impl TaskControlBlock {
pub fn new(func: fn()) -> Self {
Self {
func: func,
}
}
}
/*********************************************
* 线程管理器,用于调度线程
* 备注:
* 由于我们还没有堆内存,只能用栈内存和全局变量
* 所以这里不能使用 Vec 数组,只能写死管理 5 个线程
**********************************************/
struct TaskManager {
tasks_context : [TaskControlBlock; 5],
}
/* 定义线程管理器创建方法 */
impl TaskManager {
pub fn new() -> Self {
Self {
tasks_context: [
TaskControlBlock::new(task1),
TaskControlBlock::new(task2),
TaskControlBlock::new(task3),
TaskControlBlock::new(task4),
TaskControlBlock::new(task5),
],
}
}
}
/*********************************************
* 线程任务函数
* 每个线程只打印一下信息即可
**********************************************/
fn task1() {
println!("this is task1");
}
/* 线程2 */
fn task2() {
println!("this is task2");
}
/* 线程3 */
fn task3() {
println!("this is task3");
}
/* 线程4 */
fn task4() {
println!("this is task4");
}
/* 线程5 */
fn task5() {
println!("this is task5");
}
/*********************************************
* 进入线程调度管理
**********************************************/
pub fn task_run() {
/* 创建线程管理器 */
let mut tm = TaskManager::new();
/* 每个线程轮流执行过去 */
for mut task in tm.tasks_context {
(task.func)();
}
}
上面代码比较简单,注释都讲的比较清楚,我们就不一一讲解。核心思想是定义个线程结构体和线程管理器。由线程管理器负责调度线程运行(顺序执行一次)。在 src/main.rs 中加载并调用试试看效果吧。
mod tasks;
#[no_mangle]
unsafe extern "C" fn rust_entry(_cpu_id: usize, _dtb: usize) {
clear_bss();
println!("+++++++++++++++++++++++++++++++++++++++++++++++++\n\n");
println!(" Hello myos !!!\n\n");
println!("+++++++++++++++++++++++++++++++++++++++++++++++++\n\n");
tasks::task_run();
sbi::shutdown(false);
}
执行 run.sh 测试结果:每个函数都执行一遍并打印正确的结果,但是结果好像又不是我们想要的。问题在于我们的函数是运行完立即退出了,而线程是可以和程序生命周期一样,长时间运行并不退出的。如果每个 task 都加上 loop{} 之后,就会发现卡在第一个线程而不会执行其他线程了。
+++++++++++++++++++++++++++++++++++++++++++++++++
Hello myos !!!
+++++++++++++++++++++++++++++++++++++++++++++++++
this is task1
this is task2
this is task3
this is task4
this is task5
线程到底是什么
这里我们得好好思考一下原来的问题,线程到底是什么?根据定义线程间是共享资源的,函数间在进程空间中也是共享资源的,函数和线程的区别在哪里?从上面测试结果可以看到,函数是需要退出的,不然下个函数就无法运行。而我们的线程是可以永久执行的,任意线程不退出的时候都不会影响其他线程运行,而函数是不具备这种特性的。这里,我们终于找到线程和函数的差异了,需要怎么做才能达到以上目的呢。
为了搞定线程,我们必须学习一个新的概念上下文切换(现场保存)。CPU 在运行的时候可能会随时切换线程,在线程切换过程中为了保存当前线程的执行状态,需要将当前 CPU 的寄存器内容和其他相关状态保存到内存中,以便之后能够恢复到原来的执行状态,这个过程也称为上下文切换或者线程保存。
在了解了上下文切换之后,我们就知道了线程和函数的差异在于,线程需要一段内存可以保存当前执行状态,而函数是没有的,这就是实现线程的关键点。当前的执行状态主要包含以下几个:
- 指令地址(PC)
- 堆栈地址(SP)
- CPU 各个寄存器信息
到此为止,我们已经找到关键所在,下一节我们就照着这个思虑开干(写代码)。