級别: 中級
2006 年 8 月 28 日
LinuxThreads 項目最初将多線程的概念引入了 Linux®,但是 LinuxThreads 并不遵守 POSIX 線程标準。盡管更新的 Native POSIX Thread Library(NPTL)庫填補了一些空白,但是這仍然存在一些問題。本文為那些需要将自己的應用程式從 LinuxThreads 移植到 NPTL 上或者隻是希望了解有何差別的開發人員介紹這兩種 Linux 線程模型之間的差別。
當 Linux 最初開發時,在核心中并不能真正支援線程。但是它的确可以通過 <code>clone()</code> 系統調用将程序作為可排程的實體。這個調用建立了調用程序(calling process)的一個拷貝,這個拷貝與調用程序共享相同的位址空間。LinuxThreads 項目使用這個調用來完全在使用者空間模拟對線程的支援。不幸的是,這種方法有一些缺點,尤其是在信号處理、排程和程序間同步原語方面都存在問題。另外,這個線程模型也不符合 POSIX 的要求。
要改進 LinuxThreads,非常明顯我們需要核心的支援,并且需要重寫線程庫。有兩個互相競争的項目開始來滿足這些要求。一個包括 IBM 的開發人員的團隊開展了 NGPT(Next-Generation POSIX Threads)項目。同時,Red Hat 的一些開發人員開展了 NPTL 項目。NGPT 在 2003 年中期被放棄了,把這個領域完全留給了 NPTL。
盡管從 LinuxThreads 到 NPTL 看起來似乎是一個必然的過程,但是如果您正在為一個曆史悠久的 Linux 發行版維護一些應用程式,并且計劃很快就要進行更新,那麼如何遷移到 NPTL 上就會變成整個移植過程中重要的一個部分。另外,我們可能會希望了解二者之間的差別,這樣就可以對自己的應用程式進行設計,使其能夠更好地利用這兩種技術。
本文詳細介紹了這些線程模型分别是在哪些發行版上實作的。
<a>LinuxThreads 設計細節</a>
線程 将應用程式劃分成一個或多個同時運作的任務。線程與傳統的多任務程序 之間的差別在于:線程共享的是單個程序的狀态資訊,并會直接共享記憶體和其他資源。同一個程序中線程之間的上下文切換通常要比程序之間的上下文切換速度更快。是以,多線程程式的優點就是它可以比多程序應用程式的執行速度更快。另外,使用線程我們可以實作并行處理。這些相對于基于程序的方法所具有的優點推動了 LinuxThreads 的實作。
LinuxThreads 最初的設計相信相關程序之間的上下文切換速度很快,是以每個核心線程足以處理很多相關的使用者級線程。這就導緻了一對一 線程模型的革命。
讓我們來回顧一下 LinuxThreads 設計細節的一些基本理念:
LinuxThreads 非常出名的一個特性就是管理線程(manager thread)。管理線程可以滿足以下要求:
系統必須能夠響應終止信号并殺死整個程序。
以堆棧形式使用的記憶體回收必須線上程完成之後進行。是以,線程無法自行完成這個過程。
終止線程必須進行等待,這樣它們才不會進入僵屍狀态。
線程本地資料的回收需要對所有線程進行周遊;這必須由管理線程來進行。
如果主線程需要調用 <code>pthread_exit()</code>,那麼這個線程就無法結束。主線程要進入睡眠狀态,而管理線程的工作就是在所有線程都被殺死之後來喚醒這個主線程。
為了維護線程本地資料和記憶體,LinuxThreads 使用了程序位址空間的高位記憶體(就在堆棧位址之下)。
原語的同步是使用信号 來實作的。例如,線程會一直阻塞,直到被信号喚醒為止。
在克隆系統的最初設計之下,LinuxThreads 将每個線程都是作為一個具有惟一程序 ID 的程序實作的。
終止信号可以殺死所有的線程。LinuxThreads 接收到終止信号之後,管理線程就會使用相同的信号殺死所有其他線程(程序)。
根據 LinuxThreads 的設計,如果一個異步信号被發送了,那麼管理線程就會将這個信号發送給一個線程。如果這個線程現在阻塞了這個信号,那麼這個信号也就會被挂起。這是因為管理線程無法将這個信号發送給程序;相反,每個線程都是作為一個程序在執行。
線程之間的排程是由核心排程器來處理的。

