多路复用机制为进程提供了独占处理器的假象,实现多路复用有几个难点。首先,应该如何从运行中的一个进程切换到另一个进程?xv6 采用了普通的上下文切换机制
;虽然这里的思想是非常简洁明了的,但是其代码实现是操作系统中最晦涩难懂的一部分。第二,如何让上下文切换透明化?xv6 只是简单地使用时钟中断处理程序
来驱动上下文切换。第三,可能出现多个 CPU 同时切换进程的情况,那么我们必须使用一个带锁的方案
来避免竞争。第四,进程退出时必须释放其占用内存与资源
,但由于它本身在使用自己的资源(譬如其内核栈),所以不能由该进程本身释放其占有的所有资源。
uthread
创建两个线程,并在线程之间来回切换。每个线程打印“my thread…”,然后让另一个线程有机会运行。
您需要完成 thread_switch.S
,但在跳转到 thread_switch.S之前。首先了解uthread.c如何使用thread_switch。uthread.c有两个全局变量current_thread
和next_thread
,都是指向thread structure的指针。thread structure有一个线程堆栈
和一个保存的堆栈指针(sp,它指向线程的堆栈)。
uthread_switch的任务是将当前线程状态保存
到current_thread所指向的结构中,恢复next_thread的状态
,并使current_thread指向next_thread所指向的位置
,这样当uthread_switch返回next_thread正在运行时,它就是当前线程。
您应该学习thread_create
,它为新线程设置初始堆栈
。它提供了关于thread_switch应该做什么的提示。其目的是thread_switch使用汇编指令popal和pushal
来恢复和保存所有8个x86寄存器。注意,thread_create在新线程的堆栈上模拟8个被推入寄存器(32字节)。
要在thread_switch中编写程序集,您需要知道C编译器如何在内存中布局struct线程,如下所示:
--------------------
| 4 bytes for state|
-------------------- ----------
| stack size bytes | eip
| for stack | registers(8个)
| | eip
| | registers(8个)
| | ...
-------------------- -----------
| 4 bytes for sp |
-------------------- <--- current_thread
......
......
--------------------
| 4 bytes for state|
--------------------
| stack size bytes |
| for stack |
--------------------
| 4 bytes for sp |
-------------------- <--- next_thread
变量next_thread和current_thread都包含一个struct thread的地址。
要编写current_thread指向的结构的sp字段,应该这样编写程序集:
movl current_thread, %eax
movl %esp, (%eax)
这在current_thread->sp
中保存了%esp。这行得通是因为sp在该结构中的偏移量为0。您可以通过查看uthread.asm
来研究编译器为uthread.c生成的程序集。
主要的难点就是得弄清当前%esp指着哪个栈,指着什么?
很明显,在thread_schedule()里调用的uthread_switch(),所以栈还是current_thread->stack
,而调用uthread_switch()会把返回地址的下一条指令入栈,所以current_thread->sp=ret eip
uthread_switch.S代码:
.globl thread_switch
thread_switch:
/* YOUR CODE HERE */
/* 下面这四条语句是我没有想明白current_thread的结构
先把current_thread->sp取出,此时sp=current_thread->stack-4-32
movl current_thread, %eax
movl (%eax), %esp
// 将当前线程状态保存,那八个寄存器
add $32, %esp
pushal*/
//此时%esp=current_thread->sp
//而C中对thread_switch的调用自动把下一条指令eip入栈了,所以下面只要把寄存器保存
pushal
//将当前线程的esp保存
movl current_thread, %eax
movl %esp, (%eax)
// 使current_thread指向next_thread
movl next_thread, %eax
movl %eax, current_thread
// 恢复next_thread的状态
movl current_thread, %eax
movl (%eax), %esp
popal
// set next_thread to 0
movl $0, next_thread
ret /* pop return address from stack */
要测试代码,可以使用gdb单步执行thread_switch。你可以这样开始:
(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)
甚至在运行uthread之前就可能触发(或不触发)断点。怎么会这样呢?
运行xv6 shell后,键入“uthread”,gdb将在thread_switch处断开。现在你可以输入如下命令来检查uthread的状态:
(gdb) p/x next_thread->sp
$4 = 0x4ae8
(gdb) x/9x next_thread->sp
0x4ae8 : 0x00000000 0x00000000 0x00000000 0x00000000
0x4af8 : 0x00000000 0x00000000 0x00000000 0x00000000
0x4b08 : 0x000000d8
位于next_thread堆栈的顶部地址0xd8是什么?
根据对函数的理解,可以直到0xd8是返回地址(即调用uthread_switch返回后的下一条指令)。而在uthread.asm里可以发现
static void
thread_schedule(void)
{
d6: 89 e5 mov %esp,%ebp
d8: 83 ec 08 sub $0x8,%esp