Skip to content

Latest commit

 

History

History
84 lines (50 loc) · 7.52 KB

11.5-xv6-thread-switching-code.md

File metadata and controls

84 lines (50 loc) · 7.52 KB

11.5 XV6进程切换示例程序

接下来,我们切换到代码并展示一下刚刚介绍的内容。我们先来看一下proc.h中的proc结构体,从结构体中我们可以看到很多之前介绍的内容。

  • 首先是保存了用户空间线程寄存器的trapframe字段
  • 其次是保存了内核线程寄存器的context字段
  • 还有保存了当前进程的内核栈的kstack字段,这是进程在内核中执行时保存函数调用的位置
  • state字段保存了当前进程状态,要么是RUNNING,要么是RUNABLE,要么是SLEEPING等等
  • lock字段保护了很多数据,目前来说至少保护了对于state字段的更新。举个例子,因为有锁的保护,两个CPU的调度器线程不会同时拉取同一个RUNABLE进程并运行它

我接下来会运行一个简单的演示程序,在这个程序中我们会从一个进程切换到另一个。

这个程序中会创建两个进程,两个进程会一直运行。代码首先通过fork创建了一个子进程,然后两个进程都会进入一个死循环,并每隔一段时间生成一个输出表明程序还在运行。但是它们都不会很频繁的打印输出(注,每隔1000000次循环才打印一个输出),并且它们也不会主动出让CPU(注,因为每个进程都执行的是没有sleep的死循环)。所以我们这里有了两个运算密集型进程,并且因为我们接下来启动的XV6只有一个CPU核,它们都运行在同一个CPU上。为了让这两个进程都能运行,有必要让两个进程之间能相互切换。

接下来让我运行spin程序,

你可以看到一直有字符在输出,一个进程在输出“/”,另一个进程在输出""。从输出看,虽然现在XV6只有一个CPU核,但是每隔一会,XV6就在两个进程之间切换。“/”输出了一会之后,定时器中断将CPU切换到另一个进程运行然后又输出“\”一会。所以在这里我们可以看到定时器中断在起作用。

接下来,我在trap.c的devintr函数中的207行设置一个断点,这一行会识别出当前是在响应定时器中断。

之后在gdb中continue。立刻会停在中断的位置,因为定时器中断还是挺频繁的。现在我们可以确认我们在usertrap函数中,并且usertrap函数通过调用devintr函数来处理这里的中断(注,从下图的栈输出可以看出)。

因为devintr函数处理定时器中断的代码基本没有内容,接下来我在gdb中输入finish来从devintr函数中返回到usertrap函数。当我们返回到usertrap函数时,虽然我们刚刚从devintr函数中返回,但是我们期望运行到下面的yield函数。所以我们期望devintr函数返回2。

可以从gdb中看到devintr的确返回的是2。

在yield函数中,当前进程会出让CPU并让另一个进程运行。这个我们稍后再看。现在让我们看一下当定时器中断发生的时候,用户空间进程正在执行什么内容。我在gdb中输入print p来打印名称为p的变量。变量p包含了当前进程的proc结构体。

学生提问:怎么区分不同进程的内核线程?

Robert教授:每一个进程都有一个独立的内核线程。实际上有两件事情可以区分不同进程的内核线程,其中一件是,每个进程都有不同的内核栈,它由proc结构体中的kstack字段所指向;另一件就是,任何内核代码都可以通过调用myproc函数来获取当前CPU正在运行的进程。内核线程可以通过调用这个函数知道自己属于哪个用户进程。myproc函数会使用tp寄存器来获取当前的CPU核的ID,并使用这个ID在一个保存了所有CPU上运行的进程的结构体数组中,找到对应的proc结构体。这就是不同的内核线程区分自己的方法。

我首先会打印p->name来获取进程的名称,

当前进程是spin程序,如预期一样。

当前的进程ID是3,进程切换之后,我们预期进程ID会不一样。

我们还可以通过打印变量p的trapframe字段获取表示用户空间状态的32个寄存器,这些都是我们在Lec06中学过的内容。这里面最有意思的可能是trapframe中保存的用户程序计数器。

我们可以查看spin.asm文件来确定对应地址的指令。

可以看到定时器中断触发时,用户进程正在执行死循环的加1,这符合我们的预期。

(注,以下问答来自课程结束部分,因为相关就移过来了)

学生提问:看起来所有的CPU核要能完成线程切换都需要有一个定时器中断,那如果硬件定时器出现故障了怎么办?

Robert教授:是的,总是需要有一个定时器中断。用户进程的pre-emptive scheduling能工作的原因是,用户进程运行时,中断总是打开的。XV6会确保返回到用户空间时,中断是打开的。这意味着当代码在用户空间执行时,定时器中断总是能发生。在内核中会更加复杂点,因为内核中偶尔会关闭中断,比如当获取锁的时候,中断会被关闭,只有当锁被释放之后中断才会重新打开,所以如果内核中有一些bug导致内核关闭中断之后再也没有打开中断,同时内核中的代码永远也不会释放CPU,那么定时器中断不会发生。但是因为XV6是我们写的,所以它总是会重新打开中断。XV6中的代码如果关闭了中断,它要么过会会重新打开中断,然后内核中定时器中断可以发生并且我们可以从这个内核线程切换走,要么代码会返回到用户空间。我们相信XV6中不会有关闭中断然后还死循环的代码。

同一个学生提问:我的问题是,定时器中断是来自于某个硬件,如果硬件出现故障了呢?

Robert教授:那你的电脑坏了,你要买个新电脑了。这个问题是可能发生的,因为电脑中有上亿的晶体管,有的时候电脑会有问题,但是这超出了内核的管理范围了。所以我们假设计算机可以正常工作。

有的时候软件会尝试弥补硬件的错误,比如通过网络传输packet,总是会带上checksum,这样如果某个网络设备故障导致某个bit反转了,可以通过checksum发现这个问题。但是对于计算机内部的问题,人们倾向于不用软件来尝试弥补硬件的错误。

学生提问:当一个线程结束执行了,比如说在用户空间通过exit系统调用结束线程,同时也会关闭进程的内核线程。那么线程结束之后和下一个定时器中断之间这段时间,CPU仍然会被这个线程占有吗?还是说我们在结束线程的时候会启动一个新的线程?

Robert教授:exit系统调用会出让CPU。尽管我们这节课主要是基于定时器中断来讨论,但是实际上XV6切换线程的绝大部分场景都不是因为定时器中断,比如说一些系统调用在等待一些事件并决定让出CPU。exit系统调用会做各种操作然后调用yield函数来出让CPU,这里的出让并不依赖定时器中断。