1 Uthread: switching between threads
在本次练习中,你将要设计面向用户级线程系统的上下文切换机制,然后实现它。为了帮助你开始实现它,xv6提供了两个文件,user/uthread.c和user/uthread_switch.S,和一个在Makefile文件中提供的build uthread 程序的规则。uthread.c包括绝大多数用户级线程包,和三个简单的线程测试样例。这个线程包中丢失了部分创建线程和转换线程的代码。
你的任务是提出一种创建线程和在线程切换中保存/恢复寄存器的方案,并实现它。当你完成之后,make grade会告诉你你的方案通过了uthread测试。
一旦你完成了,当你在xv6上运行uthread你应当看到以下形式的输出。
$ make qemu
...
$ 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 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
$
这些输出来自于三个测试线程,每个测试线程都会有一个循环来打印行,然后放弃CPU给其他线程。
然而,如果没有上下文切换的代码,什么都不会输出。
你需要给user/uthread.c中的thread_create()和thread_schedule()和user/uthread_switch.S中的thread_switch部分添加代码。一个目标是确保当thread_schedule()第一次执行某个线程时,这个线程执行被传递到thread_create()的函数,在它自己的栈上面。另外一个目标是确保thread_switch保存被切换出去线程的寄存器,恢复被切换进来的线程寄存器的内容,并且返回后者线程执行的指令的位置。你必须决定哪里去保存/恢复寄存器;修改struct thread来保存寄存器内容是一个好的方案。你需要给在thread_schedule中的thread_switch增加一个调用;你可以传递任何参数给thread_switch,但是目标是将线程从线程t切换到线程next_thread。
一些提示:
-
thread_switch只需要保存或恢复callee-save的寄存器。为什么?
-
你可以看看user/uthread.asm中的相关汇编代码,这对debug非常有帮助。
-
为什么测试你的代码,单步测试是非常有帮助的。你可以按以下方式开始:
(gdb) file user/_uthread Reading symbols from user/_uthread... (gdb) b uthread.c:60
这在uthread.c的60行设置了断点。在你运行uthread之前,这个断点也可能会被触发。为什么?
一旦你的xv6 shell运行,输入“uthread”,gdb将会在60行break。现在你可以输入以下命令来查看uthread的状态:
(gdb) p/x *next_thread
用“x",你可以查看某个地址的内容:
(gdb) x/x next_thread->stack
你可以跳过thread_switch的开头:
(gdb) b thread_switch (gdb) c
你可以单步调试汇编指令:
(gdb) si
新建一个thread的成员来存放寄存器内容
struct thread {
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
uint64 regs[REG_SIZE/8]; /* the saved registers */
};
在thread_create中添加代码
注意寄存器ra中存放的就是返回执行指令的地址。
就修改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
// Clean the regs.
memset((char*)t->regs,0,REG_SIZE);
// Modify the sp and pc.
*(t->regs + 1) = (uint64)(t->stack + STACK_SIZE);
*(t->regs) = (uint64)func;
return;
}
在thread_scheduler中添加代码
其实只需要添加上下文切换的函数即可,因为这边状态的修改已经完成。
void
thread_schedule(void)
{
struct thread *t, *next_thread;
/* Find another runnable thread. */
next_thread = 0;
t = current_thread + 1;
for(int i = 0; i < MAX_THREAD; i++){
if(t >= all_thread + MAX_THREAD)
t = all_thread;
if(t->state == RUNNABLE) {
next_thread = t;
break;
}
t = t + 1;
}
if (next_thread == 0) {
printf("thread_schedule: no runnable threads\n");
exit(-1);
}
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->regs,(uint64)current_thread->regs);
} else
next_thread = 0;
}
上下文切换
跟swtch完全一样。
.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 */
这部分都比较简单,可以说是内核切换的简化版,毕竟也不用考虑并发的问题。
2 Using threads
在这部分,你将会探索并行编程,涉及到线程和使用哈希表的锁。你应当在一个真实的多核Linux或者MacOS机上面完成这个部分。大多数笔记本都是多核处理器。
这个任务会用到UNIX的pthread线程库。你可以在参考页找到线管信息,你也可以在网上查看相关信息。
notxv6/ph.c文件包含了简单的哈希表,如果是单线程,这个哈希表就是正确的,但如果是多线程就不是了。在你的xv6目录中键入:
$ make ph
$ ./ph 1
注意,为了build ph,Makefile用了自己OS的gcc,而不是6.S081的工具。ph的参数表示了在哈希表上执行put和get的线程数。在运行一会之后,ph 1将会输出类似于以下的内容:
100000 puts, 3.991 seconds, 25056 puts/second
0: 0 keys missing
100000 gets, 3.981 seconds, 25118 gets/second
你看到的数字可能会有点不一样,这取决于你的计算机有多快,是否是多核的,是否同时在处理其他内容。
ph会运行两个内容。第一个是通过put(给哈希表增加键值,并且打印出每秒的执行速率。get()从哈希表上面取得值。它会打印哈希表上本身应该具有多少值,来作为丢失数量的结果,并且打印每秒的get()速率。
你可以告诉通过给一个大于1的参数,使得ph会进行多进行执行。尝试 ph 2:
$ ./ph 2
100000 puts, 1.885 seconds, 53044 puts/second
1: 16579 keys missing
0: 16579 keys missing
200000 gets, 4.322 seconds, 46274 gets/second
第一行表示有两个线程并发地增添entry,他们每秒总共实现了53044次put。这大概是ph 1的两倍。
然而下面两行表明15479的键值丢失了,这说明很多本该在哈希表上的键值丢失了。put本应该把这些值加入到哈希表上面,但是出了一些问题。看一下notxv6/ph.c,尤其是put()和insert()
为什么单线程不会丢失,而双线程就丢失了这么多?阐述清除双线程导致这种丢失的事件先后顺序。提交这个顺序到 answers-thread.txt
为什么避免这种事件顺序,需要在put()和get()中添加锁,来实现丢失数为0.相关的pthread调用是:
pthread_mutex_t lock; // declare a lock
pthread_mutex_init(&lock, NULL); // initialize the lock
pthread_mutex_lock(&lock); // acquire lock
pthread_mutex_unlock(&lock); // release lock
当make grade显示你哦通过了 ph_safe测试,0 missing。ph_fast测试失败是正常的。
不要忘记调用pthread_mutex_init()。先用单线程测试你的代码,然后用双线程。它对吗?双线程版本的速率是两倍吗?
并发的put()并不会覆盖整个内存,因此不需要保护全部的bucket。你可以改变ph.c来得到两倍的速度吗?提示:一个锁怎么给每个bucket加锁?
修改你的代码,使得可以在某些put操作中实现并行并保证正确。当你make grade,并显示通过ph_safe和ph_fast就表示完成了。ph_fast要求双线程的速度必须是单线程素的1.25倍多。
实现
这个task非常简单~~
其中实现的连接法的哈希表,所以每次对bucket上锁即可。
// 添加锁
pthread_mutex_t lock[NBUCKET];
// 修改put
static
void put(int key, int value)
{
int i = key % NBUCKET;
pthread_mutex_lock(lock + i);
// is the key already present?
struct entry *e = 0;
for (e = table[i]; e != 0; e = e->next) {
if (e->key == key)
break;
}
if(e){
// update the existing key.
e->value = value;
} else {
// the new is new.
insert(key, value, &table[i], table[i]);
}
pthread_mutex_unlock(lock + i);
}
// 在main中初始化锁
// Init the locks.
for(int i = 0; i < NBUCKET;i++)
pthread_mutex_init(lock+i,NULL);
3 Barrier
在这个ass中,你将会实现一个栅栏:线程必须等待所有其他线程抵达才能继续。你可以使用pthread的条件变量,这个技术和xv6的sleep和wakeup类似。
你应当在一个真实计算机上完成这个ass。
文件 notxv6/barrier.c 中包含了未完成的栅栏。
$ make barrier
$ ./barrier 2
barrier: notxv6/barrier.c:42: thread: Assertion `i == t' failed.
2表示在栅栏中同步的线程数量。每个线程在循环中执行。在每个循环迭代中线程都调用*barrier()*然后sleep若干微秒。警告会在其他线程并未全部抵达栅栏,而某个线程离开栅栏的情况下引发。理想的情况是每个线程都阻塞至其他线程都调用了barrier()。
你的目标是实现一个理想的栅栏行为。除了你之前见过的lock原语,你还需要学习以下线程原语:
pthread_cond_wait(&cond, &mutex); // go to sleep on cond, releasing lock mutex, acquiring upon wake up
pthread_cond_broadcast(&cond); // wake up every thread sleeping on cond
请确保你的答案通过了make grade中的barrier测试。
pthread_cond_wait释放mutex,并且在返回之前重新申请mutex。
我们已经给你了 barrier_init()。你的工作就去实现 barrier(),使得panic不出现。我们已经定义了 struct barrier ,你可以使用它的域。
这里有两个问题会增加你工作的难度:
- 你必须处理一次成功的栅栏调用,我们把次数叫做轮数。bstate.round记录了当前的轮数。你应该在所有线程都调用barrier()之后增加bstate.round。
- 你必须处理这样一种情况:在其他进程退出这个栅栏之前,某个进程在循环中竞争。特别的,你会在不同的轮数中重复使用bstate.nthread变量。确保当先前的一轮还未结束时,一个离开栅栏并且竞争的线程不会增加bstate.nthread。
修改barrier()
这个也非常简单~~
最后一个抵达栅栏的线程修改状态,并唤醒沉睡的线程即可。
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){
bstate.nthread = 0;
bstate.round++;
pthread_cond_broadcast(&bstate.barrier_cond);
}
else{
pthread_cond_wait(&bstate.barrier_cond,&bstate.barrier_mutex);
}
pthread_mutex_unlock(&bstate.barrier_mutex);
}
总体来说这个lab难度比较低。