用户态线程作为轻量级的进程,相比于进程有着更加方便的通信机制和更加灵活的使用 方法。本次实验的主题是用户态线程,我们将主要在用户态进行线程相关的实验。

首先切换到 thread 分支

6.1 用户态进程 Uthread

在本实验中,我们需要完成一个用户态线程库中的功能。xv6 为该实验提供了基本的代码: user/uthread.c 和 user/uthread_switch.S 。我们需要在 user/uthread.c 中实现 thread_create() 和 thread_schedule() ,并且在 user/uthread_switch.S 中实现 thread_switch用于切换上下文。

首先我们查看 user/uthread.c 中关于线程的一些数据结构:

/* Possible states of a thread: */
#define FREE        0x0
#define RUNNING     0x1
#define RUNNABLE    0x2

#define STACK_SIZE  8192
#define MAX_THREAD  4

struct thread {
  char       stack[STACK_SIZE]; /* the thread's stack */
  int        state;             /* FREE, RUNNING, RUNNABLE */
};
struct thread all_thread[MAX_THREAD];
struct thread *current_thread;

线程的数据结构十分简洁,struct thread 中,一个字节数组用作线程的栈,一个整数用于表示线程的状态。不难发现我们还需要增加一个数据结构用于保存每个线程的上下文,故参照内核中关于进程上下文的代码,增加以下内容并把它加到上述 thread结构体中:

// Saved registers for user 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;
};

参考 xv6 实验手册的提示,除了 sp 、s0 和 ra 寄存器,我们只需要保存 callee-saved 寄存器,因此构造了上面的 struct context 结构体。有了该结构体,我们仿照 kernel/trampoline.S的结构,按照 struct context 各项在内存中的位置,在 user/uthread_switch.S 中加入如下的代码:

thread_switch:
	/* YOUR CODE HERE */
	
	/* Save registers */
	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)
	/* Restore registers */
	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 */

以上函数的实现过程如下:

  1. 首先,在保存寄存器状态之前,需要将保存寄存器的内存地址传递给该函数,这里假设该地址保存在寄存器a0中,恢复寄存器的地址保存在寄存器a1中。
  2. 接下来,使用sd指令将当前线程的寄存器状态保存到内存中。具体来说,将返回地址寄存器ra、栈指针寄存器sp和调用者保存寄存器s0s11的值分别保存到a0指向的内存区域的偏移量为0-104的位置。
  3. 然后,使用ld指令将要切换到的线程的寄存器状态从内存中恢复。具体来说,将返回地址寄存器ra、栈指针寄存器sp和调用者保存寄存器s0s11的值分别从a1指向的内存区域的偏移量为0-104的位置读取出来。
  4. 最后,使用ret指令返回到返回地址寄存器ra指向的位置,即切换到另一个线程的代码中执行。

这样就完成了上下文切换的功能,接下来需要完成创建线程和调度线程的部分。创建线程时,我们需要将线程的栈设置好,并且需要保证在线程被调度运行时能够将 pc 跳转到正确的位置。上面的 thread_switch 在保存第一个进程的上下文后会加载第二个进程的上下文,然后跳至刚刚加载的 ra 地址处开始执行,故而我们在创建进程时只需将 ra 设为我们所要执行的线程的函数地址即可。于是 thread_create() 的实现如下:

.....	
	// YOUR CODE HERE
  t->context.ra = (uint64)func;
  t->context.sp = (uint64)&t->stack[STACK_SIZE]

类似的,在调度线程时,我们选中下一个可运行的线程后,使用 thread_switch 切换上下文即可,实现如下:

void
thread_schedule(void)
{
	......
	/* YOUR CODE HERE
	* Invoke thread_switch to switch from t to next_thread:
	* thread_switch(??, ??);
	*/
	// t->state = RUNNABLE;
	thread_switch((uint64)&t->context, (uint64)&current_thread->context);
	......
}