當(dāng) Linux 最初開發(fā)時(shí),在內(nèi)核中并不能真正支持線程。但是它的確可以通過(guò) clone() 系統(tǒng)調(diào)用將進(jìn)程作為可調(diào)度的實(shí)體。這個(gè)調(diào)用創(chuàng)建了調(diào)用進(jìn)程(calling process)的一個(gè)拷貝,這個(gè)拷貝與調(diào)用進(jìn)程共享相同的地址空間。LinuxThreads 項(xiàng)目使用這個(gè)調(diào)用來(lái)完全在用戶空間模擬對(duì)線程的支持。不幸的是,這種方法有一些缺點(diǎn),尤其是在信號(hào)處理、調(diào)度和進(jìn)程間同步原語(yǔ)方面都存在問題。另外,這個(gè)線程模型也不符合 POSIX 的要求。
要改進(jìn) LinuxThreads,非常明顯我們需要內(nèi)核的支持,并且需要重寫線程庫(kù)。有兩個(gè)相互競(jìng)爭(zhēng)的項(xiàng)目開始來(lái)滿足這些要求。一個(gè)包括 IBM 的開發(fā)人員的團(tuán)隊(duì)開展了 NGPT(Next-Generation POSIX Threads)項(xiàng)目。同時(shí),Red Hat 的一些開發(fā)人員開展了 NPTL 項(xiàng)目。NGPT 在 2003 年中期被放棄了,把這個(gè)領(lǐng)域完全留給了 NPTL。
盡管從 LinuxThreads 到 NPTL 看起來(lái)似乎是一個(gè)必然的過(guò)程,但是如果您正在為一個(gè)歷史悠久的 Linux 發(fā)行版維護(hù)一些應(yīng)用程序,并且計(jì)劃很快就要進(jìn)行升級(jí),那么如何遷移到 NPTL 上就會(huì)變成整個(gè)移植過(guò)程中重要的一個(gè)部分。另外,我們可能會(huì)希望了解二者之間的區(qū)別,這樣就可以對(duì)自己的應(yīng)用程序進(jìn)行設(shè)計(jì),使其能夠更好地利用這兩種技術(shù)。
本文詳細(xì)介紹了這些線程模型分別是在哪些發(fā)行版上實(shí)現(xiàn)的。
LinuxThreads 設(shè)計(jì)細(xì)節(jié)
線程 將應(yīng)用程序劃分成一個(gè)或多個(gè)同時(shí)運(yùn)行的任務(wù)。線程與傳統(tǒng)的多任務(wù)進(jìn)程 之間的區(qū)別在于:線程共享的是單個(gè)進(jìn)程的狀態(tài)信息,并會(huì)直接共享內(nèi)存和其他資源。同一個(gè)進(jìn)程中線程之間的上下文切換通常要比進(jìn)程之間的上下文切換速度更快。因此,多線程程序的優(yōu)點(diǎn)就是它可以比多進(jìn)程應(yīng)用程序的執(zhí)行速度更快。另外,使用線程我們可以實(shí)現(xiàn)并行處理。這些相對(duì)于基于進(jìn)程的方法所具有的優(yōu)點(diǎn)推動(dòng)了 LinuxThreads 的實(shí)現(xiàn)。
LinuxThreads 最初的設(shè)計(jì)相信相關(guān)進(jìn)程之間的上下文切換速度很快,因此每個(gè)內(nèi)核線程足以處理很多相關(guān)的用戶級(jí)線程。這就導(dǎo)致了一對(duì)一 線程模型的革命。
讓我們來(lái)回顧一下 LinuxThreads 設(shè)計(jì)細(xì)節(jié)的一些基本理念:
-
LinuxThreads 非常出名的一個(gè)特性就是管理線程(manager thread)。管理線程可以滿足以下要求:
- 系統(tǒng)必須能夠響應(yīng)終止信號(hào)并殺死整個(gè)進(jìn)程。
- 以堆棧形式使用的內(nèi)存回收必須在線程完成之后進(jìn)行。因此,線程無(wú)法自行完成這個(gè)過(guò)程。
- 終止線程必須進(jìn)行等待,這樣它們才不會(huì)進(jìn)入僵尸狀態(tài)。
- 線程本地?cái)?shù)據(jù)的回收需要對(duì)所有線程進(jìn)行遍歷;這必須由管理線程來(lái)進(jìn)行。
- 如果主線程需要調(diào)用 pthread_exit(),那么這個(gè)線程就無(wú)法結(jié)束。主線程要進(jìn)入睡眠狀態(tài),而管理線程的工作就是在所有線程都被殺死之后來(lái)喚醒這個(gè)主線程。
- 為了維護(hù)線程本地?cái)?shù)據(jù)和內(nèi)存,LinuxThreads 使用了進(jìn)程地址空間的高位內(nèi)存(就在堆棧地址之下)。
- 原語(yǔ)的同步是使用信號(hào) 來(lái)實(shí)現(xiàn)的。例如,線程會(huì)一直阻塞,直到被信號(hào)喚醒為止。
- 在克隆系統(tǒng)的最初設(shè)計(jì)之下,LinuxThreads 將每個(gè)線程都是作為一個(gè)具有惟一進(jìn)程 ID 的進(jìn)程實(shí)現(xiàn)的。
- 終止信號(hào)可以殺死所有的線程。LinuxThreads 接收到終止信號(hào)之后,管理線程就會(huì)使用相同的信號(hào)殺死所有其他線程(進(jìn)程)。
- 根據(jù) LinuxThreads 的設(shè)計(jì),如果一個(gè)異步信號(hào)被發(fā)送了,那么管理線程就會(huì)將這個(gè)信號(hào)發(fā)送給一個(gè)線程。如果這個(gè)線程現(xiàn)在阻塞了這個(gè)信號(hào),那么這個(gè)信號(hào)也就會(huì)被掛起。這是因?yàn)楣芾砭€程無(wú)法將這個(gè)信號(hào)發(fā)送給進(jìn)程;相反,每個(gè)線程都是作為一個(gè)進(jìn)程在執(zhí)行。
- 線程之間的調(diào)度是由內(nèi)核調(diào)度器來(lái)處理的。
LinuxThreads 及其局限性
LinuxThreads 的設(shè)計(jì)通常都可以很好地工作;但是在壓力很大的應(yīng)用程序中,它的性能、可伸縮性和可用性都會(huì)存在問題。下面讓我們來(lái)看一下 LinuxThreads 設(shè)計(jì)的一些局限性:
- 它使用管理線程來(lái)創(chuàng)建線程,并對(duì)每個(gè)進(jìn)程所擁有的所有線程進(jìn)行協(xié)調(diào)。這增加了創(chuàng)建和銷毀線程所需要的開銷。
- 由于它是圍繞一個(gè)管理線程來(lái)設(shè)計(jì)的,因此會(huì)導(dǎo)致很多的上下文切換的開銷,這可能會(huì)妨礙系統(tǒng)的可伸縮性和性能。
- 由于管理線程只能在一個(gè) CPU 上運(yùn)行,因此所執(zhí)行的同步操作在 SMP 或 NUMA 系統(tǒng)上可能會(huì)產(chǎn)生可伸縮性的問題。
- 由于線程的管理方式,以及每個(gè)線程都使用了一個(gè)不同的進(jìn)程 ID,因此 LinuxThreads 與其他與 POSIX 相關(guān)的線程庫(kù)并不兼容。
- 信號(hào)用來(lái)實(shí)現(xiàn)同步原語(yǔ),這會(huì)影響操作的響應(yīng)時(shí)間。另外,將信號(hào)發(fā)送到主進(jìn)程的概念也并不存在。因此,這并不遵守 POSIX 中處理信號(hào)的方法。
- LinuxThreads 中對(duì)信號(hào)的處理是按照每線程的原則建立的,而不是按照每進(jìn)程的原則建立的,這是因?yàn)槊總€(gè)線程都有一個(gè)獨(dú)立的進(jìn)程 ID。由于信號(hào)被發(fā)送給了一個(gè)專用的線程,因此信號(hào)是串行化的 —— 也就是說(shuō),信號(hào)是透過(guò)這個(gè)線程再傳遞給其他線程的。這與 POSIX 標(biāo)準(zhǔn)對(duì)線程進(jìn)行并行處理的要求形成了鮮明的對(duì)比。例如,在 LinuxThreads 中,通過(guò) kill() 所發(fā)送的信號(hào)被傳遞到一些單獨(dú)的線程,而不是集中整體進(jìn)行處理。這意味著如果有線程阻塞了這個(gè)信號(hào),那么 LinuxThreads 就只能對(duì)這個(gè)線程進(jìn)行排隊(duì),并在線程開放這個(gè)信號(hào)時(shí)在執(zhí)行處理,而不是像其他沒有阻塞信號(hào)的線程中一樣立即處理這個(gè)信號(hào)。
- 由于 LinuxThreads 中的每個(gè)線程都是一個(gè)進(jìn)程,因此用戶和組 ID 的信息可能對(duì)單個(gè)進(jìn)程中的所有線程來(lái)說(shuō)都不是通用的。例如,一個(gè)多線程的 setuid()/setgid() 進(jìn)程對(duì)于不同的線程來(lái)說(shuō)可能都是不同的。
- 有一些情況下,所創(chuàng)建的多線程核心轉(zhuǎn)儲(chǔ)中并沒有包含所有的線程信息。同樣,這種行為也是每個(gè)線程都是一個(gè)進(jìn)程這個(gè)事實(shí)所導(dǎo)致的結(jié)果。如果任何線程發(fā)生了問題,我們?cè)谙到y(tǒng)的核心文件中只能看到這個(gè)線程的信息。不過(guò),這種行為主要適用于早期版本的 LinuxThreads 實(shí)現(xiàn)。
- 由于每個(gè)線程都是一個(gè)單獨(dú)的進(jìn)程,因此 /proc 目錄中會(huì)充滿眾多的進(jìn)程項(xiàng),而這實(shí)際上應(yīng)該是線程。
- 由于每個(gè)線程都是一個(gè)進(jìn)程,因此對(duì)每個(gè)應(yīng)用程序只能創(chuàng)建有限數(shù)目的線程。例如,在 IA32 系統(tǒng)上,可用進(jìn)程總數(shù) —— 也就是可以創(chuàng)建的線程總數(shù) —— 是 4,090。
- 由于計(jì)算線程本地?cái)?shù)據(jù)的方法是基于堆棧地址的位置的,因此對(duì)于這些數(shù)據(jù)的訪問速度都很慢。另外一個(gè)缺點(diǎn)是用戶無(wú)法可信地指定堆棧的大小,因?yàn)橛脩艨赡軙?huì)意外地將堆棧地址映射到本來(lái)要為其他目的所使用的區(qū)域上了。按需增長(zhǎng)(grow on demand) 的概念(也稱為浮動(dòng)堆棧 的概念)是在 2.4.10 版本的 Linux 內(nèi)核中實(shí)現(xiàn)的。在此之前,LinuxThreads 使用的是固定堆棧。
關(guān)于 NPTL
NPTL,或稱為 Native POSIX Thread Library,是 Linux 線程的一個(gè)新實(shí)現(xiàn),它克服了 LinuxThreads 的缺點(diǎn),同時(shí)也符合 POSIX 的需求。與 LinuxThreads 相比,它在性能和穩(wěn)定性方面都提供了重大的改進(jìn)。與 LinuxThreads 一樣,NPTL 也實(shí)現(xiàn)了一對(duì)一的模型。
Ulrich Drepper 和 Ingo Molnar 是 Red Hat 參與 NPTL 設(shè)計(jì)的兩名員工。他們的總體設(shè)計(jì)目標(biāo)如下:
- 這個(gè)新線程庫(kù)應(yīng)該兼容 POSIX 標(biāo)準(zhǔn)。
- 這個(gè)線程實(shí)現(xiàn)應(yīng)該在具有很多處理器的系統(tǒng)上也能很好地工作。
- 為一小段任務(wù)創(chuàng)建新線程應(yīng)該具有很低的啟動(dòng)成本。
- NPTL 線程庫(kù)應(yīng)該與 LinuxThreads 是二進(jìn)制兼容的。注意,為此我們可以使用 LD_ASSUME_KERNEL,這會(huì)在本文稍后進(jìn)行討論。
- 這個(gè)新線程庫(kù)應(yīng)該可以利用 NUMA 支持的優(yōu)點(diǎn)。
NPTL 的優(yōu)點(diǎn)
與 LinuxThreads 相比,NPTL 具有很多優(yōu)點(diǎn):
- NPTL 沒有使用管理線程。管理線程的一些需求,例如向作為進(jìn)程一部分的所有線程發(fā)送終止信號(hào),是并不需要的;因?yàn)閮?nèi)核本身就可以實(shí)現(xiàn)這些功能。內(nèi)核還會(huì)處理每個(gè)線程堆棧所使用的內(nèi)存的回收工作。它甚至還通過(guò)在清除父線程之前進(jìn)行等待,從而實(shí)現(xiàn)對(duì)所有線程結(jié)束的管理,這樣可以避免僵尸進(jìn)程的問題。
- 由于 NPTL 沒有使用管理線程,因此其線程模型在 NUMA 和 SMP 系統(tǒng)上具有更好的可伸縮性和同步機(jī)制。
- 使用 NPTL 線程庫(kù)與新內(nèi)核實(shí)現(xiàn),就可以避免使用信號(hào)來(lái)對(duì)線程進(jìn)行同步了。為了這個(gè)目的,NPTL 引入了一種名為 futex 的新機(jī)制。futex 在共享內(nèi)存區(qū)域上進(jìn)行工作,因此可以在進(jìn)程之間進(jìn)行共享,這樣就可以提供進(jìn)程間 POSIX 同步機(jī)制。我們也可以在進(jìn)程之間共享一個(gè) futex。這種行為使得進(jìn)程間同步成為可能。實(shí)際上,NPTL 包含了一個(gè) PTHREAD_PROCESS_SHARED 宏,使得開發(fā)人員可以讓用戶級(jí)進(jìn)程在不同進(jìn)程的線程之間共享互斥鎖。
- 由于 NPTL 是 POSIX 兼容的,因此它對(duì)信號(hào)的處理是按照每進(jìn)程的原則進(jìn)行的;getpid() 會(huì)為所有的線程返回相同的進(jìn)程 ID。例如,如果發(fā)送了 SIGSTOP 信號(hào),那么整個(gè)進(jìn)程都會(huì)停止;使用 LinuxThreads,只有接收到這個(gè)信號(hào)的線程才會(huì)停止。這樣可以在基于 NPTL 的應(yīng)用程序上更好地利用調(diào)試器,例如 GDB。
- 由于在 NPTL 中所有線程都具有一個(gè)父進(jìn)程,因此對(duì)父進(jìn)程匯報(bào)的資源使用情況(例如 CPU 和內(nèi)存百分比)都是對(duì)整個(gè)進(jìn)程進(jìn)行統(tǒng)計(jì)的,而不是對(duì)一個(gè)線程進(jìn)行統(tǒng)計(jì)的。
- NPTL 線程庫(kù)所引入的一個(gè)實(shí)現(xiàn)特性是對(duì) ABI(應(yīng)用程序二進(jìn)制接口)的支持。這幫助實(shí)現(xiàn)了與 LinuxThreads 的向后兼容性。這個(gè)特性是通過(guò)使用 LD_ASSUME_KERNEL 實(shí)現(xiàn)的,下面就來(lái)介紹這個(gè)特性。
LD_ASSUME_KERNEL 環(huán)境變量
正如上面介紹的一樣,ABI 的引入使得可以同時(shí)支持 NPTL 和 LinuxThreads 模型?;旧蟻?lái)說(shuō),這是通過(guò) ld (一個(gè)動(dòng)態(tài)鏈接器/加載器)來(lái)進(jìn)行處理的,它會(huì)決定動(dòng)態(tài)鏈接到哪個(gè)運(yùn)行時(shí)線程庫(kù)上。
舉例來(lái)說(shuō),下面是 WebSphere® Application Server 對(duì)這個(gè)變量所使用的一些通用設(shè)置;您可以根據(jù)自己的需要進(jìn)行適當(dāng)?shù)脑O(shè)置:
- LD_ASSUME_KERNEL=2.4.19:這會(huì)覆蓋 NPTL 的實(shí)現(xiàn)。這種實(shí)現(xiàn)通常都表示使用標(biāo)準(zhǔn)的 LinuxThreads 模型,并啟用浮動(dòng)堆棧的特性。
- LD_ASSUME_KERNEL=2.2.5:這會(huì)覆蓋 NPTL 的實(shí)現(xiàn)。這種實(shí)現(xiàn)通常都表示使用 LinuxThreads 模型,同時(shí)使用固定堆棧大小。
我們可以使用下面的命令來(lái)設(shè)置這個(gè)變量:
export LD_ASSUME_KERNEL=2.4.19
注意,對(duì)于任何 LD_ASSUME_KERNEL 設(shè)置的支持都取決于目前所支持的線程庫(kù)的 ABI 版本。例如,如果線程庫(kù)并不支持 2.2.5 版本的 ABI,那么用戶就不能將 LD_ASSUME_KERNEL 設(shè)置為 2.2.5。通常,NPTL 需要 2.4.20,而 LinuxThreads 則需要 2.4.1。
如果您正運(yùn)行的是一個(gè)啟用了 NPTL 的 Linux 發(fā)行版,但是應(yīng)用程序卻是基于 LinuxThreads 模型來(lái)設(shè)計(jì)的,那么所有這些設(shè)置通常都可以使用。
GNU_LIBPTHREAD_VERSION 宏
大部分現(xiàn)代 Linux 發(fā)行版都預(yù)裝了 LinuxThreads 和 NPTL,因此它們提供了一種機(jī)制來(lái)在二者之間進(jìn)行切換。要查看您的系統(tǒng)上正在使用的是哪個(gè)線程庫(kù),請(qǐng)運(yùn)行下面的命令:
$ getconf GNU_LIBPTHREAD_VERSION
這會(huì)產(chǎn)生類似于下面的輸出結(jié)果:
NPTL 0.34
或者:
linuxthreads-0.10
Linux 發(fā)行版所使用的線程模型、glibc 版本和內(nèi)核版本
表 1 列出了一些流行的 Linux 發(fā)行版,以及它們所采用的線程實(shí)現(xiàn)的類型、glibc 庫(kù)和內(nèi)核版本。
線程實(shí)現(xiàn) | C 庫(kù) | 發(fā)行版 | 內(nèi)核 |
---|---|---|---|
LinuxThreads 0.7, 0.71 (for libc5) | libc 5.x | Red Hat 4.2 | |
LinuxThreads 0.7, 0.71 (for glibc 2) | glibc 2.0.x | Red Hat 5.x | |
LinuxThreads 0.8 | glibc 2.1.1 | Red Hat 6.0 | |
LinuxThreads 0.8 | glibc 2.1.2 | Red Hat 6.1 and 6.2 | |
LinuxThreads 0.9 | Red Hat 7.2 | 2.4.7 | |
LinuxThreads 0.9 | glibc 2.2.4 | Red Hat 2.1 AS | 2.4.9 |
LinuxThreads 0.10 | glibc 2.2.93 | Red Hat 8.0 | 2.4.18 |
NPTL 0.6 | glibc 2.3 | Red Hat 9.0 | 2.4.20 |
NPTL 0.61 | glibc 2.3.2 | Red Hat 3.0 EL | 2.4.21 |
NPTL 2.3.4 | glibc 2.3.4 | Red Hat 4.0 | 2.6.9 |
LinuxThreads 0.9 | glibc 2.2 | SUSE Linux Enterprise Server 7.1 | 2.4.18 |
LinuxThreads 0.9 | glibc 2.2.5 | SUSE Linux Enterprise Server 8 | 2.4.21 |
LinuxThreads 0.9 | glibc 2.2.5 | United Linux | 2.4.21 |
NPTL 2.3.5 | glibc 2.3.3 | SUSE Linux Enterprise Server 9 | 2.6.5 |
注意,從 2.6.x 版本的內(nèi)核和 glibc 2.3.3 開始,NPTL 所采用的版本號(hào)命名約定發(fā)生了變化:這個(gè)庫(kù)現(xiàn)在是根據(jù)所使用的 glibc 的版本進(jìn)行編號(hào)的。
Java™ 虛擬機(jī)(JVM)的支持可能會(huì)稍有不同。IBM 的 JVM 可以支持表 1 中 glibc 版本高于 2.1 的大部分發(fā)行版。
結(jié)束語(yǔ)
LinuxThreads 的限制已經(jīng)在 NPTL 以及 LinuxThreads 后期的一些版本中得到了克服。例如,最新的 LinuxThreads 實(shí)現(xiàn)使用了線程注冊(cè)來(lái)定位線程本地?cái)?shù)據(jù);例如在 Intel® 處理器上,它就使用了 %fs 和 %gs 段寄存器來(lái)定位訪問線程本地?cái)?shù)據(jù)所使用的虛擬地址。盡管這個(gè)結(jié)果展示了 LinuxThreads 所采納的一些修改的改進(jìn)結(jié)果,但是它在更高負(fù)載和壓力測(cè)試中,依然存在很多問題,因?yàn)樗^(guò)分地依賴于一個(gè)管理線程,使用它來(lái)進(jìn)行信號(hào)處理等操作。
您應(yīng)該記住,在使用 LinuxThreads 構(gòu)建庫(kù)時(shí),需要使用 -D_REENTRANT 編譯時(shí)標(biāo)志。這使得庫(kù)線程是安全的。
最后,也許是最重要的事情,請(qǐng)記住 LinuxThreads 項(xiàng)目的創(chuàng)建者已經(jīng)不再積極更新它了,他們認(rèn)為 NPTL 會(huì)取代 LinuxThreads。
LinuxThreads 的缺點(diǎn)并不意味著 NPTL 就沒有錯(cuò)誤。作為一個(gè)面向 SMP 的設(shè)計(jì),NPTL 也有一些缺點(diǎn)。我曾經(jīng)看到過(guò)在最近的 Red Hat 內(nèi)核上出現(xiàn)過(guò)這樣的問題:一個(gè)簡(jiǎn)單線程在單處理器的機(jī)器上運(yùn)行良好,但在 SMP 機(jī)器上卻掛起了。我相信在 Linux 上還有更多工作要做才能使它具有更好的可伸縮性,從而滿足高端應(yīng)用程序的需求。