Linux 線程模型的比較:LinuxThreads 和 NPTL

基本信息

針對Centos6.5系統,默認使用的是html

getconf GNU_LIBPTHREAD_VERSION  NPTL 2.12java

glic的版本是:glibc-2.12-1.166.el6_7.3.x86_64linux

Non-Uniform Memory Access Architecture(NUMA) 參見:https://www.ibm.com/developerworks/cn/linux/l-numa/web

SMP(對稱多處理器)安全

當 Linux 最初開發時,在內核中並不能真正支持線程。可是它的確能夠經過 clone() 系統調用將進程做爲可調度的實體。這個調用建立了調用進程(calling process)的一個拷貝,這個拷貝與調用進程共享相同的地址空間。LinuxThreads 項目使用這個調用來徹底在用戶空間模擬對線程的支持。不幸的是,這種方法有一些缺點,尤爲是在信號處理、調度和進程間同步原語方面都存在問題。另外,這個線程模型也不符合 POSIX 的要求。網絡

要改進 LinuxThreads,很是明顯咱們須要內核的支持,而且須要重寫線程庫。有兩個相互競爭的項目開始來知足這些要求。一個包括 IBM 的開發人員的團隊開展了 NGPT(Next-Generation POSIX Threads)項目。同時,Red Hat 的一些開發人員開展了 NPTL 項目。NGPT 在 2003 年中期被放棄了,把這個領域徹底留給了 NPTL。多線程

儘管從 LinuxThreads 到 NPTL 看起來彷佛是一個必然的過程,可是若是您正在爲一個歷史悠久的 Linux 發行版維護一些應用程序,而且計劃很快就要進行升級,那麼如何遷移到 NPTL 上就會變成整個移植過程當中重要的一個部分。另外,咱們可能會但願瞭解兩者之間的區別,這樣就能夠對本身的應用程序進行設計,使其可以更好地利用這兩種技術。異步

本文詳細介紹了這些線程模型分別是在哪些發行版上實現的。jsp

LinuxThreads 設計細節

線程 將應用程序劃分紅一個或多個同時運行的任務。線程與傳統的多任務進程 之間的區別在於:線程共享的是單個進程的狀態信息,並會直接共享內存和其餘資源。同一個進程中線程之間的上下文切換一般要比進程之間的上下文切換速度更快。所以,多線程程序的優勢就是它能夠比多進程應用程序的執行速度更快。另外,使用線程咱們能夠實現並行處理。這些相對於基於進程的方法所具備的優勢推進了 LinuxThreads 的實現。性能

LinuxThreads 最初的設計相信相關進程之間的上下文切換速度很快,所以每一個內核線程足以處理不少相關的用戶級線程。這就致使了一對一 線程模型的革命。

讓咱們來回顧一下 LinuxThreads 設計細節的一些基本理念:

  • LinuxThreads 很是出名的一個特性就是管理線程(manager thread)。管理線程能夠知足如下要求:

    • 系統必須可以響應終止信號並殺死整個進程。
    • 以堆棧形式使用的內存回收必須在線程完成以後進行。所以,線程沒法自行完成這個過程。
    • 終止線程必須進行等待,這樣它們纔不會進入殭屍狀態。
    • 線程本地數據的回收須要對全部線程進行遍歷;這必須由管理線程來進行。
    • 若是主線程須要調用 pthread_exit(),那麼這個線程就沒法結束。主線程要進入睡眠狀態,而管理線程的工做就是在全部線程都被殺死以後來喚醒這個主線程。
  • 爲了維護線程本地數據和內存,LinuxThreads 使用了進程地址空間的高位內存(就在堆棧地址之下)。
  • 原語的同步是使用信號 來實現的。例如,線程會一直阻塞,直到被信號喚醒爲止。
  • 在克隆系統的最初設計之下,LinuxThreads 將每一個線程都是做爲一個具備唯一進程 ID 的進程實現的。
  • 終止信號能夠殺死全部的線程。LinuxThreads 接收到終止信號以後,管理線程就會使用相同的信號殺死全部其餘線程(進程)。
  • 根據 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

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 支持的優勢。

NPTL 的優勢

與 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 實現的,下面就來介紹這個特性。

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 模型來設計的,那麼全部這些設置一般均可以使用。

GNU_LIBPTHREAD_VERSION 宏

大部分現代 Linux 發行版都預裝了 LinuxThreads 和 NPTL,所以它們提供了一種機制來在兩者之間進行切換。要查看您的系統上正在使用的是哪一個線程庫,請運行下面的命令:

$ getconf GNU_LIBPTHREAD_VERSION

這會產生相似於下面的輸出結果:

NPTL 0.34

或者:

linuxthreads-0.10

Linux 發行版所使用的線程模型、glibc 版本和內核版本

表 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  
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 上還有更多工做要作才能使它具備更好的可伸縮性,從而知足高端應用程序的需求。

 

參考資料

學習

相關文章
相關標籤/搜索