操作系统用户级线程和内核级线程

802 阅读4分钟

概述

上篇已经了解了操作系统的多进程图像,有的时候我们不需要那么庞大的切换成本,不仅仅要切换指令还需要切换内存映射表,有的时候仅仅只需要切换指令共享内存映射表。这就是线程的概念。就像内存分为内核态和用户态的区别,线程也分为用户级线程和核心级线程

用户级线程

我们知道操作系统通过来实现函数的跳转,当一个方法调用另一个方法的时候会把当前指令的返回地址压入栈中,等调用的方法执行结束后再从栈中获取地址继续执行

image.png 假设两个线程调用状况如图,我们来看看用栈如何表示

  1. A调用B所以B的返回地址104入栈
  2. B调用Yield方法让出自己的CPU使用,让CPU执行C,所以204入栈
  3. C调用D所以C的返回地址304入栈
  4. D中Yield回B所以404入栈

image.png


看似没什么问题,但是在继续B往下执行的时候遇到了B方法的返回,返回就需要弹栈。我们从栈中开始弹会发现弹出了404,所以又跑到了D去执行,这样就错乱了。

TCB

遇到了上面的问题,所以我们用两个栈来保存不同线程的返回地址,然后在切换的时候不仅仅切换CS:IP寄存器,也要切换到当前线程的SS:SP的栈地址。就像不同进程之间要用PCB来保存各个寄存器的数据,我们用TCB这个数据结构来保存不同线程的数据,切换的时候就是切换TCB。

线程的创建

我们在上面的推理中知道了,线程的切换需要用到两个栈和两个TCB来实现。那么创建线程的过程是什么样的呢

void create_tread(A){
    // 1. 创建一个TCB数据结构
    TCB * tcb = malloc();
    // 2. 创建一个单独的栈
    *stack = malloc();
    // 3. 把A的返回地址压栈
    *stack = A
    // 关联当前TCB和栈
    tcb.esp = stack
}

核心级线程

上面说的用户级线程其实就是协程的概念,不用经过内核态,这个看似很好,但是其实有一个严重的问题就是内核感知不到用户态开启了很多个线程。 image.png 如图,当进程1的某一个线程想要访问网卡,所以yield到了线程2想去显示文字内容,但是对于操作系统来说,是内核的进程1想要访问网卡,所以直接操作系统就把CPU分配给了别的进程,等网卡的数据回来了之后才会把CPU分给进程1.这样进程1里面的用户级线程就全部卡死 image.png 核心级线程如图,内核是知道有几个线程的,所以就算线程1调用网卡阻塞了,进程1的其他线程还是可以得到操作系统的分配的

内核栈

知道了用户级线程的弊端之后,我们来考虑如何实现核心级线程。我们先来思考为什么CPU感知不到用户级线程,因为没有内核不知道创建了线程,但是我们的用户程序的代码又是放在用户空间的,所以我们在内核态也创建一个栈,这样操作系统就知道有多个线程的存在,当切换到某个内核的栈的时候,栈中存储的又是用户态的栈的数据,把CPU指向用户态的数据,这样不就又让内核知道有多个进程,又可以在内核中调用用户态的代码了么. image.png

再回首

我们再来看原先的例子 image.png 上面的是用户栈,下面的是内核栈,保存了用户栈的SS:SP,并且记录了用户最后一个进入系统调用时候的地址。再往后就是内核态自己的调用栈地址。当需要发生切换的时候状态如下图 image.png 内核态的栈发生切换,然后再通过另一个线程的栈信息找到用户态的代码继续执行

内核级线程实现

源码看不动了,有空再补吧