Uthread: switching between threads (moderate)
In this exercise you will design the context switch mechanism for a
user-level threading system, and then implement it. To get you
started, your xv6 has two files user/uthread.c and
user/uthread_switch.S, and a rule in the Makefile to build a uthread
program. uthread.c contains most of a user-level threading package,
and code for three simple test threads. The threading package is
missing some of the code to create a thread and to switch between
threads.
实现用户级的线程上下文的切换。
这里的线程相比现代操作系统中的线程而言,更接近一些语言中的“协程”(coroutine)。原因是这里的“线程”是完全用户态实现的,多个线程也只能运行在一个 CPU 上,并且没有时钟中断来强制执行调度,需要线程函数本身在合适的时候主动 yield 释放 CPU。这样实现起来的线程并不对线程函数透明,所以比起操作系统的线程而言更接近协程。
这个实验其实相当于在用户态重新实现一遍 xv6 kernel 中的 scheduler() 和 swtch() 的功能,所以大多数代码都是可以借鉴的。
uthread_switch.S 中需要实现上下文切换的代码,这里借鉴 swtch.S:
// uthread_switch.S
.text
/*
* save the old thread's registers,
* restore the new thread's registers.
*/
// void thread_switch(struct context *old, struct context *new);
.globl thread_switch
thread_switch:
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 */
从 proc.h 中借鉴一下 context 结构体,用于保存 ra、sp 以及 callee-saved registers:
// uthread.c
// Saved registers for thread context switches.
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 ctx; // 在 thread 中添加 context 结构体
};
struct thread all_thread[MAX_THREAD];
struct thread *current_thread;
extern void thread_switch(struct context* old, struct context* new); // 修改 thread_switch 函数声明
在 thread_schedule 中调用 thread_switch 进行上下文切换:
// uthread.c
void
thread_schedule(void)
{
// ......
if (current_thread != next_thread) { /* switch threads? */
next_thread->state = RUNNING;
t = current_thread;
current_thread = next_thread;
thread_switch(&t->ctx, &next_thread->ctx); // 切换线程
} else
next_thread = 0;
}
再补齐 thread_create:
// uthread.c
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;
t->ctx.ra = (uint64)func; // 返回地址
// thread_switch 的结尾会返回到 ra,从而运行线程代码
t->ctx.sp = (uint64)&t->stack + (STACK_SIZE - 1); // 栈指针
// 将线程的栈指针指向其独立的栈,注意到栈的生长是从高地址到低地址,所以
// 要将 sp 设置为指向 stack 的最高地址
}
添加的部分为设置上下文中 ra 指向的地址为线程函数的地址,这样在第一次调度到该线程,执行到 thread_switch 中的 ret 之后就可以跳转到线程函数从而开始执行了。设置 sp 使得线程拥有自己独有的栈,也就是独立的执行流。
$ uthread
thread_a started
thread_b started
thread_c started
thread_c 0
thread_a 0
thread_b 0
thread_c 1
thread_a 1
thread_b 1
......
thread_c 98
thread_a 98
thread_b 98
thread_c 99
thread_a 99
thread_b 99
thread_c: exit after 100
thread_a: exit after 100
thread_b: exit after 100
thread_schedule: no runnable threads
Using threads (moderate)
分析并解决一个哈希表操作的例子内,由于 race-condition 导致的数据丢失的问题。
Why are there missing keys with 2 threads, but not with 1 thread? Identify a sequence of events with 2 threads that can lead to a key being missing. Submit your sequence with a short explanation in answers-thread.txt
整个哈希表一个锁变成每个 bucket 一个锁
// ph.c
pthread_mutex_t locks;
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);
}
// ......
}
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;
}
编译执行:
$ ./ph 1
100000 puts, 4.940 seconds, 20241 puts/second
0: 0 keys missing
100000 gets, 4.934 seconds, 20267 gets/second
$ ./ph 2
100000 puts, 3.489 seconds, 28658 puts/second
0: 0 keys missing
1: 0 keys missing
200000 gets, 6.104 seconds, 32766 gets/second
$ ./ph 4
100000 puts, 1.881 seconds, 53169 puts/second
0: 0 keys missing
3: 0 keys missing
2: 0 keys missing
1: 0 keys missing
400000 gets, 7.376 seconds, 54229 gets/second
Barrier (moderate)
利用 pthread 提供的条件变量方法,实现同步屏障机制。
// barrier.c
static void
barrier()
{
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);
}
执行测试:
$ ./barrier 1
OK; passed
$ ./barrier 2
OK; passed
$ ./barrier 4
OK; passed
参考文章:
https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/
https://walkerzf.github.io/2020/11/17/Multithreading/