<a href="http://www.ibm.com/developerworks/cn/linux/l-threading.html#main"><b>回頁首</b></a>
<a>LinuxThreads 及其局限性</a>
LinuxThreads 的設計通常都可以很好地工作;但是在壓力很大的應用程式中,它的性能、可伸縮性和可用性都會存在問題。下面讓我們來看一下 LinuxThreads 設計的一些局限性:
它使用管理線程來建立線程,并對每個程序所擁有的所有線程進行協調。這增加了建立和銷毀線程所需要的開銷。
由于它是圍繞一個管理線程來設計的,是以會導緻很多的上下文切換的開銷,這可能會妨礙系統的可伸縮性和性能。
由于管理線程隻能在一個 CPU 上運作,是以所執行的同步操作在 SMP 或 NUMA 系統上可能會産生可伸縮性的問題。
由于線程的管理方式,以及每個線程都使用了一個不同的程序 ID,是以 LinuxThreads 與其他與 POSIX 相關的線程庫并不相容。
信号用來實作同步原語,這會影響操作的響應時間。另外,将信号發送到主程序的概念也并不存在。是以,這并不遵守 POSIX 中處理信号的方法。
LinuxThreads 中對信号的處理是按照每線程的原則建立的,而不是按照每程序的原則建立的,這是因為每個線程都有一個獨立的程序 ID。由于信号被發送給了一個專用的線程,是以信号是串行化的 —— 也就是說,信号是透過這個線程再傳遞給其他線程的。這與 POSIX 标準對線程進行并行處理的要求形成了鮮明的對比。例如,在 LinuxThreads 中,通過 <code>kill()</code> 所發送的信号被傳遞到一些單獨的線程,而不是集中整體進行處理。這意味着如果有線程阻塞了這個信号,那麼 LinuxThreads 就隻能對這個線程進行排隊,并線上程開放這個信号時在執行處理,而不是像其他沒有阻塞信号的線程中一樣立即處理這個信号。
由于 LinuxThreads 中的每個線程都是一個程序,是以使用者群組 ID 的資訊可能對單個程序中的所有線程來說都不是通用的。例如,一個多線程的 <code>setuid()</code>/<code>setgid()</code> 程序對于不同的線程來說可能都是不同的。
有一些情況下,所建立的多線程核心轉儲中并沒有包含所有的線程資訊。同樣,這種行為也是每個線程都是一個程序這個事實所導緻的結果。如果任何線程發生了問題,我們在系統的核心檔案中隻能看到這個線程的資訊。不過,這種行為主要适用于早期版本的 LinuxThreads 實作。
由于每個線程都是一個單獨的程序,是以 /proc 目錄中會充滿衆多的程序項,而這實際上應該是線程。
由于每個線程都是一個程序,是以對每個應用程式隻能建立有限數目的線程。例如,在 IA32 系統上,可用程序總數 —— 也就是可以建立的線程總數 —— 是 4,090。
由于計算線程本地資料的方法是基于堆棧位址的位置的,是以對于這些資料的通路速度都很慢。另外一個缺點是使用者無法可信地指定堆棧的大小,因為使用者可能會意外地将堆棧位址映射到本來要為其他目的所使用的區域上了。按需增長(grow on demand) 的概念(也稱為浮動堆棧 的概念)是在 2.4.10 版本的 Linux 核心中實作的。在此之前,LinuxThreads 使用的是固定堆棧。

