Uthread: switching between threads (moderate)
在这个实验中,我们需要补充完整uthread的上下文切换逻辑,以实现一个简单的用户级线程。
要实现线程切换,需要保存寄存器上下文,riscv通用的寄存器有31个,但这里只需要保存callee-save的寄存器。这是因为切换用户线程时都是在调用thread_switch函数,thread_switch中切换出去再恢复回来可以看成是一个函数调用,thread_switch的调用者会保存caller-save的寄存器,等执行完thread_switch再恢复它们原本的值,所以在thread_switch中只需要保存那些callee-save的寄存器就可以了,还能节省空间。此外还需要保存ra寄存器,因为riscv不允许在user mode直接修改程序计数器PC的值,所以需要借助ra寄存器和ret指令来修改PC的值以更改程序执行流。所以这里共计保存14个寄存器。
在uthread.c中,给struct thread添加一个字段context,用于保存寄存器上下文。
// user/uthread.c
#define CONTEXT_SIZE 14
struct thread {
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
uint64 context[CONTEXT_SIZE]; /* (Only callee reserverd resgisters) */
};
然后在创建用户线程时需要设置好它的初始上下文,好让它第一次被调度时开始执行传入的函数func,并且设置好它的栈指针地址,由于栈是从高向低增长的,所以要设置成t->stack+STACK_SIZE-1。context[0]和context[1]分别对应ra和sp(和下面的切换时保存的顺序一致)
void
thread_create(void (*func)())
{
struct thread *t;
for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
if (t->state == FREE) break;
}
t->state = RUNNABLE;
// YOUR CODE HERE
t->context[0] = (uint64)func; // set ra
t->context[1] = (uint64)t->stack + STACK_SIZE - 1;
}
在thread_schedule中调用thread_switch
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)next_thread->context);
} else
next_thread = 0;
}
thread_switch的切换上下文逻辑其实在kernel/swtch.S中已经见过了,我们直接把它复制到uthread_switch.s中即可。
// user/uthread_switch.s
.text
/*
* save the old thread's registers,
* restore the new thread's registers.
*/
.globl thread_switch
thread_switch:
/* YOUR CODE HERE */
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 */
Using threads (moderate)
接下来的两个小实验都不是在xv6上做的,而是直接在linux机器上做。这部分实验很简单,只是在一个哈希表结构上适当的位置使用锁来保护关键数据和不变量,让其支持多线程。这里有个简单的小优化,由于不同的bucket上的读写是不冲突的所以可以每个bucket使一个锁(也就是降低锁的颗粒度)来提升性能,代码如下。
// notxv6/pc.c
// ...
static
void put(int key, int value)
{
int i = key % NBUCKET;
pthread_mutex_lock(&locks[i]);
//...
pthread_mutex_unlock(&locks[i]);
}
static struct entry*
get(int key)
{
int i = key % NBUCKET;
pthread_mutex_lock(&locks[i]);
// ...
pthread_mutex_unlock(&locks[i]);
return e;
}
int
main(int argc, char *argv[])
{
pthread_t *tha;
void *value;
double t1, t0;
for(int i=0;i<NBUCKET;i++) {
pthread_mutex_init(&locks[i], NULL);
}
//...
}
Barrier(moderate)
这部分实验只是为了熟悉pthread_cond_wait和pthread_cond_broadcast的使用,也就是signal和wait来进行线程同步。
直接贴代码。
static void
barrier()
{
// YOUR CODE HERE
//
// Block until all threads have called barrier() and
// then increment bstate.round.
//
pthread_mutex_lock(&bstate.barrier_mutex);
if (++bstate.nthread < nthread) {
pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
} else {
bstate.nthread = 0;
bstate.round++;
pthread_cond_broadcast(&bstate.barrier_cond);
}
pthread_mutex_unlock(&bstate.barrier_mutex);
}
这里别忘记用互斥锁来保护bstate的数据和不变量。