1 对多核处理器以及进程、线程的困惑
虽然不记得在哪个文献上看到过,但确实记得看过类似的表述:对多核处理器,同一时间只能运行一个进程里的多个线程。一直没有深究过这句话的对错,直到看到linux的进程、线程模型,才对这句话产生了怀疑。在表述清楚我的困惑之前,先简单的介绍一下进程、线程以及linux中的进程与线程。
首先,进程的内含通常被认为是运行中的程序及相关资源的总和,而线程被包含在进程之中,是操作系统能够进行运算调度的最小单位,大部分情况下,它是进程中的实际运作单位[1]。这样说或许非常抽象,不妨借助图像理解。对于含有多线程的进程来说,进程与线程的关系如下图所示:
不难看出,进程持有其所有线程共享的公共资源,而不同线程都保存有自己的寄存器备份以及栈等内容,也就是说处理器的某个核心上各个寄存器的值来自于一个线程,在线程被切换出去时这些寄存器信息都会被保存回线程里,而被调入的线程所保存的寄存器信息将被载入。 这说明真正在CPU上运行的是进程中一个个的线程,所以才将线程称为进程中的实际运作单位。再打个比方,比如一个家,有房子有家具,家里生活了一家人,这个家的物理实体(进程)被这一家人共享,而每天真正干活的是家里的每个人(线程),房子家具是不会干活的。好了,进程、线程就介绍到这里。下面介绍一下linux的进程、线程。
各种操作系统教材上往往会对进程、线程做出泾渭分明的定义或说明,也确实有操作系统实现了清晰的进程、线程抽象,但linux没有。linux没有为进程(process)和线程(thread)分别做抽象,而是使用一个名为task_struct
的结构来描述调度的一个单元。对于那些进程拥有的公共资源,比如地址空间、打开的文件、信号等,linux分别使用相应的对象(结构体)来描述它们,例如描述进程地址空间的mm_struct。在task_struct中,不会完整的保存描述公共资源的对象,仅维护一个指向这些对象的指针。这样一来,假如两个task_struct中相应指针指向了同一个描述公共资源的对象实例,那么就说明这两个task_struct共享该公共资源,比如共享地址空间,共享打开的文件等。具体共享哪些东西是可以控制的,当我们使用clone系统调用去创建一个task_struct时,可以通过传参告诉内核新创建的任务与当前任务共享哪些资源。那么如何用task_struct去体现进程和线程呢?不难想到,假如两个task_struct不共享任何公共资源,它们就被视为两个进程;相反,如果两个task_struct共享所有公共资源,它们就被视为一个进程下的两个线程。事实上,linux中,用fork创建进程、用pthread_create创建线程,其内部都是通过调用clone,并为clone传递不共享/共享公共资源的参数来实现的,如下图:
铺垫结束,下面来说说我的困惑所在。如果对多核处理器,同一时间只能运行一个进程里的多个线程这个说法成立的话,对于进程、线程抽象清晰的操作系统,这个场景勉强还能想象:OS的调度器工作在两个层次,以进程为单位切换,调度一个进程内的多个线程到各个core上执行。而linux这样对进程、线程没有清晰的抽象,内核面对的是位于各个core的调度队列上的一个个的task_struct,其调度也是以task_struct为单位的,而这些task_struct可能共享公共资源(线程),也可能不共享公共资源(进程),难道在调度的时候各个核之间还要同步,确保调度的都是共享公共资源(特别是地址空间)的task_struct?这场景,怎么想怎么别扭!
于是我开始怀疑对多核处理器,同一时间只能运行一个进程里的多个线程这个说法是错误的,下面就要找证据证明。那么,从什么方向入手才能查找到有针对性的资料呢?我是这样考虑的,假如对多核处理器,同一时间只能运行一个进程里的多个线程成立,那么制约同一时间运行不同进程的多个线程的要素是什么呢?不难想到是MMU,MMU可谓是连接虚拟地址和物理地址的桥梁,MMU+页表=虚拟地址空间
,而进程、线程之间的重要区别就是是否共享共享地址空间。假如,一个多核处理器,所有core都共享一个MMU的话,那么同一时间确实只能运行一个进程里的多个线程。
因此,最关键的问题来了,MMU是被共享的么
?
2 多核处理器上的MMU和TLB
接着上文,为了弄清楚MMU是否是被共享的,我对这个问题做了些搜索,并在stackoverflow上发现,有人讨论过这个问题:Do multi-core CPUs share the MMU and page tables?有兴趣的同学可以自己去看看里面的回答。这里我给出答案,MMU通常不是共享的(我不确定对所有处理器成立)。不妨看一下酷睿i7的存储系统框图:
为了进一步佐证多核处理器可以同时运行不同进程的多个线程,我在《Understanding Linux Kernel》中找到这么一段话:
In a multiprocessor system, each CPU has its own TLB, called the local TLB of the CPU. Contrary to the hardware cache, the corresponding entries of the TLB need not be synchronized, because processes running on the existing CPUs may associate the same linear address with different physical ones.
翻译如下:
在多处理器系统中,每个CPU拥有自己的TLB(译者补:快表,专门用于页表的Cache)称为本地TLB。不同于硬件Cache,(多个)TLB的表项之间不需要做同步,因为运行在各个CPU上的进程可能将同一个虚拟地址映射到不同的物理地址处(译者补:说明不共享地址空间)。
当然,可能会有人指出多处理器和多核之间是有差异的,但两者之间也有很多共同之处。多核中,每个核都拥有自己的MMU,它们也可以像多处理器系统那样,每个MMU都联系到独立的页表,这样同一时间,不同的虚拟地址空间可以并存,因此也就支持同时运行不同进程的多个线程。
需要指出的是,虽然不同进程之间通过维护各自的页表使得虚拟地址空间相互独立(除了少数情况,如共享内存),但那仅限于用户部分,而内核部分的地址映射对于所有进程都是相同的。也就是说,页表中内核部分的映射对于所有进程来说都是相同的。如下图:
在结束这个问题的讨论之前,不妨分析一下linux的进程核线程的开销大小。我将从创建、切换、通信这三个方面做简要分析:
- 创建
在linux中,创建进程或线程本质上都是在内核空间建立一个task_struct结构,但着不意味着进程、线程拥有同样的开销。创建线程只需要和当前的task_struct共享资源即可;而创建进程时,不仅要建立一个task_struct结构,还需要复制当前task_struct的公共资源。虽然linux有写时复制的机制,但这只是延后复制,而非不复制,债是要还的,晚一些也是要还的。因此就创建方面来说,进程的开销大于线程的开销。 - 切换
linux切换的都是task_struct,但被换出的task_struct和换入的task_struct是否共享公共资源,这对切换的开销有不小的影响。假如两者不共享公共资源(进程切换),那么当前core的Cache和TLB都会失效,换入的进程在运行初期,Cache和TLB尚未有效建立时,运行会比较慢。而有效建立Cache和TLB是需要时间的,考虑到调度往往以毫秒为单位,因此这个时间是无法忽略的。假如两者共享公共资源(线程切换),这就意味着两者共享虚拟地址空间,也就无需失效Cache和TLB。当然,考虑到两个线程运行的指令、访问的数据可能会有部分位于不同的地址处,因此换入的线程在运行初期,可能会有较多的Cache和TLB未命中,但这比起进程切换需要失效Cache和TLB要好得多。所以,就切换方面来说,进程的开销大于线程的开销。 - 通信
由于进程不共用地址空间,因此进程间实现通信要麻烦一些,代价也大一些;而线程间共享地址空间,最简单的,使用全局变量就可以实现通信(尽管有些时候这种同步方式不一定恰当)。因此,进程间通信开销大于线程间通信开销。
综上,进程的开销是大于线程的开销的。但值得指出的是,并不能因为线程开销小而把它当作万金油。进程也有着地址空间隔离,相互影响小的优点。总的来说,进程、线程各有其适用的场景,应当恰当的适用它们。
参考文献
[1] 维基百科——线程
[2] Do multi-core CPUs share the MMU and page tables?
[3] Understanding Linux Kernel