<a>關于 NPTL</a>
NPTL,或稱為 Native POSIX Thread Library,是 Linux 線程的一個新實作,它克服了 LinuxThreads 的缺點,同時也符合 POSIX 的需求。與 LinuxThreads 相比,它在性能和穩定性方面都提供了重大的改進。與 LinuxThreads 一樣,NPTL 也實作了一對一的模型。
Ulrich Drepper 和 Ingo Molnar 是 Red Hat 參與 NPTL 設計的兩名員工。他們的總體設計目标如下:
這個新線程庫應該相容 POSIX 标準。
這個線程實作應該在具有很多處理器的系統上也能很好地工作。
為一小段任務建立新線程應該具有很低的啟動成本。
NPTL 線程庫應該與 LinuxThreads 是二進制相容的。注意,為此我們可以使用 <code>LD_ASSUME_KERNEL</code>,這會在本文稍後進行讨論。
這個新線程庫應該可以利用 NUMA 支援的優點。

<a>NPTL 的優點</a>
與 LinuxThreads 相比,NPTL 具有很多優點:
NPTL 沒有使用管理線程。管理線程的一些需求,例如向作為程序一部分的所有線程發送終止信号,是并不需要的;因為核心本身就可以實作這些功能。核心還會處理每個線程堆棧所使用的記憶體的回收工作。它甚至還通過在清除父線程之前進行等待,進而實作對所有線程結束的管理,這樣可以避免僵屍程序的問題。
由于 NPTL 沒有使用管理線程,是以其線程模型在 NUMA 和 SMP 系統上具有更好的可伸縮性和同步機制。
使用 NPTL 線程庫與新核心實作,就可以避免使用信号來對線程進行同步了。為了這個目的,NPTL 引入了一種名為 futex 的新機制。futex 在共享記憶體區域上進行工作,是以可以在程序之間進行共享,這樣就可以提供程序間 POSIX 同步機制。我們也可以在程序之間共享一個 futex。這種行為使得程序間同步成為可能。實際上,NPTL 包含了一個 <code>PTHREAD_PROCESS_SHARED</code> 宏,使得開發人員可以讓使用者級程序在不同程序的線程之間共享互斥鎖。
由于 NPTL 是 POSIX 相容的,是以它對信号的處理是按照每程序的原則進行的;<code>getpid()</code> 會為所有的線程傳回相同的程序 ID。例如,如果發送了 <code>SIGSTOP</code> 信号,那麼整個程序都會停止;使用 LinuxThreads,隻有接收到這個信号的線程才會停止。這樣可以在基于 NPTL 的應用程式上更好地利用調試器,例如 GDB。
由于在 NPTL 中所有線程都具有一個父程序,是以對父程序彙報的資源使用情況(例如 CPU 和記憶體百分比)都是對整個程序進行統計的,而不是對一個線程進行統計的。
NPTL 線程庫所引入的一個實作特性是對 ABI(應用程式二進制接口)的支援。這幫助實作了與 LinuxThreads 的向後相容性。這個特性是通過使用 <code>LD_ASSUME_KERNEL</code> 實作的,下面就來介紹這個特性。

<a>LD_ASSUME_KERNEL 環境變量</a>
正如上面介紹的一樣,ABI 的引入使得可以同時支援 NPTL 和 LinuxThreads 模型。基本上來說,這是通過 ld (一個動态連結器/加載器)來進行處理的,它會決定動态連結到哪個運作時線程庫上。
舉例來說,下面是 WebSphere® Application Server 對這個變量所使用的一些通用設定;您可以根據自己的需要進行适當的設定:
<code>LD_ASSUME_KERNEL=2.4.19</code>:這會覆寫 NPTL 的實作。這種實作通常都表示使用标準的 LinuxThreads 模型,并啟用浮動堆棧的特性。
<code>LD_ASSUME_KERNEL=2.2.5</code>:這會覆寫 NPTL 的實作。這種實作通常都表示使用 LinuxThreads 模型,同時使用固定堆棧大小。
我們可以使用下面的指令來設定這個變量:
<code>export LD_ASSUME_KERNEL=2.4.19</code>
注意,對于任何 <code>LD_ASSUME_KERNEL</code> 設定的支援都取決于目前所支援的線程庫的 ABI 版本。例如,如果線程庫并不支援 2.2.5 版本的 ABI,那麼使用者就不能将 <code>LD_ASSUME_KERNEL</code> 設定為 2.2.5。通常,NPTL 需要 2.4.20,而 LinuxThreads 則需要 2.4.1。
如果您正運作的是一個啟用了 NPTL 的 Linux 發行版,但是應用程式卻是基于 LinuxThreads 模型來設計的,那麼所有這些設定通常都可以使用。

