多进程的操作系统中线程的实现一般有这几种:N:1,M:N,1:1,这几种中可能M:N是最复杂的。因为线程的实现可以有很多种方式,可以在用户态实现,并且调度算法也在用户态,这样实现的好处是实现起来简单,不用动内核的代码,这种就是N:1,这样可以在本来只支持进程的系统上实现线程以满足多任务需求,但这样即使一个进程中有多个线程,而且有多个CPU,同一进程的不同线程也不能并行的跑,因为内核是以进程为调度对象的,不能充分利用SMP和多核的性能,所以现在的操作系统中已经很少有这种实现了,OpenBSD 早期就是这种实现,虽然现在OpenBSD还是不支持内核线程,但他对专门对用户态的线程支持做了一些改动,现在OpenBSD是可以支持多线程并行跑的。也可以再内核态实现,即内核的调度单位就是线程,现在的FreeBSD,NetBSD,Solaris 就是这样的实现。当内核调度单位也是线程的时候,就出现一个问题:调度算法是在用户态,还是放到内核?这样就出现了M:N和1:1的问题。M:N其实理论上来讲是一种完美的线程模型,这种模型也有很多种实现,最早的M:N的实现是由Sun在Solaris上做出来的,那时叫不叫Solaris我不清楚啊,有兴趣的可以去查一下,后来NetBSD和FreeBSD也实现了这种模型,但实现后可能发现维护上带来的困难比由此带来的性能上的提升大的多,或者好像是现在的任务跟本就用不了这么强悍的性能,Sun后来放弃了这种模型,改为实现较为简单的1:1的模型,具体为什么Sun会放弃M:N可以参看一本叫《Solaris内核结构》的书,里面好像有详细说为什么,不过我也忘了是在哪儿看的了哈。NetBSD在5.0,即当前最新版中改成了1:1的模型,FreeBSD在也在刚实现了M:N的模型后没多久就改成了1:1。其实关于Unix的一些新的feature,很多都是有Sun先实现,然后社区才参看Sun的实现来实现。
说了这么多该说说代码了,NetBSD中的内核线程叫lwp(light weight process),这个和Solaris的叫法是一样的。应用库叫libpthread分别位于src/sys/kern下和src/lib/libpthread下,他的pthread的大部分功能都直接映射给内核了,调度由内核完成。5.0里的pthread库应该是重写了的,很简单,大部分都直接转到以_lwp_开头的系统调用上去了,比如pthread_create()就最终转到_lwp_create()上去了,当然pthread_create()还有其他的工作要做。其实看代码主要关注这几个方面:fork(),lwp的结构以及线程上下文切换。
fork()在创建了进程之后会创建一个线程,这个线程就是这个进程内核的调度单位,当然如果这个进程同时还创建了很多线程,那么这些线程就是调度单位,内核可以同时在多个CPU上跑同一个进程的多个线程。lwp的结构在src/sys/sys/lwp.h中,对lwp的操作的函数主要在src/sys/kern/kern_lwp.c中实现。上下文切换的实现在sys/kern/kern_synch.c中,函数名叫mi_switch(),在这个函数中如果线程发生了变化,会置一个标志位,在从kernel返回到userspace的时候会检查,如果要返回的线程和当前cpu的地址空间不一样,就进行地址空间的切换,否则不会发生地址空间的切换,因为地址空间切换开销很大。相关的函数是pmap_deactivate(),pmap_activate()和pmap_load(),在前两个函数中置标志位,真正的切换发生在pmap_load()中。
和线程相关的还有同步问题,线程的调度算法问题等,后面会接着介绍。