回想大二刚上操作系统课的时候老师讲了进程和线程的知识,当时模棱两可,写下了一篇疏浅的博客(
点击打开链接
)。现在转眼即将毕业,为了找工作开始系统地复习学过的知识,看了《操作系统真象还原》后,加上这么长时间的知识积累,对线程实现也有了进一步了解。
执行流
操作系统就是一个无限循环,处理器不断取指、执行、取指……就是计算机所做的事情。通过加入中断调用让处理器先执行其他指令再跳转回内核程序执行,来实现各种各样的功能。所谓执行流是处理器执行指令的一段轨迹,也可以说是一段逻辑上独立的指令区域。执行流是独立的,它有自己的栈空间、寄存器映像和内存资源,这就是上下文环境。因此任何大小的代码块都可以独立称为执行流,只要在它在运行的时候提前准备好所需的上下文环境。
在任务调度器的眼中只有执行流才是调度单元,即处理器运行的每个任务都是由调度器分配的执行流。程序中独立的执行流就是进程和线程。而所谓的上下文切换本质上就是改变了处理器中程序计数器的指向,即改变了执行流。
线程
有了前面对执行流的概念理解,线程的理解就容易多了。线程其实是一种机制,它为代码(指令)提供所依赖的上下文环境(寄存器映像、栈等),从而使代码块具有独立性,使之成为调度单元(执行流),从而可以占有处理器资源。引入线程概念后,进程 = 线程 + 资源。对于处理器来说,进程就是执行流集合,里面至少包含一条执行流,执行流之间相互独立,但共用进程的资源,这些执行流就是线程。因此可以说线程是在进程基础上的二次并发,线程把程序更细粒化,一定程度上可以加快程序的执行。
线程的实体是TCB,这在之前的博客也说过。TCB拥有作为一个调度单元需要的绝大部分信息:寄存器映像、栈、状态、优先级、时间片等,具体依操作系统实现而异。因此对线程的实现主要是对于PCB和TCB表的实现与进程/线程调度机制的实现。
线程实现方式
线程的实现有两种——内核级线程和用户级线程。线程是个执行流,在用户空间还是内核空间实现它,最大的区别就是线程表在哪,由谁来调度。如果线程在用户空间中实现,线程表再用户进程中,用户进程就要专门写个线程用作线程调度器来调度进程内部的其他线程。如果线程在内核空间中实现,线程表就在内核中,该线程就由操作系统的调度器同一调度,无论该线程属于内核还是用户进程。
用户级线程
在用户空间中实现线程的好处是可移植性强,由于是用户级的实现,所以在不支持线程的操作系统上也可以运行支持线程的用户程序。因为在用户空间中实现线程,操作系统不需要理解线程,操作系统调度器只会以整个进程的方式调度,处理器的使用权交由进程,然后由进程中的调度器自己去协调分配处理器时间。
在用户进程中实现线程有以下优点:
·线程的调度算法由用户程序自己实现,可以根据实际应用情况修改调度算法。
·将线程的寄存器映像装载到CPU时可以在用户空间完成,即不用陷入到内核态,省去了切换的开销。
用户级线程也有缺点:
·进程中的某个线程若出现了阻塞(通常是由系统调用引起的),操作系统只理解到进程一级,因此会将整个进程挂起,造成该进程中的全部线程都无法运行。对此可以在用户空间中写个封装函数将系统调用封装起来,增加对系统调用是否会造成阻塞的判断,如果不阻塞则允许马上调用,否则待该系统调用不阻塞时再调用(推迟调用)。
·线程未在内核空间中实现,因此对于操作系统来说,操作系统调度器的调度单元是整个进程而不是进程中的线程,所以时钟中断只能影响进程一级的执行流。当时钟中断发生后,操作系统的调度器只能感知到进程一级的调度实体,即只能为进程分配一个处理器资源。这就导致了:如果进程中的某个线程占用了处理器后,只要该线程不让出处理器,进程中的其他线程都没有机会运行。对此需要开发人员“人为”地在线程中调用类似pthread_yield或pthread_exit之类的方式使线程主动让出处理器资源给其所在进程中的其他线程。
·因为操作系统调度器只认识进程,因此给进程的时间片是并没有增加,这有限的一个时间片在进程内还要分配给多个线程,再加上进程内的线程调度器维护线程表、运行调度算法的时间消耗,反而抵消了不用陷入内核而减少的开销,使效率下降。
内核级线程
内核级线程由操作系统原生支持,它有以下优点:
·相比用户级线程,内核级线程相当于让进程多占了时间片。例如一个进程有3个线程,那么由内核实现的线程调度器就会为该进程分配3个时间片,而用户级线程不管有多少线程都只有一个时间片。
·当进程中的某一线程阻塞后,操作系统认识线程,所以调度器只会阻塞这一个线程,此线程所在进程内的其他线程不受影响。
本文部分内容摘自《操作系统真象还原》,有改动