实现线程包的方式
1. 在用户空间实现
内核对线程包一无所知,从内核角度考虑,就是按正常单线程进程管理。每个进程需要有其专用的线程表,用来跟踪该进程中的线程。这些表和内核中的进程表类似,不过仅仅记录各个线程的属性,如每个线程的程序计数器,堆栈指针、寄存器和状态等,并由运行时系统管理。
优点:
用户级线程包可以在不支持线程的操作系统上实现。线程切换比陷入内核要快一个数量级。因为如果某个线程阻塞,线程表保存该线程的寄存器,然后查看表中可运行的线程,并且把新线程的保存值重新装入机器的寄存器中。只要堆栈指针和程序计数器被切换,新的线程即可立即投入运行。如果机器有一条保存所有寄存器的指令和另一条装入全部寄存器的指令,那么切换可以在几条指令内完成。另外,保存线程状态的过程和调度程序是本地过程,不需要内核调用。不需要陷阱,上下文切换,也不需要对Cache进行刷新。
允许每个进程有自己定制的调度算法。
扩展性:内核空间中内核线程需要一些固定表格空间和堆栈空间,如果数量过大,就会出现问题。
缺点:
阻塞系统调用:假设在没有任何击键之前,一个线程读取键盘,让该线程实际进行该系统调用是不可接受的。因为这会停止所有线程。使用线程的一个目标是,首先要允许每个线程使用阻塞调用,但是还要避免被阻塞的线程影响其他线程。有了阻塞系统调用,这个目标很难实现。
改进:如果某个调用会阻塞,提前通知。例如在Unix中使用select允许调用者通知预期read是否会阻塞。如果有这个调用,那么read就可以被新的操作替代。首先进行select调用,然后只有在不会阻塞的情况下才进行read调用。如果read调用会被阻塞,有关的调用就不进行,代之以运行另一个线程。
如果一个线程引起页面故障(跳转到不在内存上的指令),内核由于不知道有线程存在,通常会把整个进程阻塞,直到磁盘I/O完成为止,尽管其他线程是可以运行的。
如果一个线程开始运行,那么在该进程中其他的线程就不能运行,除非第一个线程自动放弃CPU。在一个单独的进程内部,没有时钟中断,所以不可能用轮转调度。除非某个线程能够按照自己的意志进入运行时系统,否则调度程序没有任何机会。 可能的方法是让运行时系统请求每秒一次的时钟信号(中断),但是这样对程序也是生硬和无序的。不可能总是高频率地发生周期性的时钟中断。有可能扰乱系统使用的时钟,因为线程可能也需要时钟中断。
2.内核中实现
不需要运行环境,每个进程没有自己的线程表。相反,在内核中有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或撤销工作。
优点:
所有能够阻塞线程的调用都以系统调用的形式实现,当一个线程阻塞时,内核根据其选择, 可以运行同一个进程中的另一个线程(若有就绪线程)或者运行另一个进程中的线程。而在用户级线程中,运行环境始终运行自己进程中的线程,直到内核剥夺它的CPU为止。
因为在内核中创建或撤销线程代价较大,某些系统采取的处理方式为,回收线程,当某个线程被撤销时,把它标志为不可运行的,但是内核数据结构没有收到影响,在稍后创建新线程时,就重新启动某个旧线程,节省开销。
缺点:
当一个多线程进程创建新的进程时,新进程是拥有与原进程相同数量的线程还是应该只有一个线程。取决于进程下一步要做什么。如果要启动一个新程序,一个线程或许就可以。如果要继续运行,则应该复制所有线程。实现复杂。
信号:因为信号是发给进程而不是线程的,当一个信号到达时,应该如何决定由哪一个线程处理它。
3. 混合实现
使用内核级线程,一个内核级线程可能对应多个用户级线程
4. 调度程序激活机制
模拟内核线程的功能,但是为线程包提供通常在用户空间中才能实现的更好的性能和更大的灵活性。例如:如果用户线程从事某种系统调用时是安全的,那就不应该进行专门的非阻塞调用或进行提前检查。如果线程阻塞在某个系统调用或页面故障上,只要在同一个进程中有任何就绪的线程,就应该有可能运行其他的线程。
避免了用户空间和内核空间之间的不必要转换。 例如:如果某个线程由于等待另一个线程的工作而阻塞,此时没有理由请求内核,这样就减少了内核-用户转换的开销。用户空间的运行环境可以阻塞同步的线程而另外调度一个新线程。
内核可以给每个进程安排一定数量的虚拟处理器,并且让用户空间运行环境将线程分配到处理器上。当这一机制用在多处理器中时,虚拟处理器可能成为真实CPU。分配给每个进程的虚拟处理器的初始数量是一个,但是该进程可以申请更多的处理器并且在不用的时候退回。内核也可以取回已经分配的虚拟处理器,以便把它们分给需要多处理器的进程。
思路:当内核了解到一个线程被阻塞之后,内核通知该进程的运行时环境,并且在堆栈中以参数形式传递。有问题的线程编号和所发生事件的一个描述。内核通过在一个已知的起始地址启动运行时系统,从而发出了通知,称为上行调用(upcall)。把当前线程标记为阻塞并从就绪表中取出另一个线程,设置其寄存器,然后再启动之。稍后,当内核知道原来的线程又可运行时,内核就又一次上行调用运行时系统,通知它这一事件。此时运行时系统可以立即重启原来被阻塞的线程,或者放入就绪表中稍后运行。
在某个用户线程运行的同时发生一个硬件中断时,被中断的CPU切换进核心态。如果被中断的进程对引起该中断的时间不感兴趣,比如是另一个进程的I/O完成了,那么在中断处理程序结束之后,就把被中断的线程恢复到中断之前的状态。相反,若是该进程对中断感兴趣(如该进程中某个线程所需资源到达),那么被中断的线程就不再启动,代之为挂起被中断的线程。而运行时系统则启动对应的虚拟CPU,此时被中断线程的状态保存在堆栈中。随后,运行时系统决定在该CPU上调度哪个线程:被中断线程,新就绪线程,或第三种选择。
在层次系统结构中,通常n层提供n+1层可调用的特定服务,但是n层不能调用n+1层中的过程,但是上行调用不遵守这个原理。
5. 弹出式线程
一个消息的到达导致系统创建一个处理该消息的线程(传统方法为将进程或线程阻塞在receive系统调用上)
优点:
- 因为是新线程,没有必须存储的寄存器、堆栈等内容。每个线程从全新开始,可以快速创建。
缺点:
- 需要提前计划,哪个进程中的线程线运行。如果系统支持在内核上下文种运行线程,线程就有可能在那里运行。
6.单线程程序多线程化
对于全局变量:
为每个线程赋予其私有的全局变量(全局变量的私有副本)。避免冲突。但是多数程序设计语言只具有表示局部变量和全局变量的方式,而没有中间形式。所以想使用这种方法,有可能为全局变量分配一块内存,并将它传送给线程中的每个过程作为额外的参数。
引入库过程,将特殊存储区上替全局变量分配存储空间。无论该存储空间分配在何处,只有调用线程才可访问其全局变量。如果另一个线程创建了同名的全局变量,由于在不同的存储单元上,所以不会与已有变量冲突。但是有许多库过程不是可重入的。(可重入的意思为程序在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该程序不会出错,即子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结。不可重入即意味着,不同任务调用这个过程时可能修改其他任务调用这个过程的数据,从而导致不可预料的后果)
给每个过程提供一个包装器,该包装器设置一个二进制位从而标志某个库处于使用中。在先前调用还没有完成之前,任何试图使用该库的其他进程都会被阻塞。
信号:
一些信号是线程专用的,但是另外一些并不是,当线程完全在用户空间实现时,内核根本不知道有线程存在,所以很难将信号发送给正确线程。
一些信号不是线程专用,那么哪个线程该捕捉它。指定线程?所有线程?弹出式线程?如果某个线程修改了信号处理程序,是否需要通知其他线程。如果一个线程想捕捉一个特定的信号,而另一个线程却想用这个信号终止进程,会发生什么。
堆栈的管理:当一个进程堆栈溢出时,内核只是自动为该进程提供更多堆栈。当一个进程有多个线程时,就必须有多个堆栈。内核不了解所有堆栈,无法为他们提供增长,直到堆栈出错。内核可能没有意识到内存错误是和某个线程栈的增长有关系的。
参考书目:现代操作系统第三版