文章目录
- 一、Uthread: switching between threads
- 简介
- 提示
- 实验代码
- 实验结果
- 二、Using threads
- 简介
- 实验代码
- 三、Barrier
- 简介
- 实验代码
- 实验结果
一、Uthread: switching between threads
简介
切换到 thread
分支
- git fetch
- git checkout thread
- make clean
实现用户态线程的上下文切换机制,该实验已准备了 user/uthread.c
和 user/uthread_switch.S
,makefile
添加了构建规则。但是缺少了创建用户态线程以及线程切换的代码。
我们的任务就是实现一套机制,包含创建用户态线程以及保存/恢复线程上下文的功能。
完成实验之后,执行make qemu
,并运行 uthread
程序,应该可以看到如下图输出:
- 在
user/uthread.c
中完善thread_create()
和thread_schedule()
函数 - 在
user/uthread_switch.S
中完善thread_switch
- 目标是保证
thread_schedule()
首次调度一个给定的用户态线程时,该线程需要在它的栈上执行通过thread_create()
创建线程时传递的函数 - 另一个目标是保存被切换用户态线程的寄存器上下文,恢复获得cpu的线程的寄存器上下文,以使得其可以从上一次中断的位置继续执行
提示
thread_switch
只需要保存/恢复仅由被调用者(callee)
负责保存的寄存器。为什么? 这是因为线程被切换出去时肯定是在一个函数中,此时寄存器中的值只有被调用者
保存的寄存器是可能被线程切换代码导致丢失的。因此需要保存,而由调用者
保存的寄存器,已经在函数栈帧中保存了一次。- 可以在
user/uthread.asm
中看到uthread
的汇编代码,这可以帮助调试 - 使用
riscv64-linux-gnu-gdb
进行thread_switch
的单步调试,方法如下:
实验代码
思路:
- user/uthread.c:添加一个
struct thread_context
结构体,用于记录用户态线程上下文 - user/uthread.c:
thread_create()
完善用户态线程的创建逻辑 - user/uthread_switch.S:实现
thread_switch()
用于保存被调度用户态线程的上下文,恢复切换用户态线程的上下文
代码:
- user/uthread.c
//定义用户态线程上下文
struct thread_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 thread_context context; // 新增
};
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
// 新增
memset(&t->context, 0, sizeof(struct thread_context));
t->context.ra = (uint64)func;
t->context.sp = (uint64)t->stack + STACK_SIZE; //栈顶为高地址
}
void
thread_schedule(void)
{
struct thread *t, *next_thread;
/* Find another runnable thread. */
// ...
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(参考内核态 swtch()函数)
thread_switch((uint64)&t->context, (uint64)&next_thread->context);
}
// ...
}
- user/uthread_switch.S
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 */
实验结果
- uthread
二、Using threads
简介
本实验将使用哈希表来探索锁和线程的并发编程。
notxv6/ph.c
中包含一个简单的不保证并发安全的哈希表实现,可以在xv6的主目录使用 make ph
、./ph 1
将产生如下结果(需要使用gcc
编译,而不是riscv
的交叉工具链,且结果的数值取决于具体的机器):
然后通过 ./ph 2
尝试并发版本,将得到如下结果:
实验代码
本实验比较简单
static
void put(int key, int value)
{
int i = key % NBUCKET;
// 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 key is new.
pthread_mutex_lock(&locks[i]);
insert(key, value, &table[i], table[i]);
pthread_mutex_unlock(&lock[i]);
}
}
三、Barrier
简介
实现一个 barrier,即每个线程都要在 barrier 处等待所有线程到达 barrier 之后才能继续运行。因此我们需要用到 unix
提供的条件变量以及 wait/broadcast
机制。
实验代码
static void
barrier()
{
// YOUR CODE HERE
//
// Block until all threads have called barrier() and
// then increment bstate.round.
//
pthread_mutex_lock(&bstate.barrier_mutex);
bstate.nthread++;
if (bstate.nthread < nthread) {
pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
} else {
bstate.round++;
bstate.nthread = 0;
pthread_cond_broadcast(&bstate.barrier_cond);
}
pthread_mutex_unlock(&bstate.barrier_mutex);
}