一 开篇
这篇博客开始介绍pintos project的第一个任务:
- Project 1 Threads
操作系统这门课对我个人而言算是比较有难度很有挑战的一门课,但是我非常的喜欢,关于一些算法性质的东西,在课上的时候可能不是很了解,但是通过pintos这个实验,很多东西自然而然的就明白了。
实验一从一开始我参考了这篇博客 Pintos-斯坦福大学操作系统Project详解-Project1 原博客的博主对于实验一讲解的非常细致,对于一些函数基本上也是一个个的讲解,所以如果觉得我这篇博客写的不是很好的朋友,请移步原博,希望你能有所收获。
之所以在原博客已经存在的情况下再写一篇这样的博客,是因为我觉得,在同一个实验中每个人的收获是不同的,比如一些地方的修改可能使你豁然开朗,有些函数该如何调用,有些变量如何使用,很多程度上让我觉得,这个实验让我学到了很多的东西。
OK,闲话不多说,开始任务。
二 任务分解
Project thread 的主要实验目标是使我们make check的时候,27个test全部pass。在不进行任何修改的情况下,make check 得到的结果是这样的:
pintos project 1 thread 可具体的划分成三个子任务
- Mission 1 重新实现 timer_sleep()函数。
- Mission 2 实现优先级调度
- Mission 3 实现多级反馈调度
Mission 1:
重新实现timer_sleep()函数
pintos系统使用的是busy waiting的方式实现,即: 线程在不停的循环,寻求进入CPU的机会,直至时间片耗尽,进程被饿死。我们需要做的就是更改timer_sleep()的实现。
timer_sleep()函数在src/devices/timer.c中。
我们可以看到 int64_t start = timer_ticks(); 这一行,调用了timer_ticks()函数,让我们找到这个函数:
在这个函数里面使用了一个枚举类型 intr_lever,让我们找到这个枚举值。在interrupt.h中,我们可以看到:
很明显的我们可以知道,这个枚举值是一个控制中断的值,当INTR_OFF的时候,禁止中断,当INTR_ON的时候,允许中断。
在timer_ticks()中我们看到了intr_disable()函数,继续找到intr_disable(),在interrupt.c中:
通过注释我们可以得知,这个函数的作用是,在调用的时候,禁止中断并且放回上一个中断状态。
而在这里我们看到了一个intr_get_level()函数,虽然通过名字我们基本上已经判断出来它的作用,但是我们还是要看一下实现:
intr_get_level()的作用是: 返回当前中断的状态。(具体的实现使用了汇编,然而我并没有学过汇编,因此放在一边,我们只需要明白它的作用就好,具体的汇编实现我们并不需要去了解。如果有兴趣了解的,请寻找相关资料。)
到这里为止我们分析完了timer_sleep()函数,让我们来整理一下。
首先,intr_get_level()返回了intr_level的值;
然后,intr_disable()获取了当前的中断状态给变量old_level,并将当前的中断状态设置为OFF 即不能中断,再返回操作之前的中断状态,即old_level。
继续分析timer_ticks(), 我们可以看到,用一个变量t获取了全局变量ticks的值,然后返回这个变量t,中间调用了一个intr_set_level()函数,继续分析这个函数
函数的return语句用了一个三元运算符, return level == INTR_ON ? intr_enable () : intr_disable ();
作用是:如果level之前允许中断,那么设置成enable, 否则就设置成disable。而这里的intr_enable()函数和intr_disable()函数,则可以继续查看下去:
好的,到这里为止我们算是分析完了timer_ticks() 函数,函数的核心作用其实就一个:
让t获取ticks的当前值。
而之所以先要用old_level来保存当前中断状态,最后intr_set_level(old_level)返回到之前的状态,
是为了让 t = ticks这个过程不被打断。
都分析到这里了,我们还没看全局变量ticks到底是什么,搜索一下ticks的声明:
/* Number of timer ticks since OS booted. */ static int64_t ticks;
根据注释我们知道,ticks是一个计数器,记录了从pintos启动开始,一共进行了多少个时间单位。
到这里我们再来看看timer_sleep()函数,
while (timer_elapsed (start) < ticks) thread_yield();
找到timer_elapsed()函数:
/* Returns the number of timer ticks elapsed since THEN, which should be a value once returned by timer_ticks(). */ int64_t timer_elapsed (int64_t then) { return timer_ticks () - then; }
这个函数返回了一个当前时间距离then的时间间隔,现在我们明白那句while是什么意思了:
在一个时间间隔里,不停的执行thread_yield(),直到当前这个时间片被耗尽。
那我们现在只需要分析thread_yield()就可以了,继续看代码:
/* Yields the CPU. The current thread is not put to sleep and may be scheduled again immediately at the scheduler's whim. */ void thread_yield (void) { struct thread *cur = thread_current (); enum intr_level old_level; ASSERT (!intr_context ()); old_level = intr_disable (); if (cur != idle_thread) list_push_back (&ready_list, &cur->elem); cur->status = THREAD_READY; schedule (); intr_set_level (old_level); }
/* Returns the running thread. This is running_thread() plus a couple of sanity checks. See the big comment at the top of thread.h for details. */ struct thread * thread_current (void) { struct thread *t = running_thread (); /* Make sure T is really a thread. If either of these assertions fire, then your thread may have overflowed its stack. Each thread has less than 4 kB of stack, so a few big automatic arrays or moderate recursion can cause stack overflow. */ ASSERT (is_thread (t)); ASSERT (t->status == THREAD_RUNNING); return t; }
/* Returns true if T appears to point to a valid thread. */ static bool is_thread (struct thread *t) { return t != NULL && t->magic == THREAD_MAGIC; }
从上面几个函数和注释,我们可以得到thread_yield()的作用:让当前的进程放弃CPU,然后进入ready队列继续等着下一次的执行,如果ready队列是空的就让它继续执行。
分析了上面这么多,我们可以得到一个结论:timer_sleep()函数的作用就是,在一个ticks的周期内,如果线程处于running,就把它扔到ready队列去。
这么做的缺点是很明显的,线程在CPU的running队列和ready队列中不断切换,耗费了大量的资源,我们要做的第一件事情,就是要改善这种实现方式。
现在我们看到的pintos的实现方式里,thread的状态一直在running和ready队列中,我们现在来思考一种实现方式:
调用timer_sleep的时候直接把线程阻塞掉,然后给线程结构体加一个成员ticks_blocked来记录这个线程被sleep了多少时间, 然后利用操作系统自身的时钟中断(每个tick会执行一次)加入对线程状态的检测, 每次检测将ticks_blocked减1, 如果减到0就唤醒这个线程。
这样一来,在线程被阻塞并且ticks_blocked的值大于0的时候,线程不再能够切换到running,即进入真正的sleep中,直至被系统时钟唤醒。
具体的实现见下一篇博客。