多程序與多執行緒(五)--Linux 執行緒模型的比較:LinuxThreads 和 NPTL(轉)
當 Linux 最初開發時,在核心中並不能真正支援執行緒。但是它的確可以通過 clone() 系統呼叫將程序作為可排程的實體。這個呼叫建立了呼叫程序(calling process)的一個拷貝,這個拷貝與呼叫程序共享相同的地址空間。LinuxThreads 專案使用這個呼叫來完全在使用者空間模擬對執行緒的支援。不幸的是,這種方法有一些缺點,尤其是在訊號處理、排程和程序間同步原語方面都存在問題。另外,這個 執行緒模型也不符合 POSIX 的要求。
要改進 LinuxThreads,非常明顯我們需要核心的支援,並且需要重寫執行緒庫。有兩個相互競爭的專案開始來滿足這些要求。一個包括 IBM 的開發人員的團隊開展了 NGPT(Next-Generation POSIX Threads)專案。同時,Red Hat 的一些開發人員開展了 NPTL 專案。NGPT 在 2003 年中期被放棄了,把這個領域完全留給了 NPTL。
儘管從 LinuxThreads 到 NPTL 看起來似乎是一個必然的過程,但是如果您正在為一個歷史悠久的 Linux 發行版維護一些應用程式,並且計劃很快就要進行升級,那麼如何遷移到 NPTL 上就會變成整個移植過程中重要的一個部分。另外,我們可能會希望瞭解二者之間的區別,這樣就可以對自己的應用程式進行設計,使其能夠更好地利用這兩種技 術。
本文詳細介紹了這些執行緒模型分別是在哪些發行版上實現的。
執行緒 將應用程式劃分成一個或多個同時執行的任務。執行緒與傳統的多工程序 之間的區別在於:執行緒共享的是單個程序的狀態資訊,並會直接共享記憶體和其他資源。同一個程序中執行緒之間的上下文切換通常要比程序之間的上下文切換速度更 快。因此,多執行緒程式的優點就是它可以比多程序應用程式的執行速度更快。另外,使用執行緒我們可以實現並行處理。這些相對於基於程序的方法所具有的優點推動 了 LinuxThreads 的實現。
LinuxThreads 最初的設計相信相關程序之間的上下文切換速度很快,因此每個核心執行緒足以處理很多相關的使用者級執行緒。這就導致了一對一 執行緒模型的革命。
讓我們來回顧一下 LinuxThreads 設計細節的一些基本理念:
LinuxThreads 非常出名的一個特性就是管理執行緒(manager thread)。管理執行緒可以滿足以下要求:
- 系統必須能夠響應終止訊號並殺死整個程序。
- 以堆疊形式使用的記憶體回收必須線上程完成之後進行。因此,執行緒無法自行完成這個過程。
- 終止執行緒必須進行等待,這樣它們才不會進入殭屍狀態。
- 執行緒本地資料的回收需要對所有執行緒進行遍歷;這必須由管理執行緒來進行。
- 如果主執行緒需要呼叫 pthread_exit(),那麼這個執行緒就無法結束。主執行緒要進入睡眠狀態,而管理執行緒的工作就是在所有執行緒都被殺死之後來喚醒這個主執行緒。
- 為了維護執行緒本地資料和記憶體,LinuxThreads 使用了程序地址空間的高位記憶體(就在堆疊地址之下)。
- 原語的同步是使用訊號 來實現的。例如,執行緒會一直阻塞,直到被訊號喚醒為止。
- 在克隆系統的最初設計之下,LinuxThreads 將每個執行緒都是作為一個具有惟一程序 ID 的程序實現的。
- 終止訊號可以殺死所有的執行緒。LinuxThreads 接收到終止訊號之後,管理執行緒就會使用相同的訊號殺死所有其他執行緒(程序)。
- 根據 LinuxThreads 的設計,如果一個非同步訊號被髮送了,那麼管理執行緒就會將這個訊號傳送給一個執行緒。如果這個執行緒現在阻塞了這個訊號,那麼這個訊號也就會被掛起。這是因為管理執行緒無法將這個訊號傳送給程序;相反,每個執行緒都是作為一個程序在執行。
- 執行緒之間的排程是由核心排程器來處理的。
LinuxThreads 的設計通常都可以很好地工作;但是在壓力很大的應用程式中,它的效能、可伸縮性和可用性都會存在問題。下面讓我們來看一下 LinuxThreads 設計的一些侷限性:
- 它使用管理執行緒來建立執行緒,並對每個程序所擁有的所有執行緒進行協調。這增加了建立和銷燬執行緒所需要的開銷。
- 由於它是圍繞一個管理執行緒來設計的,因此會導致很多的上下文切換的開銷,這可能會妨礙系統的可伸縮性和效能。
- 由於管理執行緒只能在一個 CPU 上執行,因此所執行的同步操作在 SMP 或 NUMA 系統上可能會產生可伸縮性的問題。
- 由於執行緒的管理方式,以及每個執行緒都使用了一個不同的程序 ID,因此 LinuxThreads 與其他與 POSIX 相關的執行緒庫並不相容。
- 訊號用來實現同步原語,這會影響操作的響應時間。另外,將訊號傳送到主程序的概念也並不存在。因此,這並不遵守 POSIX 中處理訊號的方法。
- LinuxThreads 中對訊號的處理是按照每執行緒的原則建立的,而不是按照每程序的原則建立的,這是因為每個執行緒都有一個獨立的程序 ID。由於訊號被髮送給了一個專用的執行緒,因此訊號是序列化的 —— 也就是說,訊號是透過這個執行緒再傳遞給其他執行緒的。這與 POSIX 標準對執行緒進行並行處理的要求形成了鮮明的對比。例如,在 LinuxThreads 中,通過 kill() 所傳送的訊號被傳遞到一些單獨的執行緒,而不是集中整體進行處理。這意味著如果有執行緒阻塞了這個訊號,那麼 LinuxThreads 就只能對這個執行緒進行排隊,並在執行緒開放這個訊號時在執行處理,而不是像其他沒有阻塞訊號的執行緒中一樣立即處理這個訊號。
- 由於 LinuxThreads 中的每個執行緒都是一個程序,因此使用者和組 ID 的資訊可能對單個程序中的所有執行緒來說都不是通用的。例如,一個多執行緒的 setuid()/setgid() 程序對於不同的執行緒來說可能都是不同的。
- 有一些情況下,所建立的多執行緒核心轉儲中並沒有包含所有的執行緒資訊。同樣,這種行為也是每個執行緒都是一個程序這個事實所導致的結果。如果任何執行緒 發生了問題,我們在系統的核心檔案中只能看到這個執行緒的資訊。不過,這種行為主要適用於早期版本的 LinuxThreads 實現。
- 由於每個執行緒都是一個單獨的程序,因此 /proc 目錄中會充滿眾多的程序項,而這實際上應該是執行緒。
- 由於每個執行緒都是一個程序,因此對每個應用程式只能建立有限數目的執行緒。例如,在 IA32 系統上,可用程序總數 —— 也就是可以建立的執行緒總數 —— 是 4,090。
- 由於計算執行緒本地資料的方法是基於堆疊地址的位置的,因此對於這些資料的訪問速度都很慢。另外一個缺點是使用者無法可信地指定堆疊的大小,因為使用者可能會意外地將堆疊地址對映到本來要為其他目的所使用的區域上了。按需增長(grow on demand) 的概念(也稱為浮動堆疊 的概念)是在 2.4.10 版本的 Linux 核心中實現的。在此之前,LinuxThreads 使用的是固定堆疊。
NPTL,或稱為 Native POSIX Thread Library,是 Linux 執行緒的一個新實現,它克服了 LinuxThreads 的缺點,同時也符合 POSIX 的需求。與 LinuxThreads 相比,它在效能和穩定性方面都提供了重大的改進。與 LinuxThreads 一樣,NPTL 也實現了一對一的模型。
Ulrich Drepper 和 Ingo Molnar 是 Red Hat 參與 NPTL 設計的兩名員工。他們的總體設計目標如下:
- 這個新執行緒庫應該相容 POSIX 標準。
- 這個執行緒實現應該在具有很多處理器的系統上也能很好地工作。
- 為一小段任務建立新執行緒應該具有很低的啟動成本。
- NPTL 執行緒庫應該與 LinuxThreads 是二進位制相容的。注意,為此我們可以使用 LD_ASSUME_KERNEL,這會在本文稍後進行討論。
- 這個新執行緒庫應該可以利用 NUMA 支援的優點。
與 LinuxThreads 相比,NPTL 具有很多優點:
- NPTL 沒有使用管理執行緒。管理執行緒的一些需求,例如向作為程序一部分的所有執行緒傳送終止訊號,是並不需要的;因為核心本身就可以實現這些功能。核心還會處理每個 執行緒堆疊所使用的記憶體的回收工作。它甚至還通過在清除父執行緒之前進行等待,從而實現對所有執行緒結束的管理,這樣可以避免殭屍程序的問題。
- 由於 NPTL 沒有使用管理執行緒,因此其執行緒模型在 NUMA 和 SMP 系統上具有更好的可伸縮性和同步機制。
- 使用 NPTL 執行緒庫與新核心實現,就可以避免使用訊號來對執行緒進行同步了。為了這個目的,NPTL 引入了一種名為futex 的新機制。futex 在共享記憶體區域上進行工作,因此可以在程序之間進行共享,這樣就可以提供程序間 POSIX 同步機制。我們也可以在程序之間共享一個 futex。這種行為使得程序間同步成為可能。實際上,NPTL 包含了一個 PTHREAD_PROCESS_SHARED 巨集,使得開發人員可以讓使用者級程序在不同程序的執行緒之間共享互斥鎖。
- 由於 NPTL 是 POSIX 相容的,因此它對訊號的處理是按照每程序的原則進行的;getpid() 會為所有的執行緒返回相同的程序 ID。例如,如果傳送了 SIGSTOP 訊號,那麼整個程序都會停止;使用 LinuxThreads,只有接收到這個訊號的執行緒才會停止。這樣可以在基於 NPTL 的應用程式上更好地利用偵錯程式,例如 GDB。
- 由於在 NPTL 中所有執行緒都具有一個父程序,因此對父程序彙報的資源使用情況(例如 CPU 和記憶體百分比)都是對整個程序進行統計的,而不是對一個執行緒進行統計的。
- NPTL 執行緒庫所引入的一個實現特性是對 ABI(應用程式二進位制介面)的支援。這幫助實現了與 LinuxThreads 的向後相容性。這個特性是通過使用 LD_ASSUME_KERNEL 實現的,下面就來介紹這個特性。
正如上面介紹的一樣,ABI 的引入使得可以同時支援 NPTL 和 LinuxThreads 模型。基本上來說,這是通過 ld (一個動態連結器/載入器)來進行處理的,它會決定動態連結到哪個執行時執行緒庫上。
舉例來說,下面是 WebSphere® Application Server 對這個變數所使用的一些通用設定;您可以根據自己的需要進行適當的設定:
- LD_ASSUME_KERNEL=2.4.19:這會覆蓋 NPTL 的實現。這種實現通常都表示使用標準的 LinuxThreads 模型,並啟用浮動堆疊的特性。
- LD_ASSUME_KERNEL=2.2.5:這會覆蓋 NPTL 的實現。這種實現通常都表示使用 LinuxThreads 模型,同時使用固定堆疊大小。
我們可以使用下面的命令來設定這個變數:
export LD_ASSUME_KERNEL=2.4.19
注意,對於任何 LD_ASSUME_KERNEL 設定的支援都取決於目前所支援的執行緒庫的 ABI 版本。例如,如果執行緒庫並不支援 2.2.5 版本的 ABI,那麼使用者就不能將 LD_ASSUME_KERNEL 設定為 2.2.5。通常,NPTL 需要 2.4.20,而 LinuxThreads 則需要 2.4.1。
如果您正執行的是一個啟用了 NPTL 的 Linux 發行版,但是應用程式卻是基於 LinuxThreads 模型來設計的,那麼所有這些設定通常都可以使用。
大部分現代 Linux 發行版都預裝了 LinuxThreads 和 NPTL,因此它們提供了一種機制來在二者之間進行切換。要檢視您的系統上正在使用的是哪個執行緒庫,請執行下面的命令:
$ getconf GNU_LIBPTHREAD_VERSION
這會產生類似於下面的輸出結果:
NPTL 0.34
或者:
linuxthreads-0.10
表 1 列出了一些流行的 Linux 發行版,以及它們所採用的執行緒實現的型別、glibc 庫和核心版本。
執行緒實現 | 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 | |
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 版本的核心和 glibc 2.3.3 開始,NPTL 所採用的版本號命名約定發生了變化:這個庫現在是根據所使用的 glibc 的版本進行編號的。
Java™ 虛擬機器(JVM)的支援可能會稍有不同。IBM 的 JVM 可以支援表 1 中 glibc 版本高於 2.1 的大部分發行版。
LinuxThreads 的限制已經在 NPTL 以及 LinuxThreads 後期的一些版本中得到了克服。例如,最新的 LinuxThreads 實現使用了執行緒註冊來定位執行緒本地資料;例如在 Intel® 處理器上,它就使用了 %fs 和 %gs 段暫存器來定位訪問執行緒本地資料所使用的虛擬地址。儘管這個結果展示了 LinuxThreads 所採納的一些修改的改進結果,但是它在更高負載和壓力測試中,依然存在很多問題,因為它過分地依賴於一個管理執行緒,使用它來進行訊號處理等操作。
您應該記住,在使用 LinuxThreads 構建庫時,需要使用 -D_REENTRANT 編譯時標誌。這使得庫執行緒是安全的。
最後,也許是最重要的事情,請記住 LinuxThreads 專案的建立者已經不再積極更新它了,他們認為 NPTL 會取代 LinuxThreads。
LinuxThreads 的缺點並不意味著 NPTL 就沒有錯誤。作為一個面向 SMP 的設計,NPTL 也有一些缺點。我曾經看到過在最近的 Red Hat 核心上出現過這樣的問題:一個簡單執行緒在單處理器的機器上執行良好,但在 SMP 機器上卻掛起了。我相信在 Linux 上還有更多工作要做才能使它具有更好的可伸縮性,從而滿足高階應用程式的需求。
學習
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文 。
- Ulrich Drepper 和 Ingo Molnar 編寫的 “The Native POSIX Thread Library for Linux”(PDF)介紹了設計 NPTL 的原因和目標,其中包括了 LinuxThreads 的缺點和 NPTL 的優點。
- LinuxThreads FAQ 包含了有關 LinuxThreads 和 NPTL 的常見問題。這對於瞭解早期的 LinuxThreads 實現的缺點來說是一個很好的資源。
- Ulrich Drepper 撰寫的 “Explaining LD_ASSUME_KERNEL” 提供了有關這個環境變數的詳細介紹。
- “Native POSIX Threading Library (NPTL) support” 從 WebSphere 的視角介紹了 LinuxThreads 和 NPTL 之間的區別,並解釋了 WebSphere Application Server 如何支援這兩種不同的執行緒模型。
- Diagnosis documentation for IBM ports of the JVM 定義了 Java 應用程式在 Linux 上執行時面臨問題時所要蒐集的診斷資訊。
- 在 developerWorks Linux 專區 中可以找到為 Linux 開發人員準備的更多資源。
- 隨時關注 developerWorks 技術事件和網路廣播。
獲得產品和技術
- LinuxThreads README 對 LinuxThreads 概要進行了介紹。
- 在您的下一個開發專案中採用 IBM 試用軟體,這可以從 developerWorks 上直接下載。
討論
- 通過參與 developerWorks blogs 加入 developerWorks 社群。
Vikram Shukla 具有 6 年使用面嚮物件語言進行開發和設計的經驗,目前是位於印度 Banglore 的 IBM Java Technology Center 的一名資深軟體工程師,負責對 IBM JVM on Linux 進行支援。
轉自:http://blog.chinaunix.net/uid-20556054-id-3068081.html