1 概述 1.1 線程的定義 傳統的UNIX進程概念在開發有分布式系統中的許多應用時已經顯得力不從心(有時 連簡單的窗口響應問題都很難做好)。這些問題的最好解決之道就是線程,線程推 廣了進程的概念使一個進程可以包含多個活動(或者說執行序列等等)。如今,由 於線程概念的普及,在UNIX系統中已經普遍實現了線程機制,開發並發應用的程序 員現在也可以廣泛接觸到線程的函數庫了。 使用線程的優點在於: A 改進程序的實時響應能力 B 更有效的使用多處理器 C 改進程序結構(多個線程共享同一地址空間,多爽;要是進程就只能通過那些亂 七八糟的IPC來通訊) D 減少對系統資源的使用(線程的切換要比進程快幾個數量級,尤其是用戶態線程) [注意]:有的人將線程和輕量級進程(LWP)視為等同,但其實在不同的系統/實現 中有不同的解釋,LWP更恰當的解釋可能為一個虛擬CPU或內核的線程。它 可以在幫助用戶態線程實現一些特殊的功能。但具體我也不清楚。 1.2 線程的實現 目前線程用兩種方法實現: (1)用戶態線程: 由於內核並沒有對多線程進程的支持,因此,內核中只有單線程進程的概念, 而多線程進程是通過一個和應用程序連接的函數庫實現的。由於內核沒有輕量 級進程(線程)的概念,因此它不能獨立的對之進行調度,而是由一個線程運 行庫來組織線程的調度,其主要工作在於在各個線程的棧之間調度。如果一個 進程中的某一個線程調用了一個阻塞的系統調用,該進程就會被阻塞,當然該 進程中的其他所有線程也同時被阻塞,因此UNIX使用了異步I/O機制。 這種機制主要的缺點在於在一個進程中的多個線程的調度中無法發揮多處理器 的優勢(如上述的阻塞情況)。 其優點包括: A (相對於進程操作而言)某些線程操作的系統消耗大大減少。比如,對屬於 同一個進程的線程之間進行調度切換時不需要調用系統調用,因此將減少額 外的消耗,往往一個進程可以啟動上千個線程也沒有什麼問題。 B 用戶態線程的實現方式可以被定制或修改以適應特殊應用的要求。這對於多 媒體實時過程等尤其有用。另外,用戶態線程可以比核心態線程實現方法的 默認情況支持更多的線程。 (2)核心態線程 這種線程的實現方法允許不同進程中的線程按照同一相對優先調度方法進行調 度。這有利於發揮多處理器的並發優勢。 目前線程主要的實現方法是用戶態線程。有幾個研究項目已經實現了一些核心 態線程的形式。其中比較著名的是MACH分布式操作系統。通過允許用戶代碼對 內核線程調度的參與,該系統將用戶態和核心態兩種線程實現方法的優點結合 了起來。通過提供這樣一個兩級調度機制,內核在保留了對處理器時間分配的 控制的同時,也使一個進程可以充分利用多處理器的優勢。 1.3 線程庫 POSIX線程庫和Solaris線程庫是目前使用最廣泛的線程庫。這兩種實現方法都是可 內操作的(INTER-OPERABLE),它們的功能相似,並可以用在同樣的應用中。但是 只有采用POSIX標准的線程才能確保可以完全的移植到其他符合POSIX標准的環境中 。 兩者相似處: 這兩個庫--libpthread和liBThread--中的大部分函數都是相互對應的。 往往具有 相似後綴名的的POSIX和SOLARIS函數就具有相似的功能、參數個數以及參數的作用 。所有POSIX線程庫的函數都以pthread為前綴,而所有SOLARIS線程庫函數都以thr 為前綴。 兩者不同處: POSIX A 可移植性更好; B 對每一個線程進行配置; C 實現了線程取消; D 強迫調度算法; E 允許clean-up handlers for fork(2)調用; Solaris A 線程可以被掛起和繼續; B 優化的互斥和讀寫鎖定; C 可以提高並發性; D 實現了守護線程,其進程不等待其死亡 1.4 線程標准 現在有三種不同的線程庫的定義,其中每一種都想成為標准:WIN32,OS/2和POSIX 。其中前兩種都是專用的,只能用在它們各自的平台上(WIN32線程僅能運行於 WINNT和WIN9X平台上,OS/2線程運行於OS/2平台上)。POSIX規范(IEEE1003.1c aka Pthreads)則是適用於各種平台,而且已經或正在在所有主要的UNIX系統(包 括Linux)上實現,也包括VMS平台。 POSIX線程: POSIX標准規定了所有線程庫必須符合的標准,包括API和其相應的動作。它是POSIX 擴展的一部分,因此它並不是XPG4標准所必需的,但它是X/OPEN UNIX98所要求的, 而目前所有主要的UNIX生產商都遵從此標准。就在本文寫作時,幾乎所有UNIX生產商 都發布了相應的線程庫。 WIN32線程: 無論NT還是OS/2的實現都存在著和POSIX標准的一些基本性的不同之處--以至於無論 從兩者中任何一者移植到POSIX上都被證明存在相當難度。微軟至今沒有任何采用 POSIX標准的計劃。目前有一些自由共享的WIN32平台上的POSIX庫,在OS/2平台上也 有可供選擇的POSIX庫。 DCE線程: 在POSIX完成它的標准指定工作之前,它發布了一些僅供參考的草案。其中的草案4 就被用作DCE線程庫的基本內容。該草案和最後發布的標准基本相似,但缺少一些重 要的不同。可能是因為現在已經沒有人在為DCE寫代碼了。 SOLARIS線程: 也被稱為UI線程,它是太陽微系統公司在POSIX委員會完成標准指定工作之前開發 SOLARIS2所采用的線程庫。盡管預計如今大部分程序員都將采用POSIX標准,但 SOLARIS線程在一個相當長的時間內還將存在於SOLARIS2系統上。SOLARIS線程和 POSIX線程兩者的決大部分實質上是相同的。 1.5 LINUX線程的思想及特點 在INRIA(位於法國巴黎)的Xavier Leroy,以及Pavel Krauz,Richard Henderson 和另外一些人,已經開發出了LINUX下的一種線程庫,該線程庫實現了一個叫“一對 一 ”模型(One-to-One), 它可以利用多處理器。該庫基於LINUX的一個新的系統 調用--clone()(也就是說它只能用在LINUX上了)。該調用實現於LINUX2.0或以 上版本,並可以運行於Intel,Alpha SPARC,m68k以及MIPS等處理器的機器上。其 一個缺點在於它對信號的非標准處理上。 LinuxThreads采用稱為1-1模型:每個線程實際上在核心是一個個單獨的進程,核心 的調度程序負責線程的調度,就象調度普通進程。線程是用系統調用clone()創建的 ,clone()系統調用是fork()的推廣形式,它允許新進程共享父進程的存儲空間、文 件描述符和信號處理程序。 “一對一”模型的優點在於: A 最小限度消耗的CPU級多處理技術(每個CPU一個線程); B 最小限度消耗的I/O操作; C 一種簡單和強壯的實現(核心調度程序為我們做了大部分艱難的工作)。 該模型主要的缺點在於在互斥和條件操作時的環境切換的系統消耗更大,而該切換 是通過內核來進行的。但是由於LINUX內核對環境切換的處理相當有效,因此在一定 程度上彌補了這個缺點。 除了“一對一”模型之外還有兩種基本模型。 “多對一”模型基於用戶級的調度,線程切換完全由用戶程序完成;從核心角度看, 只有一個進程正在運行。這種模型不是我們所關心的,因為它無法利用多處理器的 優點,而且要用不合理的方法處理I/O操作阻塞。目前LINUX上存在有數個用戶態的 線程庫,但我發現它們在功能、性能以及強壯性上都存在缺陷。 “多對多”模型結合了核心態和用戶態的調度:數個核心態線程並發的執行,每個 都作為一個用戶態線程的調度者對用戶態線程進行調度。大多數商業版本的UNIX( SOLARIS,DIGITAL UNIX和IRIX)都采用此模型實現POSIX線程標准。該模型結合了 “多對一”和“一對一”模型的優點,而且由於它能夠避免另外兩種模型缺陷,尤 其是當內核在處理環境切換時效率較低時,比如DIGITAL UNIX,因而相當具有吸引 力。但不幸的是,這種模型的實現相當復雜,而且需要LINUX內核所不能提供的內核 功能。Linus Torvalds和其它Linux內核開發者一直以來都在全面簡單化的原則下推 動“一對一”模型,並且他們大大的提高了Linux內核線程切換的效率。Linux線程遵 從了他們一直以來的原則。 2 Linux核心對線程的支持 Linux核心對線程的支持主要是通過其系統調用,下文將進行系統的介紹。 2.1 系統調用clone() 以下是系統調用clone的代碼: asmlinkage int sys_clone(strUCt pt_regs regs) { unsigned long clone_flags; unsigned long newsp; clone_flags = regs.ebx; newsp = regs.ecx; if (!newsp) newsp = regs.esp; return do_fork(clone_flags, newsp, ®s); } 與系統調用clone功能相似的系統調用有fork,但fork事實上只是clone的功能的一部 分,clone與fork的主要區別在於傳遞了幾個參數,而當中最重要的參數就是 conle_flags,下表是系統定義的幾個clone_flags標志: 標志 Value 含義 CLONE_VM 0x00000100 置起此標志在進程間共享地址空間 CLONE_FS 0x00000200 置起此標志在進程間共享文件系統信息 CLONE_FILES 0x00000400 置起此標志在進程間共享打開的文件 CLONE_SIGHAND 0x00000800 置起此標志在進程間共享信號處理程序 如果置起以上標志所做的處理分別是: 置起CLONE_VM標志: mmget(current->mm); /* * Set up the LDT descriptor for the clone task. */ copy_segments(nr, tsk, NULL); SET_PAGE_DIR(tsk, current->mm->pgd); 置起CLONE_ FS標志: atomic_inc(¤t->fs->count); 置起CLONE_ FILES標志: atomic_inc(&oldf->count); 置起CLONE_ SIGHAND標志: atomic_inc(¤t->sig->count); 2.2 與線程調度相關的系統調用 以下是glibc-linuxthread用來進行調度的系統調度: .long SYMBOL_NAME(sys_sched_setparam) /* 系統調用154 */ /*用來設置進程(或線程)的調度參數*/ .long SYMBOL_NAME(sys_sched_getparam) /*用來獲取進程(或線程)的調度參數*/ .long SYMBOL_NAME(sys_sched_setscheduler) /*用來設置進程(或線程)的調度參數*/ .long SYMBOL_NAME(sys_sched_getscheduler) /*用來獲取進程(或線程)的調度參數*/ .long SYMBOL_NAME(sys_sched_yield) /*用來強制核心重新調度進程(或線程)*/ .long SYMBOL_NAME(sys_sched_get_priority_max) /*用來設置進程(或線程)的調度參數*/ .long SYMBOL_NAME(sys_sched_get_priority_min) /*用來獲取進程(或線程)的調度參數*/ .long SYMBOL_NAME(sys_sched_rr_get_interval) /* 系統調用161 */ /*用來獲取進程(或線程)的調度時間間隔*/ 3 Linux線程的實現 現在的0.8版LinuxThreads,是迄今為止在Linux下支持threads的最好的 Runtime-library,而包含0.8版LinuxThreads的最好的Runtime-library 是glibc- 2.1,下文所要分析的正是glibc-linuxthreads-2.1。 首先介紹一下0.8版LinuxThreads,它實現了一種BiCapitalized面向 Linux的Posix 1003.1c"pthread"標准接口。LinuxThreads提供核心級線 程即每個線程是一個獨立的UNIX進程,通過調用新的系統調用與其它線程 共享地址空間。線程由核心調度,就象UNIX進程調度一樣。使用它的要求 是:LINUX 版本2.0 或以上(要求有新的clone() 系統調用和新的實時調 度程序)。對於Intel平台:要求有libc 5.2.18或後續版本,推薦使用 5.2.18 或 5.4.12 及其後續版本;5.3.12和5.4.7有問題,也支持glibc 2 ,實際上是支持它的一個特別合適的版本。到目前支持Intel, Alpha, Sparc , Motorola 68k, ARM and MIPS平台,還支持多處理器 4 問題及發展 4.1LinuxThread與POSIX標准 4.1.1 已知的漏洞和局限 A 沒有共享的是它們的進程號和父進程號。根據標准應該相同,但這是我們沒 有實現的(直到clone()的CLONE_PID標志可用)。在最近的內核中(最近的 2.1版本和即將發布的2.2版本),超過32個信號是作為實時信號提供的。當 運行在這些內核上時,LinuxThreads使用兩個保留的實時信號為內部操作用 ,因而留下兩個空余信號SIGUSR1和SIGUSR2給用戶代碼用。線程共享進程號 會使/proc出現問題並且信號可能會錯傳到父進程那裡。現在,實現時使用 了兩個信號SIGUSR1和SIGUSR2,所以用戶不能使用它們。理論上,應該為庫 保留兩個信號,但是大概2.1.60版本以上就沒有這個問題了。 B 線程棧分配在高端存儲空間,在初始進程的堆棧下2M遠處。采用"按需增長" 策略,所以初始化時不會使用很多虛擬空間(現在是4K),如果需要可以增 長到2M。為每個線程保留這麼大的地址空間意味著,在32位體系結構下,每 個進程不能有超過大約1023個線程共存(假設每個用戶進程有2GB地址空間) ,但是這是合理的,因為每個線程使用核心的進程表的一項,而它通常限制 為512項。 C “按需增長”的潛在問題是無法防止用戶映射某些數據到為線程棧保留的2M 地址,這可能導致以後的堆棧擴展失敗。在使用這個庫的時候,應該避免在 固定的地址上映射。 D 信號處理不是與Posix標准完全一致,基於一個事實即線程是可以能夠單獨發 送信號的特殊進程,所以沒有發送一個信號到這個進程(所有線程的集合) 的概念。更精確的說,如下是標准的要求和實現如何滿足它們的:(括號中 是LINUX的具體處理方法) 1-同步信號(比如SIGFPE,是由線程的執行產生的)被傳送到產生它們的線 程那裡(符合標准)。 2-一個致命的異步信號將終止進程中的所有線程。(符合標准。線程處理器 一旦發現某一個線程因一個信號而終止,它將同時終止接收到此信號的其 它線程。) 3-一個異步信號將被送給程序的某一個沒有阻塞該信號的線程(未指定具體 是哪一個)。(不符合標准,信號只會根據進程ID發送給相應的線程,如 果該線程正在阻塞該信號則該信號被阻塞。) 4-信號將被發送給至少一個線程。(符合標准。對於由終端產生或發送給進 程組的信號,它們將被發送給所有符合條件的線程。) E 目前對MIPS支持的實現是基於MIPS ISA II或更高級的處理器的。這些處理器 通過ll/sc指令支持原子操作。而老式的R2000/R3000系列的處理器目前還不 被支持,支持這些老式處理器需要更大的開銷。 F 目前對ARM系列處理器的支持的實現中假設它們都有SWP指令(內存原子交換 登記)。但實際是ARM1和ARM2處理器並不支持此指令。在StrongARM處理器中 ,SWP指令沒有繞過CACHE,因此實現對多處理器的支持將會比較復雜。 4.1.2 進程共享的互斥、條件和信號量 為什麼Linux線程沒有實現進程共享的互斥、條件和信號量? 這是POSIX標准的一個可選部分。可移植的程序在使用此機制之前必需檢查宏 _POSIX_THREAD_PROCESS_SHARED是否存在。 該擴展標准的目的是使不同的進程(也就是說在不同的地址空間中)可以通過 在共享內存(無論是SRV4的共享內存段還是用mmfile()產生的內存文件)中的 互斥、條件和信號量來實現進程間的同步。 在Linux線程中沒有實現此功能的原因是在Linux中互斥、條件和信號並不是獨 立的:它們的等待隊列包含著指向線程描述符連接表的指針,而這些指針只在 特定的地址空間才有效。 Matt Messier和Sean Walton花了相當長的時間來設計一個在進程間共享等待隊 列的適合的機制。我們得到了數個可以將下列三項特點中的兩項結合起來的解決 方案,但沒有一個可以將三者結合起來: * 允許不同UID的進程之間的共享; * 支持取消操作; * 支持pthread_cond_timedwait 由此我們知道進程間共享互斥、條件和信號量需要內核的某些支持(而目前並不 支持)。這也許是Linus Torvalds的直覺“在內核中我們只需要clone()”的失敗 之處之一。 在內核提供對它們的支持之前,你最好使用傳統的進程間通訊方式來同步進程: SYSTEM V信號量和消息隊列,或管道或SOCKETS。 4.2 Linux線程展望 未來的目標是全面的兼容POSIX標准和盡可能實現POSIX標准中可選的功能,並進一步 提高LinuxThreads的效率。我相信這一天很快就會到來。 [email protected] 綜合了一些資料加上自己的一些解釋 引用包括: 〈Write Multi-thread Code Solaris> -- 來自SUN的網站 〈Beyond Multiprocessing ...Multithreading the SunOS Kernel〉 -- J. R. Eykholt, S. R. Kleiman, S. Barton, R. Faulkner, A. Shivalingiah, M. Smith, D. Stein, J. Voll, M. Weeks, D. Williams ? SunSoft, Inc.