0%

L10-用户级线程

  • ★★★进程 = 资源 + 指令执行序列。

    进程的切换 = 资源的切换(内存映射表的切换) + 指令执行序列的切换(线程的切换)。

L10 用户级线程

★线程概念的引出

  • ★★★进程 = 资源 + 指令执行序列。

    进程的切换 = 资源的切换(内存映射表的切换) + 指令执行序列的切换(线程的切换)。

  • 由于在进程切换时,都要进行资源的切换和指令执行序列的切换,所以为了提高计算机的速度,将资源与指令执行分开来,可以让一个进程包含一个资源和多个指令执行序列,这样就会存在 “只在进程内部多个指令执行序列之间进行切换而不进行资源切换” 的情况,提高了计算机的速度,这既保留了并发的优点,又避免了进程切换的代价,故将进程内部的多个指令执行序列称为线程

    实质就是映射表不变(映射表不切换)而PC指针变(线程切换)。

线程实例

  • 接下来以一个例子来讲述引入线程概念的好处:

    下面是一个网页加载的过程,如果不采用并发的思想,只是让程序线性执行,那就是:从服务器接收数据 -> 待所有数据接收好以后显示文本和图片,这会造成的问题是:用户刚打开网页时什么也没有,然后等了比较久的时间后文本和图片全都显示出来,用户体验感不好。所以应该让程序并发执行,即:从服务器先接收文本数据 -> 显示文本 -> 从服务器接收图片数据 -> 处理图片 -> 显示图片,这样在用户刚打开网页时,能较快地显示出文本,然后再进一步显示出图片,用户体验感较好。

    该并发执行可以通过进程切换来实现,但没必要,因为进程切换是要涉及到资源的切换的,但在这个例子中,从服务器中接收文本数据的程序和用来显示文本的程序显然可以共用一个内存地址,而不用进一步地址分离,这避免了资源的切换,提升了计算机的速度,即只是指令执行序列之间的切换(线程切换)而不涉及资源的切换,这就是引入了线程的好处。

  • 接下来就要实现这个网页的大致流程:

    由于该讲只是涉及用户级线程,故线程之间的切换是通过Yield这个函数实现的,而Create函数则实现多个线程“并行”(即同时出发执行)

Yield函数
  • 接下来是实现关键函数Yield的具体思路:

    如下图所示,有两个线程,从左边线程的函数A开始执行,函数A调用函数B,同时把104压栈;然后函数B中的Yield函数让CPU去处理右边的线程,执行函数C,同时把204压栈;同理,函数C调用函数D,同时把304压栈;最后函数D调用Yield,同时把404压栈。

    但是,再往下执行却会产生问题,因为此时D函数中的Yield使得CPU去处理左边的线程,即跳到204继续执行,此时就应该把204弹栈,除此之外,在B函数执行完以后,应当把104弹栈,但在D函数调用Yield函数时,栈顶元素却是404,要先弹栈也是先将404弹栈,所以是会产生错误的。

    所以,只有一个栈是不够的,而是要从一个栈到两个栈。那么,在Yield切换前,就要先切换栈,所以应当有一个东西能够指示当前使用的栈,这就用到了CPU中的寄存器esp(extended stack pointer,扩展栈指针寄存器);还应当有东西能够记录每个线程的栈,这就用到了**TCB(Thread Control Block,线程控制块)**。

    切换的核心是TCB(Thread Control Block,线程控制块)和栈的相互配合(TCB中存储了每个线程的栈顶指针)。

    这里去掉 jmp 204 的原因是:当函数D中调用函数Yield后,esp此时指向左边线程的栈的栈顶,通过Yield函数中的 } ,就刚好把204弹栈,使得函数正确地从204往下继续执行。

Created函数
  • 由Yield函数内容可知,两个线程对应两个栈,栈之间的切换涉及三个部分,分别是:线程控制块TCB、栈Stack和栈中的地址。

    线程控制块TCB中存储线程中栈的栈顶指针;栈Stack中存储函数的返回地址;栈中的地址指向函数的返回处。所以Created函数的核心就是用程序做出这三样东西,如下图所示。

★★★总结流程
  • 接下来总结该网页线程切换的流程:

    1. 首先在用户刚进入网页时,通过Created函数创建了接收数据的线程显示数据的线程创建了这两个线程的TCB和栈,通过将栈顶指针存储在TCB中将TCB和栈关联起来,并分别将GetData和Show函数的地址入对应的栈
    2. 然后GetData函数向前执行,当把文本数据接收好后就执行Yield函数,跳到Show函数执行,此时,将GetData中的当前函数的下一步执行地址压栈,并把接收数据线程的栈的当前栈顶指针存储在接收数据线程的TCB中,再让esp等于显示数据线程的栈的栈顶指针,即从GetData函数跳到了Show函数;
    3. 然后Show函数向前执行,当把文本数据显示在网页上以后就执行Yield函数,跳到GetData函数继续执行,此时,将Show中的当前函数的下一步执行地址压栈,并把显示数据线程的栈的当前栈顶指针存储在显示数据线程的TCB中,再让esp等于接收数据线程的栈的栈顶指针,即从Show函数又跳到了GetData函数,然后GetData从之前跳出去的地方继续向前执行;
    4. 同理通过两个线程之间的切换将图片也显示在网页上,最终完成网页的全部显示。

用户级线程的缺点

  • 之所以称上述的线程为用户级线程是因为Yield是用户程序,虽然用户级线程的切换能满足线程切换的要求,但其实是有缺点的,看下面的例子:

    当GetData函数是要通过I/O设备来接收数据时,由于要操作I/O设备,所以要通过操作系统内核,这时接收数据的线程要进行等待,而在操作系统内核看来,就是进程1要等待,所以通过Schedule,内核将进程1切换到进程2,导致进程1中的其他线程不能执行,如果用户的进程足够少,少到只有1个进程,那就相当于操作系统一直在等接收数据线程完成操作I/O设备的操作,内核陷入空转状态,使CPU利用率下降。

  • 为了解决前面用户级线程的问题,就要用到核心级线程,这将在下一章节进行详细介绍。

---------------The End---------------