Skip to the content.

0x03 内核线程之间的切换

1. 内核线程切换的例子

编写一个简单的用户程序例子,使用fork创建一个进程,父子两个进程不断在终端输出字符A和字符B,两个进程没有优先顺序,理论上来说无法判断输出的顺序。

// user/spin.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(){
    char c;
    int i = 0;
    int pid = fork();

    if( pid == 0){
        c = 'A';
    }else{
        c = 'B';
    }

    while(1){
        if(i % 10000000L == 0)
            write(1,&c,1);
        i++;
    }
    return 0;
}

运行效果如下:

那么输出A和输出B是两个进程,进程之间在不断地切换,同时也会在进程的内核线程之间进行切换。

2. 内核线程切换

从我们的例子来看,父进程和子进程都没有放弃CPU的操作,但是为啥还是能出现进程之间的切换?因为有 时钟中断的存在,过一小段时间,就会打断用户进程,进入内核空间。

开始进入GDB调式,在trap.c:80 设置一个断点,并执行。 which_dev 是devintr的返回值,是一个设备中断 类型的判断,这里暂时不纠结。 下面将执行 yield 主动放弃 CPU,这里将会发生进程的内核线程切换。

当前进程是 spin,pid=3,然后我们进入 yield 函数,看看发生了什么。 yiled 函数非常简单,修改当前进程 的状态并进入sched()函数。

sched函数也很简单,就是进行 swtch 切换内核线程,切换到 scheduler 线程。 另外,其他的部分要么是条件的判断,要么是设置中断相关内容。

其切换过程如下所示:

这段比较难以理解,为啥两个 swtch 之间会相互切换。 最主要是需要理解内核线程状态,这里定义是 strcut context,只要切换context,就可以切换线程。

所以内核线程切换需要单独保存 sp,s0-s11,还有保存函数返回值的ra寄存器,保存到 struct context 中。 另外swtch函数(kernel/swtch.S)用来切换两个内核线程(swtch函数名,避免与C关键字switch冲突。), 将设计的14个寄存器保存到a0指定的context,恢复a1指定的context。

另外,关于切换过程中的进程锁(p->lock),可以看到 kernel/proc.h 中struct proc的定义,修改p->state 的时候需要持有进程锁。这点本身不难理解,以为有多个CPU在执行调度程序,我们如果修改了进程的状态,但是context还没有进行切换的话,就会出现混乱,这是我们不想出现问题,所以将这些过程用锁来保护起来。

但是,在切换线程的时候,加锁和解锁的过程可能有点特殊。一般情况下,都是那个线程加的锁,哪个线程来解锁,但 这里不是这样的。

3. 过程演示

执行ret之后,因为ra发生了改变,直接切换到了scheduler

切换过来之后,看下当前的进程,pid=3,已加锁。

(gdb) p p->name
$10 = "spin", '\000' <repeats 11 times>
(gdb) p p->pid
$11 = 3
(gdb) p p->lock
$12 = {locked = 1, name = 0x80008248 "proc", cpu = 0x80010b80 <cpus>}

可能有疑惑: 为啥切换到scheduler线程之后,也是pid=3进程。 因为scheduler是先运行的,先从这个位置跳转到了用户进程,后续才有用户进程跳回来。

现在这个进程是加锁的状态,需要解锁再返回用户空间(usertrapret())开始执行新进程代码。

如果不是刚被创建的进程,已经执行过了,那么就会跳转到 sched() 并跳转到 yield(),这时候 已经切换为pid=4的进程,状态如下:

yield解锁该进程,通过usertrapret()返回用户空间,到sret时,看下返回到用户空间哪?

可以看到sepc保存的值是0x2c,返回到用户进程继续执行。