这里主要讲解思路,代码可能展示得不会很详细
完整代码在Github
<0x01> Task 1: Alarm Clock
任务摘要
这个任务要求重新实现./devices/timer.c的timer_sleep()函数
原本的timer_sleep()实现是基于无限循环直到到达等待时间的忙等待
我们需要避免这个忙等待
怎么实现
参考5状态模型,线程运行部分主要有三个状态
分别是运行态、准备态和阻塞态
原本的忙实现仅在运行态和准备态之间切换
所以这里我们要做的就是把阻塞态用上
所以可以单独开一个线程队列,用来存放所有休眠的线程
然后在每次时钟中断时检查休眠线程队列,把到时间的线程放到准备队列中即可
代码实现
首先先看下原始代码
void timer_sleep(int64_t ticks)
{
int64_t start = timer_ticks();
enum intr_level old_level;
ASSERT(intr_get_level() == INTR_ON);
while (timer_elapsed(start) < ticks)
thread_yield();
}
可以看到就是记录了开始的时间刻,然后无限循环
无限循环就是我们要删除的
添加休眠线程队列
在./threads/thread.c的最上面,有定义ready_list的部分
按这个格式再定义一个sleep_list作为休眠线程队列
static struct list sleep_list;
定义完后往下找,会找到一个thread_init函数,这里初始化了ready_list
我们也跟着初始化一下
void thread_init(void)
{
ASSERT(intr_get_level() == INTR_OFF);
lock_init(&tid_lock);
list_init(&ready_list);
list_init(&all_list);
// lab1 初始化休眠线程队列
list_init(&sleep_list);
/* Set up a thread structure for the running thread. */
initial_thread = running_thread();
init_thread(initial_thread, "main", PRI_DEFAULT);
initial_thread->status = THREAD_RUNNING;
initial_thread->tid = allocate_tid();
}
给线程加上苏醒时间信息
在tcb中加上线程的休眠时间信息,thread.h中添加
struct thread
{
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
enum thread_status status; /* Thread state. */
char name[16]; /* Name (for debugging purposes). */
uint8_t *stack; /* Saved stack pointer. */
int priority; /* Priority. */
struct list_elem allelem; /* List element for all threads list. */
int64_t wakeup_ticks; // lab1 添加苏醒时间刻
/* Shared between thread.c and synch.c. */
struct list_elem elem; /* List element. */
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint32_t *pagedir; /* Page directory. */
#endif
/* Owned by thread.c. */
unsigned magic; /* Detects stack overflow. */
};
这个信息用来给内核检查是否到达对应的时间,然后做相应的操作
添加线程休眠逻辑
现在有休眠队列了,在线程需要休眠的时候将其添加到这个队列中
// 用来比较线程等待至目标时间刻的函数
bool thread_less_wakeup_tick(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED)
{
struct thread *p_thread_a = list_entry(a, struct thread, elem);
struct thread *p_thread_b = list_entry(b, struct thread, elem);
return p_thread_a->wakeup_ticks < p_thread_b->wakeup_ticks;
}
// 这个记得在thread.h中声明,要在timer.c中用的
void thread_sleep(int64_t target_ticks)
{
// 获取当前线程
struct thread *cur = thread_current();;
// 给当前线程添加苏醒时间刻
cur->wakeup_ticks = target_ticks;
// 按苏醒时间刻从小到大插入休眠队列
list_insert_ordered(&sleep_list, &cur->elem, thread_less_wakeup_tick, NULL);
// 当前线程进入阻塞态
thread_block();
}
然后在timer_sleep中调用即可
void timer_sleep(int64_t ticks)
{
enum intr_level old_level;
ASSERT(intr_get_level() == INTR_ON);
old_level = intr_disable();
thread_sleep(timer_ticks() + ticks);
intr_set_level(old_level);
}
除了thread_sleep的代码跟中断处理有关系,我还没摸透
添加线程苏醒逻辑
现在线程会休眠了,但线程自己不会苏醒,需要内核操作
既然跟时间相关,那么就应该在时钟中断里面做点什么
也就是./devices/timer.c的timer_interrupt函数
static void timer_interrupt(struct intr_frame *args UNUSED)
{
ticks++;
thread_tick();
// lab1 将到达休眠时间将线程移出休眠队列
thread_wakeup();
}
这里的thread_wakeup是需要在./threads/thread.c中实现的函数
// 记得添加声明到thread.h中
void thread_wakeup(void)
{
struct list_elem *p_elem;
struct thread *p_thread;
while (!list_empty(&sleep_list))
{
p_elem = list_front(&sleep_list);
p_thread = list_entry(pe, struct thread, elem);
// 休眠队列本身就是从小到大排序的,可以这样提前退出
if (p_thread->wakeup_ticks > timer_ticks())
{
break;
}
// 移除出休眠队列
list_remove(p_elem);
// 让pt指向的线程退出阻塞态进入准备态
thread_unblock(p_thread);
}
}
运行结果
运行测试alarm-multiple
(alarm-multiple) begin
(alarm-multiple) Creating 5 threads to sleep 7 times each.
(alarm-multiple) Thread 0 sleeps 10 ticks each time,
(alarm-multiple) thread 1 sleeps 20 ticks each time, and so on.
(alarm-multiple) If successful, product of iteration count and
(alarm-multiple) sleep duration will appear in nondescending order.
(alarm-multiple) thread 0: duration=10, iteration=1, product=10
(alarm-multiple) thread 1: duration=20, iteration=1, product=20
(alarm-multiple) thread 0: duration=10, iteration=2, product=20
(alarm-multiple) thread 2: duration=30, iteration=1, product=30
(alarm-multiple) thread 0: duration=10, iteration=3, product=30
(alarm-multiple) thread 3: duration=40, iteration=1, product=40
(alarm-multiple) thread 1: duration=20, iteration=2, product=40
(alarm-multiple) thread 0: duration=10, iteration=4, product=40
(alarm-multiple) thread 4: duration=50, iteration=1, product=50
(alarm-multiple) thread 0: duration=10, iteration=5, product=50
(alarm-multiple) thread 2: duration=30, iteration=2, product=60
(alarm-multiple) thread 1: duration=20, iteration=3, product=60
(alarm-multiple) thread 0: duration=10, iteration=6, product=60
(alarm-multiple) thread 0: duration=10, iteration=7, product=70
(alarm-multiple) thread 3: duration=40, iteration=2, product=80
(alarm-multiple) thread 1: duration=20, iteration=4, product=80
(alarm-multiple) thread 2: duration=30, iteration=3, product=90
(alarm-multiple) thread 4: duration=50, iteration=2, product=100
(alarm-multiple) thread 1: duration=20, iteration=5, product=100
(alarm-multiple) thread 3: duration=40, iteration=3, product=120
(alarm-multiple) thread 2: duration=30, iteration=4, product=120
(alarm-multiple) thread 1: duration=20, iteration=6, product=120
(alarm-multiple) thread 1: duration=20, iteration=7, product=140
(alarm-multiple) thread 4: duration=50, iteration=3, product=150
(alarm-multiple) thread 2: duration=30, iteration=5, product=150
(alarm-multiple) thread 3: duration=40, iteration=4, product=160
(alarm-multiple) thread 2: duration=30, iteration=6, product=180
(alarm-multiple) thread 4: duration=50, iteration=4, product=200
(alarm-multiple) thread 3: duration=40, iteration=5, product=200
(alarm-multiple) thread 2: duration=30, iteration=7, product=210
(alarm-multiple) thread 3: duration=40, iteration=6, product=240
(alarm-multiple) thread 4: duration=50, iteration=5, product=250
(alarm-multiple) thread 3: duration=40, iteration=7, product=280
(alarm-multiple) thread 4: duration=50, iteration=6, product=300
(alarm-multiple) thread 4: duration=50, iteration=7, product=350
(alarm-multiple) end
当然,有人要说了,这样看不出来跟忙等待实现的差别啊
这里就可以在./threads/init.c的main函数中加点东西
// main函数,东西太多就不贴了
// ...
printf("Boot complete.\n");
/* Run actions specified on kernel command line. */
run_actions(argv);
// 加上这句
thread_print_stats();
/* Finish up. */
shutdown();
thread_exit();
// ...
然后执行,看最后的一句输出
忙等待实现是这样的
Thread: 0 idle ticks, 592 kernel ticks, 0 user ticks
休眠队列的实现是这样的
Thread: 550 idle ticks, 40 kernel ticks, 0 user ticks
可以看到忙等待实现不会跑idle
需要注意的是,虽然alarm-priority在这个部分,但这个测试目前是过不了的
要到task2写完才可以
<0x02> Task2: Priority Scheduling
测试目标为[Lab1] Default Scheduler Debug
任务摘要
优先级调度器不会在之后的项目中使用
Task2.1 实现优先级调度
当一个线程加入到就绪队列时,如果这个新线程的优先级高于当前线程,当前线程应该立即让步
同样的,如果线程正在等待锁、信号或者条件时,高优先级的线程应当最先唤醒
线程自己可以改变优先级,但如果优先级改变后,自己不具有最高优先级,应当立即让步
Task2.2
优先级反转问题
考虑三个线程,分别有高中低的优先级
低优先级线程占有一个锁,然后这时候高优先级线程也想占用这个锁
那么高优先级线程会被阻塞,等待低优先级线程释放这个锁
但如果后面中优先级的线程启动了,那么由于优先级确实比低优先级高
中优先级线程会在低优先级线程时钟中断后执行,并且直到处理完毕
本来应该最优先处理的高优先级线程实际上没有最优先处理
这个现象称为优先级反转
priority
↑
│ ↓ 高优先级线程尝试获取锁,但会阻塞
│ │─│ │ high ──────────│
│ ↓ 中优先级启动 ↑ 高优先级反而最后处理
│ │ mid ─────────────│
│ ↓ 低优先级启动
│ │ low ─────│ │ low ──│
│ ↑ 低优先级时间片耗尽
└────────────────────────────────────────────────────────────→ time
所以需要实现优先级借用机制来解决这个问题
(原词是priority donation,我觉得翻译成优先级借用比较合适)
优先级借用就是如果有高优先级线程尝试获取锁
那么原来低优先级线程暂时获得高优先级线程的优先级
还是前面例子,如果使用优先级借用机制,那么优先级与时间关系图将会时这样
priority
↑
│ ↓ 尝试获取锁 ↓ 低优先级线程处理完毕后马上处理高优先级
│ │─││ low ──────││ high ──────────│
│ ↑ 低优先级线程优先级暂时提升 ↓ 中优先级启动
│ │ mid ───────────│
│ ↓ 低优先级启动
│ │ low ─│
│
└────────────────────────────────────────────────────────────→ time
低优先级线程优先级会暂时提升到高优先级线程的水平
实现低优先级处理完就处理高优先级线程任务
你需要考虑所有的优先级借用的情况
必须实现锁的优先级借用,其他的同步结构的优先级借用不必实现
你需要实现所有情况的优先级调度
确保多个优先级借用时的工作情况
支持嵌套优先级借用
还是一样的优先级借用,但是需要考虑嵌套时的优先级借用的传递问题
Task2.3 实现优先级的获取与修改
这个任务需要你修改thread_set_priority和thread_get_priority
对于thread_set_priority,如果新优先级不具有最高的优先级,那么当前线程应当让步
对于thread_get_priority,返回当前优先级或者说借用的优先级
pintos默认实现的东西
pintos虽然没有实现优先级调度
但已经提供了一套优先级
范围0到63,线程默认优先级为31
调度没做高优先级优先调度的功能,是先进先出的
在信号量、锁、条件这些同步设施上,pintos的实现也是最基本的
只是能用,也都没做优先级处理的操作
代码实现
Task2.1
这部分还是简单的
先在./threads/thread.c实现一个线程优先级比较函数
// lab1 线程优先级比较函数
bool thread_more_priority(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED)
{
struct thread *p_thread_a = list_entry(a, struct thread, elem);
struct thread *p_thread_b = list_entry(b, struct thread, elem);
return p_thread_a->priority > p_thread_b->priority;
}
修改的是thread_create函数
Task2.2
这个可以说是这个lab最难的部分
如果lock的部分没写好,main线程初始化都会出问题,调试都不好调试
所以很多时候都不知道自己哪里错了
Task2.3
thread_get_priority非常简单,直接返回当前线程优先级
int thread_get_priority(void)
{
return thread_current()->priority;
}
thread_set_priority也很简单,只是要处理一下锁的优先级借用问题
// lab1 当前线程是否持有锁
bool thread_is_holding_lock(void)
{
struct thread *cur = thread_current();
return !list_empty(&cur->lock_list);
}
void thread_set_priority(int new_priority)
{
// 断言限制优先级范围
ASSERT(PRI_MIN <= new_priority && new_priority <= PRI_MAX);
struct thread *cur = thread_current();
cur->base_priority = new_priority;
if (!thread_is_holding_lock() || new_priority > cur->priority)
{
cur->priority = new_priority;
thread_yield();
}
}
如果没持有锁,那么直接更新就可以,如果有锁,那么还要判断下是否大于当前优先级
基础优先级是一定会更新的
<0x03> Task3: Advanced Scheduler
测试目标为[Lab1] Advanced Scheduler Debug
任务摘要
实现附录中的BSD调度
高级调度器不会在之后的项目中使用
BSD调度
那么什么是BSD调度呢
前面我们实现的调度是基于优先级的,线程需要根据自己的情况给内核一个优先级
但这样不够智能,更何况如果有恶意线程啥都不干但占用最高优先级,那不就完了
BSD调度算是一种优先级计算方法,它会评估系统总体的占用负载和线程的cpu使用率
然后给线程一个合理的优先级
整个BSD调度中线程需要新增两个量,niceness和recent_cpuniceness表示线程的好心程度,范围是-20~20,默认值为0niceness越大,表示这个线程越好心,越愿意让步主动权
相反,如果这个值越小,表示这个线程不希望让步recent_cpu表示这个线程的cpu使用频率,没啥好说的
BSD调度的优先级计算方法如下,每4个时钟中断更新一次
$$priority=PRI_MAX-\frac{recent_cpu}{4}-2niceness$$
其中,recent_cpu的更新方法如下
每个时钟中断时,recent_cpu自增1
每过了物理时间1s,recent_cpu按下面公式重新计算
$$recent_cpu=\frac{2load_avg}{2*load_avg+1}*recent_cpu+niceness$$
这里出现的load_avg表示系统负载水平,计算公式如下,每物理时间1s更新一次
$$load_avg=\frac{59}{60}*load_avg+\frac{1}{60}*ready_threads$$
ready_threads表示不包括idle的运行线程和就绪线程的个数
(这里省略了很多公式推导的细节)
代码实现
这个还是比较简单的,基本上按着文档走就可以
实现模拟浮点库
pintos本身没有浮点类型,但BSD调度中会用到小数
好在文档的后面提供了一个方案,可以用模拟的浮点数
这个就按文档最后实现一下就好了
在我的代码中,实现的文件是./threads/fp.h和./threads/fp.c
这里就不贴了,挺重复的
需要指出的是,我使用了typedef int fp;,提示自己是浮点数
禁用优先级借用机制
文档中有提到,BSD调度器不需要优先级借用机制
所以就把它禁用掉就可以了
在测试这个任务的代码时,会传入-mlfqs这个参数
这个参数会使thread_mlfqs变为true
通过这个值可以判断是否测试的是高级调度器
void lock_acquire(struct lock *lock)
{
ASSERT(lock != NULL);
ASSERT(!intr_context());
ASSERT(!lock_held_by_current_thread(lock));
// lab1 高级调度不需要优先级借用
if (!thread_mlfqs)
{
lock_acquire_priority_donation(lock);
}
sema_down(&lock->semaphore);
lock->holder = thread_current();
}
void lock_release(struct lock *lock)
{
ASSERT(lock != NULL);
ASSERT(lock_held_by_current_thread(lock));
// lab1 高级调度不需要优先级借用
if (!thread_mlfqs)
{
lock_release_priority_donation(lock);
}
lock->holder = NULL;
sema_up(&lock->semaphore);
}
lock_acquire_priority_donation和lock_release_priority_donation就是前面实现的优先级借用方法
只是把这两个封装成单独的函数而已
修改thread相关的代码
在./threads/thread.h中添加niceness的范围与默认值定义
// lab1 高级调度 niceness部分
#define NICENESS_MIN -20
#define NICENESS_DEFAULT 0
#define NICENESS_MAX 20
添加load_avg的定义(在./threads/thread.c中也加一个)
// lab1 高级调度 系统负载
extern fp thread_load_avg;
给线程控制块加上niceness和recent_cpu这两个值
struct thread
{
// ...
int niceness; // lab1 高级调度 线程的好心程度
fp recent_cpu; // lab1 高级调度 线程使用的cpu时间
// ...
};
实现BSD调度的公式计算
因为这个调度的值都是线程相关的,所以相关的计算我也就放在./threads/thread.c里面了
后面./devices/timer.c也会用到这几个函数,所以要添加声明到对应头文件中
// lab1 高级调度 基于nice与recent_cpu计算线程优先级
void thread_mlfqs_refresh_priority(struct thread *t)
{
if (t == idle_thread)
{
return;
}
fp f_priority = fp_convert_from(PRI_MAX);
// 1/4 * recent_cpu
fp f_a = fp_div_int(t->recent_cpu, 4);
// 2*niceness
fp f_b = fp_convert_from(t->niceness);
f_b = fp_mul_int(f_b, 2);
// PRI_MAX - 1/4 * recent_cpu - 2 * niceness
fp f_ans = fp_sub_fp(f_priority, f_a);
f_ans = fp_sub_fp(f_ans, f_b);
t->priority = fp_toint_round(f_ans);
}
// lab1 高级调度 当前线程recent_cpu自增1
void thread_mlfqs_increase_recent_cpu(void)
{
struct thread *cur = thread_current();
if (cur == idle_thread)
{
return;
}
cur->recent_cpu = fp_add_int(cur->recent_cpu, 1);
}
// lab1 高级调度 计算线程最近cpu使用率
void thread_mlfqs_refresh_recent_cpu(struct thread *t)
{
if (t == idle_thread)
{
return;
}
// (2 * load_avg) / (2 * laod_avg + 1) * recent_cpu
fp f_a = fp_mul_int(thread_load_avg, 2);
f_a = fp_div_fp(f_a, fp_add_int(f_a, 1));
f_a = fp_mul_fp(f_a, t->recent_cpu);
fp f_niceness = fp_convert_from(t->niceness);
// (2 * load_avg) / (2 * laod_avg + 1) * recent_cpu + niceness
t->recent_cpu = fp_add_fp(f_a, f_niceness);
}
// lab1 高级调度 计算系统使用负载
void thread_mlfqs_refresh_load_avg(void)
{
// 59/60 * load_avg
fp f_a = fp_convert_from(59);
f_a = fp_div_int(f_a, 60);
f_a = fp_mul_fp(f_a, thread_load_avg);
// 1/60 * ready_threads
fp f_b = fp_convert_from(1);
f_b = fp_div_int(f_b, 60);
f_b = fp_mul_int(f_b, list_size(&all_list) - list_size(&sleep_list) - 1);
// 59/60 * load_avg + 1/60 * ready_threads
thread_load_avg = fp_add_fp(f_a, f_b);
}
修改时钟中断行为
毕竟是涉及到时间的操作,相关代码修改在./devices/timer.c中完成
static void timer_interrupt(struct intr_frame *args UNUSED)
{
ticks++;
thread_tick();
// lab1 将到达休眠时间将线程移出休眠队列
thread_wakeup();
// lab1 高级调度
if (thread_mlfqs)
{
// 每tick增加当前线程recent_cpu
thread_mlfqs_increase_recent_cpu();
if (timer_ticks() % TIMER_FREQ == 0)
{
// 每1s重新计算系统负载和所有线程的recent_cpu
thread_mlfqs_refresh_load_avg();
thread_foreach(thread_mlfqs_refresh_recent_cpu, NULL);
}
if (timer_ticks() % 4 == 0)
{
// 每4个tick重新计算所有线程的优先级
thread_foreach(thread_mlfqs_refresh_priority, NULL);
}
}
}
以上完成了所有修改,并且所有的测试也都可以过了
这部分的测试耗时很长,这个是正常的