linux線程實現

前言

前面提到進程和線程的區別,進程是資源分配的基本單位,線程是程序執行的基本單位。線程都屬於某個進程,而同一個進程下的不一樣線程分別有共享和獨享的數據,這裏再列舉一下:linux

同一進程內的全部線程除了共享全局變量外還共享:多線程

  • 進程指令
  • 大多數數據
  • 打開的文件(即描述符)
  • 信號處理函數和信號處置
  • 當前工做目錄
  • 用戶ID和組ID

不過每一個線程有各自的:併發

  • 線程ID
  • 寄存器集合,包括程序計數器和棧指針
  • errno
  • 信號掩碼
  • 優先級

linux是遵循POSIX標準的操做系統,因此linux也須要提供遵循POSIX標準的線程實現。而最初linux系統中的線程機制則是LinuxThreads,在2.6版本以後又增長了NPTL(Native POSIX Thread Library)。函數

內核線程和用戶線程

對於線程的實現機制來講,一般能夠選擇在內核內或者內核外實現,這兩種方式的區別在於線程是在覈內仍是核外調度。核內調度更利於併發使用多處理器的資源,內核能夠將同一個進程的不一樣線程調度到不一樣處理器上執行,當某個線程阻塞時,內核能夠將處理器調度到同一個進程的另外一個線程。而核外調度的上下文切換開銷更低,由於線程的切換不用陷入內核態。性能

進程-線程模型

當內核既支持進程也支持線程時,就能夠實現線程-進程的"多對多"模型,即一個進程的某個線程由核內調度,而同時它也能夠做爲用戶級線程池的調度者,選擇合適的用戶級線程在其空間中運行。這樣既可知足多處理機系統的須要,也能夠最大限度的減少調度開銷。優化

在內核外實現的線程又能夠分爲"一對一"、"多對一"兩種模型,前者用一個內核進程對應一個線程,將線程調度等同於進程調度,交給內核完成,然後者則徹底在覈外實現多線程,調度也在用戶態完成。後者就是前面提到的單純的用戶級線程模型的實現方式,顯然,這種核外的線程調度器實際上只須要完成線程運行棧的切換,調度開銷很是小,但同時由於內核信號都是以進程爲單位的,於是沒法定位到線程,因此這種實現方式不能用於多處理器系統。ui

linux的輕量級進程

linux內核只提供了輕量進程的支持,限制了更高效的線程模型的實現,但linux着重優化了進程的調度開銷,必定程度上也彌補了這一缺陷。目前linux的線程機制都採用的線程-進程"一對一"模型,調度交給內核,而在用戶級實現一個包括信號處理在內的線程管理機制。spa

linux內核在2.0.x版本就已經實現了輕量進程,應用程序能夠經過一個統一的clone系統調用接口,用不一樣的參數指定建立輕量進程仍是普通進程。在內核中,clone調用通過參數傳遞和解釋後會調用do_fork,這個核內函數同時也是forkvfork系統調用的最終實現:操作系統

intdo_fork(unsignedlongclone_flags,unsignedlongstack_start,structpt_regs*regs,unsignedlongstack_size);
複製代碼

在do_fork中,不一樣的clone_flags將致使不一樣的行爲(共享不一樣的資源),下面列舉幾個flag的做用。線程

CLONE_VM 若是do_fork時指定了CLONE_VM開關,建立的輕量級進程的內存空間將會和父進程指向同一個地址,即建立的輕量級進程將與父進程共享內存地址空間。

CLONE_FS

若是do_fork時指定了CLONE_FS開關,對於輕量級進程則會與父進程共享相同的所在文件系統的根目錄和當前目錄信息。也就是說,輕量級進程沒有獨立的文件系統相關的信息,進程中任何一個線程改變當前目錄、根目錄等信息都將直接影響到其餘線程。

CLONE_FILES

若是do_fork時指定了CLONE_FILES開關,建立的輕量級進程與父進程將會共享已經打開的文件。這一共享使得任何線程都能訪問進程所維護的打開文件,對它們的操做會直接反映到進程中的其餘線程。

CLONE_SIGHAND

若是do_fork時指定了CLONE_FILES開關,輕量級進程與父進程將會共享對信號的處理方式。也就是說,子進程與父進程的信號處理方式徹底相同,並且能夠相互更改。

儘管linux支持輕量級進程,但並不能說它就支持內核線程,由於linux的"線程"和"進程"實際上處於一個調度層次,共享一個進程標識符空間,這種限制使得不可能在linux上實現徹底意義上的POSIX線程機制,所以衆多的linux線程庫實現嘗試都只能儘量實現POSIX的絕大部分語義,並在功能上儘量逼近。

LinuxThreads的線程機制

LinuxThreads是linux平臺上使用過的一個線程庫。它所實現的就是基於內核輕量級進程的"一對一"線程模型,一個線程實體對應一個核心輕量級進程,而線程之間的管理在覈外函數庫中實現。對於LinuxThreads,它使用(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND)參數來調用clone建立"線程",表示共享內存、共享文件系統訪問計數、共享文件描述符表,以及共享信號處理方式。

管理線程

LinuxThreads最初的設計相信相關進程之間的上下文切換速度很快,所以每一個內核線程足以處理不少相關的用戶級線程。LinuxThreads很是出名的一個特性就是管理線程(manager thread)。在LinuxThreads中,專門爲每個進程構造了一個管理線程,負責處理線程相關的管理工做。當進程第一次調用pthread_create建立一個線程的時候就會建立並啓動管理線程。

在一個進程空間內,管理線程與其餘線程之間經過一對"管理管道(manager_pipe[2])"來通信,該管道在建立管理線程以前建立,在成功啓動了管理線程以後,管理管道的讀端和寫端分別賦給兩個全局變量__pthread_manager_reader__pthread_manager_request,以後,每一個用戶線程都經過__pthread_manager_request向管理線程發請求,但管理線程自己並無直接使用__pthread_manager_reader,管道的讀端(manager_pipe[0])是做爲__clone()的參數之一傳給管理線程的,管理線程的工做主要就是監聽管道讀端,並對從中取出的請求做出反應。

管理線程在進行一系列初始化工做後,進入while(1)循環。在循環中,線程以2秒爲timeout查詢(__poll())管理管道的讀端。在處理請求前,檢查其父線程是否已退出,若是已退出就退出整個進程。若是有退出的子線程須要清理,則進行清理。而後纔是讀取管道中的請求,根據請求類型執行相應操做(switch-case)。

每一個LinuxThreads線程都同時具備線程id和進程id,其中進程id就是內核所維護的進程號,而線程id則由LinuxThreads分配和維護。

LinuxThreads的侷限性

LinuxThreads的設計一般均可以很好地工做;可是在壓力很大的應用程序中,它的性能、可伸縮性和可用性都會存在問題。下面讓咱們來看一下LinuxThreads設計的一些侷限性:

  • 進程id問題:linux內核並不支持真正意義上的線程,LinuxThreads是用與普通進程具備一樣內核調度視圖的輕量級進程來實現線程支持的。這些輕量級進程擁有獨立的進程id,在進程調度、信號處理、IO等方面享有與普通進程同樣的能力。在源碼閱讀者看來,就是linux內核的clone沒有實現對CLONE_PID參數的支持。按照POSIX定義,同一進程的全部線程應該共享一個進程id和父進程id,這在目前的"一對一"模型下是沒法實現的。
  • 管理線程容易成爲瓶頸,這是這種結構的通病;同時,管理線程又負責用戶線程的清理工做,所以,儘管管理線程已經屏蔽了大部分的信號,但一旦管理線程死亡,用戶線程就不得不手工清理了,並且用戶線程並不知道管理線程的狀態,以後的線程建立等請求將無人處理。
  • 信號用來實現同步原語,這會影響操做的響應時間。另外,將信號發送到主進程的概念也並不存在。所以,這並不遵照POSIX中處理信號的方法。
  • LinuxThreads中對信號的處理是按照每線程的原則創建的,而不是按照每進程的原則創建的,這是由於每一個線程都有一個獨立的進程ID。因爲信號被髮送給了一個專用的線程,所以信號是串行化的——也就是說,信號是透過這個線程再傳遞給其餘線程的。這與POSIX標準對線程進行並行處理的要求造成了鮮明的對比。例如,在LinuxThreads中,經過kill()所發送的信號被傳遞到一些單獨的線程,而不是集中總體進行處理。這意味着若是有線程阻塞了這個信號,那麼LinuxThreads就只能對這個線程進行排隊,並在線程開放這個信號時在執行處理,而不是像其餘沒有阻塞信號的線程中同樣當即處理這個信號。
  • 因爲LinuxThreads中的每一個線程都是一個進程,所以用戶和組ID的信息可能對單個進程中的全部線程來講都不是通用的。例如,一個多線程的setuid()/setgid()進程對於不一樣的線程來講可能都是不一樣的。
  • 因爲每一個線程都是一個單獨的進程,所以/proc目錄中會充滿衆多的進程項,而這實際上應該是線程。
  • 因爲每一個線程都是一個進程,所以對每一個應用程序只能建立有限數目的線程。
  • 因爲計算線程本地數據的方法是基於堆棧地址的位置的,所以對於這些數據的訪問速度都很慢。另一個缺點是用戶沒法可信地指定堆棧的大小,由於用戶可能會意外地將堆棧地址映射到原本要爲其餘目的所使用的區域上了。按需增加(growondemand)的概念(也稱爲浮動堆棧的概念)是在2.4.10版本的linux內核中實現的。在此以前,LinuxThreads使用的是固定堆棧。

NPTL

NPTL(Native POSIX Thread Library)是linux線程的一個新實現,它克服了LinuxThreads的缺點,同時也符合POSIX的需求。與LinuxThreads相比,它在性能和穩定性方面都提供了重大的改進。與LinuxThreads同樣,NPTL也實現了一對一的模型。

NPTL出現的一部分緣由是對LinuxThreads進行改進,它設計目標以下:

  • 這個新線程庫應該兼容POSIX標準。
  • 這個線程實現應該在具備不少處理器的系統上也能很好地工做。
  • 爲一小段任務建立新線程應該具備很低的啓動成本。
  • NPTL線程庫應該與LinuxThreads是二進制兼容的。
  • 這個新線程庫應該能夠利用NUMA支持的優勢。

NPTL的優勢

NPTL總的來講採用了LinuxThreads相似的解決辦法,內核看到的依然是一個進程,新線程是經過clone()系統調用產生的。與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實現的。

futex

futex(Fast Userspace muTexes)意爲快速用戶區互斥,它是linux提供的一種同步(互斥)機制,特色是對於條件的判斷是發生在用戶空間的,在競爭不激烈的狀況下能有更好的性能表現。futex在2.6.x系列穩定版內核中出現。

futex由一塊可以被多個進程共享的內存空間(一個對齊後的整型變量)組成;這個整型變量的值可以經過彙編語言調用CPU提供的原子操做指令來增長或減小,而且一個進程能夠等待直到那個值變成正數。Futex 的操做幾乎所有在用戶空間完成;只有當操做結果不一致從而須要仲裁時,才須要進入操做系統內核空間執行。這種機制容許使用 futex 的鎖定原語有很是高的執行效率:因爲絕大多數的操做並不須要在多個進程之間進行仲裁,因此絕大多數操做均可以在應用程序空間執行,而不須要使用(相對高代價的)內核系統調用。

相關文章
相關標籤/搜索