<a>GNU_LIBPTHREAD_VERSION 宏</a>
大部分現代 Linux 發行版都預裝了 LinuxThreads 和 NPTL,是以它們提供了一種機制來在二者之間進行切換。要檢視您的系統上正在使用的是哪個線程庫,請運作下面的指令:
<code>$ getconf GNU_LIBPTHREAD_VERSION</code>
這會産生類似于下面的輸出結果:
<code>NPTL 0.34</code>
或者:
<code>linuxthreads-0.10</code>

<a>Linux 發行版所使用的線程模型、glibc 版本和核心版本</a>
表 1 列出了一些流行的 Linux 發行版,以及它們所采用的線程實作的類型、glibc 庫和核心版本。
表 1. Linux 發行版及其線程實作
線程實作
C 庫
發行版
核心
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
glibc 2.1.2
Red Hat 6.1 and 6.2
LinuxThreads 0.9
Red Hat 7.2
2.4.7
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
glibc 2.2
SUSE Linux Enterprise Server 7.1
glibc 2.2.5
SUSE Linux Enterprise Server 8
United Linux
NPTL 2.3.5
glibc 2.3.3
SUSE Linux Enterprise Server 9
2.6.5
注意,從 2.6.x 版本的核心和 glibc 2.3.3 開始,NPTL 所采用的版本号命名約定發生了變化:這個庫現在是根據所使用的 glibc 的版本進行編号的。
Java™ 虛拟機(JVM)的支援可能會稍有不同。IBM 的 JVM 可以支援表 1 中 glibc 版本高于 2.1 的大部分發行版。

<a>結束語</a>
LinuxThreads 的限制已經在 NPTL 以及 LinuxThreads 後期的一些版本中得到了克服。例如,最新的 LinuxThreads 實作使用了線程注冊來定位線程本地資料;例如在 Intel® 處理器上,它就使用了 <code>%fs</code> 和 <code>%gs</code> 段寄存器來定位通路線程本地資料所使用的虛拟位址。盡管這個結果展示了 LinuxThreads 所采納的一些修改的改進結果,但是它在更高負載和壓力測試中,依然存在很多問題,因為它過分地依賴于一個管理線程,使用它來進行信号處理等操作。
您應該記住,在使用 LinuxThreads 建構庫時,需要使用 <code>-D_REENTRANT</code> 編譯時标志。這使得庫線程是安全的。
最後,也許是最重要的事情,請記住 LinuxThreads 項目的建立者已經不再積極更新它了,他們認為 NPTL 會取代 LinuxThreads。
LinuxThreads 的缺點并不意味着 NPTL 就沒有錯誤。作為一個面向 SMP 的設計,NPTL 也有一些缺點。我曾經看到過在最近的 Red Hat 核心上出現過這樣的問題:一個簡單線程在單處理器的機器上運作良好,但在 SMP 機器上卻挂起了。我相信在 Linux 上還有更多工作要做才能使它具有更好的可伸縮性,進而滿足高端應用程式的需求。
<a>參考資料</a>
<b>學習</b>
<b>獲得産品和技術</b>
<b>讨論</b>
<a>關于作者</a>
Vikram Shukla 具有 6 年使用面向對象語言進行開發和設計的經驗,目前是位于印度 Banglore 的 IBM Java Technology Center 的一名資深軟體工程師,負責對 IBM JVM on Linux 進行支援。