笔记017 - HW8: User-level threads

Switching threads

本次练习是在用户程序uthread.c中模拟进程之间的切换。
进程结构为:

1
2
3
4
5
6
7
struct thread {
int sp; /* curent stack pointer */
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* running, runnable, waiting */
};

static thread_t all_thread[MAX_THREAD];

sp变量用于保存切换进程时“当前进程”的esp。初始化进程的时候,留出栈顶4字节空间保存函数mythread地址,如果进程切换时切换到的进程尚未执行过,则会执行该函数;然后留出32字节的空间,并更新sp变量指向距离栈顶36字节位置,如果进程切换时切换到的进程尚未执行过,则将这32字节弹出到通用寄存器。stack即为模拟的进程执行过程中所使用的栈。

uthread.c定义了进程树组all_thread,并在初始化过程中指定

1
2
current_thread = &all_thread[0];
current_thread->state = RUNNING;

这样,第一个进程被假设为正在执行,它永远不会被真正执行,因为之后的进程切换都在其他RUNNABLE进程中选取。

本次作业主要要完成进程切换的工作,即完成uthread_switch.S。每次跳转到thread_switch执行进程切换时,由于是运行在“当前进程”current_thread栈上,所以直接在当前栈上保存通用寄存器(pushal),然后将esp保存到“当前进程”current_thread的sp上,这样下次切换为该进程时直接将sp赋值给esp之后就可以执行popal。之后跳转到next_thread的sp指向的位置(进程第一次被调用时,sp指向的是距离栈顶36字节位置),并将current_thread指向next_thread,将next_thread置0,然后弹出通用寄存器,执行ret。

执行ret之后会有什么效果呢?分两种情况:1、进程未被执行过时,sp指向距离栈顶36字节位置,弹出32字节到通用寄存器之后,执行ret会执行绑定的函数mythread。2、此后,进程在“当前进程”栈上执行,当再次次跳转到thread_switch执行进程切换时,首先将下一条指令的eip进栈,然后再pushal保存通用寄存器,并将最新的esp保存到“当前进程”的sp变量中。这样后续切换到该进程时,则弹出通用寄存器后执行ret会执行保存eip,即恢复执行调用thread_switch的下一条指令。

需要注意的是:由于current_thread一开始被初始化为all_thread[0],且没有被真正执行过,所以第一次跳转到thread_switch执行pushal的时候是保存通用寄存器到用户程序uthread的用户栈上的;后续进程切换的时候,跳转到thread_switch后执行pushal才真正地在“当前进程”栈上保存通用寄存器,但是这对于本程序的模拟没有影响。

uthread_switch.S如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	.text

/* Switch from current_thread to next_thread. Make next_thread
* the current_thread, and set next_thread to 0.
* Use eax as a temporary register, which should be caller saved.
*/
.globl thread_switch
thread_switch:
/* YOUR CODE HERE */

# We are running on current_thread's stack, push all registers to save
# attention: first time we call thread_switch, we actually do not run on current_thread's stack
# because current_thread point to all_thread[0], and we just fake it is running, but it does not matter
pushal

# Save current thread's stack pointer
movl current_thread, %eax
movl %esp, (%eax)

# Switch stacks to next thread
movl next_thread, %eax
movl (%eax), %esp #point to sp'value, if is every thread's first-time-running, it already leaves the space to save registers

# Set current_thread to next_thread
movl %eax, current_thread
# Set next_thread to 0
movl $0, next_thread

# Pop next thread's registers from the stack into the appropriate registers
popal

// exec current_thread
ret /* pop return address from stack */

调试内置用户程序技巧

启动qemu之后,可以使用以下方法调试内置的用户程序:

1
2
3
4
5
6
7
8
9
(gdb) symbol-file _uthread
Load new symbol table from "/Users/kaashoek/classes/6828/xv6/_uthread"? (y or n) y
Reading symbols from /Users/kaashoek/classes/6828/xv6/_uthread...done.
(gdb) b thread_switch
Breakpoint 1 at 0x204: file uthread_switch.S, line 9.
(gdb) c

(gdb) print /x *next_thread
$1 = {sp = 0x4d48, stack = {0x0 , 0x61, 0x1, 0x0, 0x0}, state = 0x1}

Challenge exercises

上述模拟的进程切换是通过进程主动调用thread_switch完成的,有以下缺点:
1、假如用户进程A阻塞等待系统调用结果,用户进程B不会被执行,因为xv6的调度器不知道用户进程A被取消执行。
2、不同进程不能在不同核上并发执行,因为xv6的调度器没有将多个进程并发执行的策略。在当前的实现下,进程不能被并发执行,例如不同处理器同时调用thread_schedule的话会导致错误。

有以下解决方法:
1、scheduler activations
2、one kernel thread per user-level thread (as Linux kernels do)
借助locks、condition variables、barriers等,尝试在xv6实现其中一种方式。

1
???

显示 Gitment 评论