操作系统 MIT6.S081 Lab5 Multithreads
实验原理
① Xv6 通过两种方式实现多路复用:
- 当一个进程带等待外设完成动作、等待一个子进程终结或者处于
sleep
时,Xv6 会将其从wakeup
状态切换为sleep
状态 - Xv6 定期切换一些占用 CPU 时间的进程的
sleep/wakeup
状态
② 具体实现:
-
Xv6 的每个 CPU 都有一个专门用于进程切换的调度器线程(运行函数
scheduler
),因为在老进程的内核栈上做进程切换是不安全的;通过shed
和sheculer
的协程实现这个过程 -
Xv6 通过函数
swtch
来实现进程上下文的保存和切换
③ 进程切换的过程:
- 老的用户进程进程由于中断、系统调用等进入 trap,执行该进程对应的内核线程
- 内核线程认为应当
yeild
出 CPU,然后调用函数shed
,并通过swtch
函数保存上下文、切换到调度器线程 - 调度器通过一个循环找出另一个可以运行的新进程,再通过
swtch
函数恢复上下文、切换到它对应的内核线程,再由内核线程返回到用户进程
Part 1 Uthread: switching between threads
实验目的: 为用户级线程系统设计并实现上下文切换机制
实验步骤:
① 给 struct thread
添加新的成员 struct context
,用于保存 PC、栈指针和 callee-saved 寄存器:
// kernel/uthread.c
struct context {
uint64 ra;
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};
struct thread {
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
struct context context; // pc, sp and callee-saved register
};
② 实现函数 swtch
,用于切换进程上下文:由于 struct context
在内存中是连续排布的,每个寄存器占 8B
,且栈向低地址方向增长,所以各个寄存器对应的位置分别是基址 + 8,+16 …
/* kernel/uthread_switch.S */
.text
/*
* save the old thread's registers,
* restore the new thread's registers.
*/
.globl thread_switch
thread_switch:
/* YOUR CODE HERE */
/* TODO */
sd ra, 0(a0)
sd sp, 8(a0)
sd s0, 16(a0)
sd s1, 24(a0)
sd s2, 32(a0)
sd s3, 40(a0)
sd s4, 48(a0)
sd s5, 56(a0)
sd s6, 64(a0)
sd s7, 72(a0)
sd s8, 80(a0)
sd s9, 88(a0)
sd s10, 96(a0)
sd s11, 104(a0)
ld ra, 0(a1)
ld sp, 8(a1)
ld s0, 16(a1)
ld s1, 24(a1)
ld s2, 32(a1)
ld s3, 40(a1)
ld s4, 48(a1)
ld s5, 56(a1)
ld s6, 64(a1)
ld s7, 72(a1)
ld s8, 80(a1)
ld s9, 88(a1)
ld s10, 96(a1)
ld s11, 104(a1)
ret /* return to ra */
③ 在 schedule
函数里调用 swtch
函数,切换上下文:
// kernel/uthread.c
void
thread_schedule(void)
{
...
if (current_thread != next_thread) { /* switch threads? */
next_thread->state = RUNNING;
t = current_thread;
current_thread = next_thread;
/* YOUR CODE HERE
* Invoke thread_switch to switch from t to next_thread:
* thread_switch(??, ??);
*/
thread_switch((uint64)(&(t->context)), (uint64)(&(current_thread->context)));
} else
next_thread = 0;
}
④ 补全 thread_create()
:创建线程时,要设置其入口地址和栈的起始位置;由于栈式向低地址方向增长,因此要取栈的最高位作为栈底:
// kernel/uthread.c
void
thread_create(void (*func)())
{
...
t->context.ra = (uint64)func;
t->context.sp = (uint64)(t->stack + STACK_SIZE);
}
实验结果:
① 执行 uthread
:
② make grade
的 uthread 部分:
遇到问题:
① proc.h
中已经有 struct context
,本来想直接 #include "kernel/proc.h"
来引用 struct context
,但是出现了其他错误:
proc.h
和其他文件还有依赖关系,比较复杂,所以最终直接在 uthread.c
中重新定义了一遍 struct context
② 调用 thread_swtch
时没有注意到保存进程的变量已经发生交换,原来的 current_thread
变成了 t
,现在的 current_thread
是原来的 new_thread
,结果写成了:
// kernel/uthread.c thread_schduler()
thread_switch((uint64)(&(current_thread->context)), (uint64)(&(new_thread->context)));
问题回答:thread_switch
只需要保存/恢复 callee-save registers,思考下为什么
因为调用者需要保存的寄存器,在调用 swtch
函数之前就已经被压入栈了,并不会受到寄存器的影响
Part 2 Using threads
实验目的: 为哈希表加锁,防止 race condition 造成的损失
实验步骤:
① 为每个桶都创建锁:
// notxv6/ph.c
#include <pthread.h>
pthread_mutex_t bucket_locks[NBUCKET];
int
main(int argc, char *argv[])
{
...
// initialize locks
for (int i = 0; i < NBUCKET; ++i) {
pthread_mutex_init(&bucket_locks[i], NULL);
}
...
}
② 在 put()
函数插入 key 前后加上锁:
// notxv6/ph.c
static
void put(int key, int value)
{
...
pthread_mutex_lock(&bucket_locks[i]);
insert(key, value, &table[i], table[i]);
pthread_mutex_unlock(&bucket_locks[i]);
...
}
实验结果:
① 没有加锁之前,多线程的情况下会出现某些 key 添加失败的情况:
② 加锁之后,即使是多线程也不会出现 key 添加失败的情况:
并且能够通过 make grade
的 ph_test
部分:
问题回答:为什么两个线程会丢失 keys,但是一个线程不会?确定一种两个线程
的执行序列,可以使得 key 丢失
当两个线程同时向同一个桶添加 key 时,有可能会出现 race condition,使得某个 key 丢失;向哈希表中插入结点分为两步:e->next = (*p)->next
和 (*p)->next = e
;例如,当线程 1 和 2 同时向桶 *p
插入 key 时,线程 1 执行了 e1->next = (*p)->next
后被调度走,接着线程 2 执行 e2->next = (*p)->next
和 (*p)->next = e2
,最后调度回线程 1 完成 (*p)->next = e1
,此时e2
结点就被丢失了
Part 3 Barrier
实验目的: 实现一个 barrier:当一个线程到这个点后,必须等待其余所有线程都到达这点
实验步骤: 在 barrier.c
中添加以下代码:当线程进入 barrier()
时,先对 bstate
上锁,接着增加进入 barrier()
的线程个数;当发现所有线程都进入时,就广播唤醒所有线程,否则自己也进入睡眠
// notxv6/barrier.c
static void
barrier()
{
pthread_mutex_lock(&(bstate.barrier_mutex));
++bstate.nthread;
if (bstate.nthread == nthread) {
bstate.nthread = 0;
++bstate.round;
pthread_cond_broadcast(&(bstate.barrier_cond));
pthread_mutex_unlock(&(bstate.barrier_mutex));
} else {
pthread_cond_wait(&(bstate.barrier_cond), &(bstate.barrier_mutex));
}
pthread_mutex_unlock(&(bstate.barrier_mutex));
}
实验结果:
① 可以通过 barrier
测试:
并且能够通过 make grade
的 ph_test
部分: