实验文档链接:Lab1: Threads
我的实现(更新至Lab 2):Altair-Alpha/pintos
开始之前
如文档所述,在开始编写代码之前需要先阅读掌握 GETTING STARTED 部分和附录中 Code Guide 部分的前四节(重点是 Synchronization)、4.4BSD Scheduler 部分以及 C Standards 部分内容。
博主自己整理的一些本节中你可能会用到的命令行:
# docker运行容器
docker run -it --rm --name pintos --mount type=bind,source="你的Pintos路径",target=/home/PKUOS/pintos pkuflyingpig/pintos bash
# 启动另一个命令行
docker exec -it pintos bash
# 启动gdb
cd pintos/src/threads/build && pintos-gdb kernel.o
# 编译并运行单个测试(根据需要更换测试名,注意要有.result后缀)
make && make tests/threads/priority-donate-multiple.result
# 编译并调试单个测试(根据需要更换测试名)
make && pintos --gdb -- run priority-donate-multiple
# 打印list的第一个元素的某成员,这里以打印ready_list的第一个线程的名称为例
# 使用原因是访问列表元素的list_entry是宏,无法直接打印(除非改编译选项),这里的命令是手动展开后的结果
# 可以将list_begin(&ready_list)换成任何返回值为list_elem的表达式
((struct thread *) ((uint8_t *) &(list_begin(&ready_list))->next - ((size_t)&(((struct thread*)0)->elem.next))))->name
Task 1: Alarm Clock
Exercise 1.1
- Reimplement timer_sleep(), defined in devices/timer.c.
- Although a working implementation is provided, it “busy waits,” that is, it spins in a loop checking the current
- time and calling thread_yield() until enough time has gone by. Reimplement it to avoid busy waiting.
本节实验的热身环节,要求将 timer.c
中已经用忙等待实现的线程睡眠函数 timer_sleep
改为用非忙等实现。timer_sleep
接受一个参数,为请求睡眠的 tick 数。timer.c
中其它基于物理时间的睡眠函数最终也是通过该函数实现。原代码为:
/** Sleeps for approximately TICKS timer ticks. Interrupts must
be turned on. */
void
timer_sleep (int64_t ticks)
{
int64_t start = timer_ticks ();
ASSERT (intr_get_level () == INTR_ON);
while (timer_elapsed (start) < ticks)
thread_yield ();
}
timer_ticks
函数返回自开机以来经历的 tick 数,time_elapsed
则是将当前 tick 数与参数 tick 数相减得到 tick 的时间差,原实现通过一个 while
循环询问已经历的时间直到 ticks
,如果没到达则调用 thread_yield
,该函数让出 CPU 但仍将自身设为随时可被调度的 THREAD_READY
状态,因此是 busy wait 的。
分析
实现的要点在于将不断主动询问的行为改为先将自身阻塞,然后等待一个管理者被动地唤醒。如此,就必须要有这样一个高频掌握时间更新的管理者,其是否存在呢?当然。考虑时间系统如何更新,关注到 timer.c
中有一个全局变量 ticks
记录了开机至今的 tick 数。它是如何被更新的?答案是在 timer_interrupt
函数中,该函数正是时间中断的 handler,也就是 CPU 在每个时间片都会调用该函数通知时间的更新。时间中断是一种外部中断,我们在 timer_init
初始化函数中也可以看到该 handler 函数的注册:
intr_register_ext (0x20, timer_interrupt, "8254 Timer");
于是,我们可以在 timer_sleep
中将线程和睡眠时间记录起来,然后转为阻塞状态。在被高频调用的 timer_interrupt
中,更新所有睡眠线程的时间,如果降至 0,则将其唤醒。
实现
首先从设计上,我不希望为了睡眠这样的非核心功能而向 thread
结构体添加成员,因此我选择仅在 timer.c
中添加代码。定义如下结构:
/** Record sleeping threads. */
struct sleep_thread_entry
{
struct thread *t;
int64_t sleep_ticks;
struct list_elem elem;
};
static struct list sleep_list;
- 关于
list
的实现请详细阅读源码,使用上与 C++ 的std::list
有明显区别
该结构由一个线程和其请求的睡眠时间构成,然后以一个 sleep_list
记录所有的项。
在 timer_init
中,初始化 sleep_list
:
list_init(&sleep_list);
在新版的 timer_sleep
中,构建一个记录项,放入 sleep_list
中,然后转为阻塞状态即可:
void
timer_sleep (int64_t ticks)
{
if (ticks < 0) {
return;
}
enum intr_level old_level = intr_disable();
struct sleep_thread_entry *entry = (struct sleep_thread_entry *)
malloc(sizeof(struct sleep_thread_entry));
entry->t = thread_current();
entry->sleep_ticks = ticks;
list_push_back(&sleep_list, &entry->elem);
thread_block();
intr_set_level(old_level);
}
- 当 ticks < 0 时,测试期望的行为是正常返回而非 Kernel Panic,所以不能使用
ASSERT
- 当 ticks = 0 时,参考 Linux 编程 sleep(0) 的意义,应该也执行后续代码让出 CPU,此时相当于执行了
thread_yield
thread_block
需要在关闭中断状态下被调用
编写一个新函数 sleep_tick
,遍历更新 sleep_list
,并唤醒到时间的线程:
/** Tick the sleep list. Threads whose sleep tick becomes zero
* will be unblocked. This process shouldn't be interrupted. */
static void
sleep_tick(void)
{
struct list_elem *e = list_begin(&sleep_list);
while (e != list_end(&sleep_list)) {
struct sleep_thread_entry *entry = list_entry(e, struct sleep_thread_entry, elem);
if (--(entry->sleep_ticks) <= 0) {
e = list_remove(e);
thread_unblock(entry->t);
} else {
e = list_next(e);
}
}
}
- Core Guide / Interrupt Handling 文档最后部分说明了 “The handler will run with interrupts disabled”,因此这里无需再手动关闭中断</