这个实验是来自 User Level threads。学习用户级线程可以为内核级线程的实现作准备。
用户级线程是利用用户程序的线程库实现的,这里并没有涉及系统调用,每个线程都有自己的栈。
本节实验我们将实现代码完成线程之间的上下文切换,从而完成一个简单的用户级线程库。
相比于 xv6 源码,我们添加了 uthread.c 和 uthread_switch.S。确保 uthread_switch.S
是以 .S
结尾。并在 Makefile
中添加相应的生成规则(在 _forktest
之后),内容如下:
_uthread: uthread.o uthread_switch.o
$(LD) $(LDFLAGS) -N -e main -Ttext 0 -o _uthread uthread.o uthread_switch.o $(ULIB)
$(OBJDUMP) -S _uthread > uthread.asm
注意是以 tab
缩进,而不是空格。
然后添加 _uthread
到 Makefile
中的 UPROGS
变量中。
运行 xv6,然后在 xv6 shell 中运行 uthread
,接着你会看到如下信息:
~ make CPUS=1 qemu-nox
qemu-system-i386 -nographic -drive file=fs.img,index=1,media=disk,format=raw -drive file=xv6.img,index=0,media=disk,format=raw -smp 1 -m 512
xv6...
cpu0: starting 0
sb: size 1000 nblocks 941 ninodes 200 nlog 30 logstart 2 inodestart 32 bmap start 58
init: starting sh
$ uthread
my thread running
my thread 0x2DA8
my thread running
my thread 0x4DB0
my thread 0x2DA8
my thread 0x4DB0
my thread 0x2DA8
uthread
创建两个线程并在它们之间来回切换,每个线程输出 my thread ...
后让出 CPU 给另一个线程。
在研究 uthread_switch.S
之前,你得理解 uthread.c
是如何使用 thread_switch
。
uthread.c
有两个全局变量:current_thread
和 next_thread
,分别指向一个 thread
结构体。这个结构体有一个给线程使用的栈和一个栈指针(sp
,指向线程的栈顶)。thread_switch
的工作是通过 current_thread
保存当前线程的状态,并通过 next_thread
恢复下一个线程的状态,并让 current_thread
指向 next_thread
所指的地方,因此当 uthread_switch
返回的时候,next_thread
开始运行,也就是 current_thread
。
你应该学习 thread_create
,它给新的线程设置用户栈,这也暗示了你 thread_switch
的功能。thread_switch
使用汇编指令 popal
和 pushal
来恢复和保存 8 个通用 x86
寄存器。注意 thread_create
在栈中模拟了此过程。
为了实现 thread_switch
,你需要知道 C 编译器如何在内存中展开 thread
结构体,如下
--------------------
| 4 bytes for state|
--------------------
| stack size bytes |
| for stack |
--------------------
| 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
分别包含了 thread
结构体的起始地址。
若要写 thread
结构体的 sp
域,你的汇编应该为:
movl current_thread %eax
movl %esp, (%eax)
上述指令将 %esp
保存到 current_thread->sp
,因为 sp
就在 thread
结构体的 0 偏移处。你可以通过编译器编译 uthread.c
时产生了 uthread.asm
学习汇编。
你可以通过 gdb
单步调试你的 thread_switch
,调试步骤如下:
(gdb) symbol-file _uthread
Load new symbol table from "_uthread"? (y or n) y
Reading symbols from _uthread...done.
(gdb) b thread_switch
Breakpoint 1 at 0x2c8: file uthread_switch.S, line 11.
(gdb) c
断点会在你运行 uthread
时触发。然后你可以通过以下指令查看 uthread
的状态
(gdb) p/x next_thread->sp
$1 = 0x4d88
(gdb) x/9x next_thread->sp
0x4d88 <all_thread+16360>: 0x00000000 0x00000000 0x00000000 0x00000000
0x4d98 <all_thread+16376>: 0x00000000 0x00000000 0x00000000 0x00000000
0x4da8 <all_thread+16392>: 0x00000190
什么地址是 0x190
,哪里是 next_thread
的栈顶?
用户级线程在操作系统中有时表现差劲。例如,如果一个用户级线程由于调用系统调用而阻塞,另一个用户级线程也不会运行,因为用户级线程调度器不知道哪个线程被 xv6 内核调出去了。再举一个例子,两个用户级线程不能同时运行在两个处理核上,因为 xv6 调度器意识不到用户级线程的存在,xv6 调度器是以 PCB 进行调度的,用户级线程是公用一个 PCB 的。
本实验的代码不能实现并行执行线程,因为多核处理器有可能同时调用 thread_schedule
,选择同一个线程,导致临界资源竞争问题。
有如下几个方法解决这个问题:
添加锁、条件变量、障碍到你的线程包中。
一个网页浏览器的一次网页访问可以分为以下步骤:
下载线程下载完文本后,切换到文本线程显示文本;显示文本后又回到下载线程下载图片,然后切换到解压线程解压图片,最后切换到图片线程显示图片。
如果没有线程概念,那么文本和图片一起处理,导致前期浏览器一片空白,影响交互。
这些线程共享资源,包括缓冲区,下载线程将数据放到缓冲区,显示线程可以直接读取数据,而不需要切换页表。
文本线程和图片线程公用 IO,因此输出到屏幕都不需要切换 IO。
用户线程和函数调用有什么区别呢?
用户线程体现在并发这个特性,而函数只是顺序执行流的封装。用户线程要主动调用 yield()
释放 CPU 使用权,切换到其他线程,而函数是被调用者调用。
用户线程有自己的栈,而函数调用只是普通的压栈。
用户线程一阻塞,同个进程的全部线程都阻塞。因为阻塞进入了内核,转去了其他进程。