POSIX線程
POSIX(Portable Operating System Interface Threads,Pthread)是基于POSIX標準的線程編程接口,它使程序能夠在多處理器或多核系統上并行處理多個任務,有效地提高了執行效率和系統響應速度。與單線程應用程序相比,POSIX線程支持更高效的資源共享和任務并行性,其強大的可移植性允許跨平臺應用程序并易于在不同操作系統之間移植。
POSIX線程經歷了一個重要的發展階段。最初,POSIX標準作為統一UNIX系統中多線程的國際標準,旨在提供一種跨平臺的線程支持方案。隨后,Linux系統使用LinuxThreads來實現線程管理,但這種方案在信號處理和線程同步方面存在局限性。為了解決這些問題并進一步提高性能和標準之間的兼容性,開發人員后來引入了NPTL(原生POSIX線程庫)。NPTL不僅顯著提高了性能,而且在遵循POSIX標準方面更加嚴格,成為GNU C庫的核心組件,極大地優化了Linux系統的多線程處理能力。
POSIX線程管理涉及精細的屬性設置,包括優先級調整、堆棧大小控制、線程調度、線程同步和互斥。在這些功能的支持下,POSIX線程可以廣泛應用于各種需要高實時性的領域,例如實時測量設備的協同操作和自動處理、GPS實時數據處理、超聲數據成像和實時采集。與微軟的Win32線程相比,POSIX線程遵守國際開放標準,具有更高的可用性和可移植性。與Java線程相比,它在底層提供了更精細的線程控制,特別適合C和C++環境,滿足了高實時性的需求。
起源發展
POSIX標準的起源
1985年,IEEE著手為類Unix系統制定統一的接口標準。最初,項目組考慮使用“IEEEIX”作為標準名稱,但這一名稱被認為難以普及,可能會引起混淆。這時,理查德·斯托爾曼(Richard Stallman)提議使用“POSIX”作為標準名稱。他將“便攜操作系統”的首字母與后綴“ix”結合起來,創造了“POSIX”這個易于發音和記憶的名稱。斯托曼的提議很快被IEEE委員會采納,這避免了標準可能無意中升級競爭對手美國電話電報公司的Unix系統的問題。
POSIX線程技術的發展和重點項目
1996年,Xavier Leroy在Linux系統中引入了LinuxThreads庫,這是在Linux系統中支持內核級線程的首次嘗試。LinuxThreads可以利用克隆系統調用創建一個共享父進程地址空間的新進程,實現線程的基本功能。然而,這種實現在信號處理、調度和進程間同步原語方面與POSIX線程標準存在明顯的兼容性問題。這些限制阻礙了Linux在多線程管理方面的進一步發展,特別是其在企業應用程序中的廣泛應用。
為了解決Linux線程的問題,NGPT(下一代POSIX線程)項目于1998年啟動。NGPT是由包括IBM在內的幾個開發團隊開發的,目標是全面改革Linux中的POSIX線程實現。與此同時,Red Hat的開發人員還啟動了NPTL(Native POSIX Thread Library)項目,計劃集成NGPT和LinuxThreads的優勢和特性。這兩個項目在2002年密切合作,以整合各自的技術優勢。
2003年,NPTL在Red Hat Linux 9中正式發布,并從Linux 2.6版開始包含在Linux內核中。NPTL解決了以前的線程模型無法搶占和放棄系統資源的問題,顯著提高了POSIX標準的兼容性和系統性能。NPTL的成功標志著POSIX線程在Linux中實現的重大突破。該技術已成為GNU C庫的核心組件,極大地提高了Linux系統的多線程處理能力。
開發其他工具
隨著NPTL的普及,社區也開發了一些輔助工具,如2005年發布的POSIX線程跟蹤工具(PTT)和open POSIX測試套件(OPTS)。這些工具支持Linux中多線程應用程序的開發和調試,進一步推動了多線程技術在Linux平臺上的應用和發展。
線程模型
POSIX線程模型定義了在操作系統或運行時環境中如何創建、調度、管理和終止線程。在操作系統中,線程模型分為用戶線程模型、內核線程模型和兩級線程模型。
用戶線程模型:在POSIX用戶線程模型中,線程管理操作完全在用戶空間中進行。線程的調度和管理不需要操作系統內核的直接干預,從而減少了上下文切換的開銷,提高了運行效率。在該模型中,由于操作系統內核將這些線程視為單個執行實體,因此一個線程的阻塞將影響同一進程中的所有線程。
內核線程模型:在POSIX內核線程模型下,每個線程都由操作系統內核直接管理和調度。內核可以在不同的處理器內核上并行調度線程,這使得線程可以更好地利用多核處理器的優勢。內核級線程支持真正的并發執行,一個線程的阻塞不會影響進程中的其他線程。這種模型需要通過系統調用與內核進行交互,這將消耗更多資源,并且需要更長時間來創建和管理線程。
兩級線程模型:在POSIX模型中,兩級線程模型結合了用戶線程和內核線程的優點,提供了靈活的線程管理策略。在這個模型中,線程可以映射到一個或多個內核線程。這種映射策略允許應用程序根據需要調整線程的并發級別,同時可以避免單個線程阻塞整個進程的問題。
生命周期
線程的生命周期通常包括幾個階段:創建、開始、執行、阻塞、終止和恢復。
創建:當調用pthread_create函數創建新線程時,它確保新線程已創建并在返回前處于就緒狀態。也就是說,盡管pthread_create將在新線程準備就緒后返回,但新線程的實際執行開始時間取決于操作系統的調度策略。因此,新線程可能在pthread_create函數返回之前開始執行,也可能在函數返回之后開始執行,具體取決于操作系統的調度決策。
啟動:一個線程通過pthread_create創建并執行一個啟動函數。在初始線程中,線程的“啟動函數(main)”是從程序外部調用的。應該用pthread_exit終止初始線程,以允許進程中的其他線程繼續運行。初始線程的堆棧大小與其他線程不同,這可能會受到更多限制,并且堆棧溢出會導致錯誤。
運行和阻塞:線程在其生命周期中主要經歷三種狀態:就緒、運行和阻塞。新創建或未阻塞的線程處于就緒狀態,等待處理器分配。當處理器選擇一個線程來執行時,它就變成了運行狀態。線程通過等待資源(如互斥體)、條件變量、信號或完成I/O操作而進入阻塞狀態。線程從阻塞狀態釋放后,它將再次進入就緒狀態,并可能立即執行或等待處理器再次可用。
終止:線程通過從啟動函數返回或調用pthread_exit來終止。如果設置了特定于線程的數據,將調用相應的析構函數。
回收:如果線程在創建時被設置為分離狀態,或者被pthread_detach調用,當它終止時將立即被回收。未分離的線程終止后,需要通過pthread_detach或pthread_join進行回收。線程回收將釋放所有系統或進程資源,包括線程返回值的存儲空間、堆棧和寄存器狀態的內存等。這些資源不再被其他線程訪問。
線程管理
線程管理在現代多線程應用中起著關鍵作用。通過并行處理任務,它可以顯著提高程序效率并優化多核處理器資源的使用。允許在同一進程中共享內存和資源,提高數據交換和通信的效率,并增強應用程序的響應能力。
線程管理可以分為線程創建、線程終止和線程屬性。
創建線程:創建POSIX線程需要調用pthread_create()函數,傳入一個線程標識符指針、一個線程屬性指針(可以為空以使用默認屬性)、一個線程函數指針和一個函數參數。函數創建成功時結果返回0,失敗時返回錯誤號;線程函數完成后,它通過返回void* value結束,或者可以被取消,允許它通過pthread_join()捕獲其退出狀態。
線程屬性:設置線程優先級
這種優先級配置不僅提高了系統處理關鍵任務的能力,還優化了整體運行效率和響應時間,這是實時系統編程中的重要功能。
在POSIX線程規范中,線程優先級允許用戶指定線程的執行順序。用戶可以通過調用sched_get_priority_min和sched_get_priority_max來查詢系統支持的實時優先級范圍,這通常適用于SCHED_FIFO(先進先出)和SCHED_RR(輪詢調度)調度策略。
例如,在Linux中,實時優先級的范圍通常是從1到99。要設置線程優先級,用戶需要選擇一個值,并使用sched_setscheduler系統調用將其應用于線程或進程。如果pid參數為0,則表示修改調用進程的調度屬性。優先級配置增強了系統處理關鍵任務的能力,提高了運行效率和響應時間,對于實時系統編程非常重要。
控制線程堆棧大小:在POSIX線程中,合理設置線程堆棧大小對于優化程序性能和內存管理非常重要。
在Linux x86-32位系統中,線程的默認堆棧大小為2MB;在64位IA-64架構中,默認大小為32MB。這些默認值通常滿足大多數應用程序的一般要求。開發人員可以通過pthread_attr_setstacksize()函數自定義線程堆棧大小,該函數需要兩個參數:線程屬性對象的指針和要設置的堆棧大小(單位:字節)。設置后,可以使用pthread_attr_getstacksize()函數檢查堆棧大小是否符合預期。設置堆棧大小時,請確保它不低于系統的最小堆棧限制。系統支持的堆棧大小范圍可以通過調用sysconf(SC _ THREAD _ STACK _ MIN)來確認。
可連接狀態(可接合狀態):在POSIX線程中,線程連接是一個進程,其中一個線程通過pthread_join()函數暫停執行,直到另一個特定線程結束。線程連接保證了在線程結束時可以及時回收資源,同時防止了因資源未回收而導致的僵尸線程現象。線程連接是線程同步的一個重要方面,它確保一個線程完成工作后,其他線程可以繼續安全執行,實現有效的線程間協調和資源利用。這對于保持程序的運行效率和系統資源的整體管理非常重要。
分離狀態:在POSIX線程中,默認情況下線程是可連接的。可以通過pthread_detach()函數設置線程分離,以便線程結束時不會留下任何要處理的資源。當線程處于分離狀態時,它會在執行后自動清理并釋放所有占用的資源,而無需其他線程的干預。一旦線程被設置為分離狀態,任何對其使用pthread_join()的嘗試都將導致失敗。線程可以在其生命周期的任何階段設置為分離狀態。一旦設置,它將在任務完成后立即釋放所有資源。它消除了對pthread_join()的依賴,并使資源恢復更快。
線程ID:在POSIX線程中,每個線程都有一個唯一的標識符,稱為線程ID。線程ID可用于查詢特定線程的狀態,或管理線程之間的關系和數據交換。線程的創建和線程ID的獲取通常由pthread_create函數實現,該函數填充一個pthread_t類型的變量,該變量唯一標識進程中的一個線程。同時,線程ID可以幫助開發人員實現特定線程的特定操作,例如調整優先級和設置親和力。
POSIX線程ID與特定于Linux的系統調用gettid()返回的線程ID不同。POSIX線程ID由線程庫實現,負責分配和維護。gettid()返回的線程id是內核(Kemel)分配的一個數字,它類似于ID(進程ID。
CPU關聯性:在POSIX線程中,CPU親和力是指將線程綁定到特定CPU內核的能力,以優化線程的執行效率和系統性能。CPU affinity通過減少線程在不同內核之間的切換來降低緩存失效的可能性,從而提高CPU緩存的利用率??梢酝ㄟ^sched_setaffinity和sched_getaffinity的系統調用來設置和查詢此綁定。
終止線程:在POSIX thread中,通常通過執行函數中的pthread_exit()或從函數中返回void*值來終止線程,這與main函數(main)以return結束的方式類似。此外,該線程還可以通過pthread_cancel()被其他線程強制終止,但需要注意的是,使用pthread_cancel()可能會導致線程無法執行清理代碼,因此它可能不會釋放分配的資源或執行必要的狀態更新。線程的終止將觸發資源的釋放,并允許其他線程通過pthread_join()捕獲它們的退出狀態。因此,一個線程終止后,其他線程可以繼續運行,但進程級的資源不會自動釋放,需要通過適當的同步和管理操作手動回收資源。
線程終止后,其線程ID和返回值將被保留。如果線程被取消,其返回值為PTHREAD_CANCELED。您可以回收已終止線程的返回值并分離資源,以避免使用線程堆棧地址作為返回值來避免數據覆蓋。未分離的線程可以在終止后通過pthread_join進行連接。像UNIX這樣的僵尸進程必須顯式加入才能恢復它們的資源。
線程調度
線程調度是操作系統用來管理多個線程的執行順序的一種機制。它允許設置調度策略來決定應該首先執行哪個線程。
調度策略:在Linux內核中,調度策略是指內核用來決定應該執行哪個線程的方法。這些策略包括完全公平調度器(CFS)和實時調度策略,旨在平衡系統性能和響應時間,確保高優先級任務獲得足夠的CPU時間,同時保持系統的整體效率。調度策略對于操作系統非常重要,因為它們直接影響系統資源的分配和任務的執行順序。POSIX定義了兩種實時調度策略:SCHED_FIFO(先進先出)和SCHED_RR(輪詢)。這些策略確保在這些策略下運行的進程的優先級始終高于標準分時策略下的進程(由常量SCHED_OTHER標識)。
優先級反轉問題:優先級反轉是指低優先級線程占用高優先級線程所需的資源,導致高優先級線程被迫等待的情況。在這樣的系統中,每個CPU都有一個單獨的運行隊列,進程只在每個CPU的運行隊列中按優先級排序。這個問題可能會嚴重影響系統的響應時間和性能。Linux內核通過實現優先級繼承等機制來緩解優先級反轉的影響,以確保系統能夠有效地管理線程之間的資源競爭。實時應用程序通常使用CPU affinity)API來避免這種調度行為可能導致的問題。例如,在四處理器系統中,所有非關鍵進程都可以隔離在單個CPU上,而其他三個CPU則保留給應用程序。
線程本地存儲:在POSIX線程中,線程本地存儲(TLS)允許線程擁有自己獨立的數據副本。即使同一程序中的不同線程訪問同一個全局變量,每個線程都有該變量的私有副本。這對于避免多線程環境中全局變量的同步問題非常有用。線程本地存儲由一組API函數實現。其中,pthread_key_create函數用于創建特殊密鑰。這些鍵使每個線程能夠綁定和存儲自己唯一的數據值。這些鍵及其相關數據在程序啟動或線程創建時被初始化,每個鍵可以指定一個析構函數,以便在線程結束時自動執行數據清理。可以調用Pthread_setspecific來綁定值和特定的鍵,并且可以通過pthread_getspecific檢索綁定的數據。如果不再需要某個密鑰,請調用pthread_key_delete函數銷毀該密鑰,這樣可以避免將來出現潛在問題。根據POSIX標準,至少可以同時創建和使用128個這樣的密鑰。
線程池:線程池是POSIX線程中預先創建和管理的線程的集合,用于限制并發線程的數量并重用線程以降低線程創建和銷毀的成本。應用程序通過創建幾個線程并讓它們執行任務隊列中的任務來實現線程池功能。線程池通常與工作隊列結合使用,工作隊列管理要處理的任務,線程池中的線程負責執行這些任務。這種組合方法優化了任務的分配和執行,提高了應用程序的性能和響應速度。
實時應用
實時應用程序是指需要及時響應輸入的應用程序。通常,該輸入來自外部傳感器或專用輸入設備,輸出采取控制某些外部硬件的形式。具有實時響應要求的應用示例包括自動裝配線、銀行自動取款機和飛機導航系統。
POSIX線程在實時應用中的應用主要關注系統在指定的截止時間內對輸入事件的可靠響應。這種需求涉及從硬件輸入設備獲取數據和控制外部硬件,這需要操作系統的支持以確保及時的任務調度和執行。盡管傳統的UNIX系統不是為實時應用程序設計的,但Linux的實時變體和最近的內核改進正在增加對此類應用程序的本機支持。
計時器和時間管理:計時器使流程能夠安排未來某個時間的通知。setitimer()系統調用可以建立一個在未來某個時間點到期的計時器,并可以選擇在此之后定期到期。對于周期性定時器的精度,盡管setitimer()使用的timeval結構允許微秒級精度,但定時器的精度傳統上受到軟件時鐘頻率的限制。然而,在現代Linux內核中,高分辨率定時器提供了比軟件時鐘頻率更高的精度。
資源鎖定:文件鎖定通常與文件I/O結合使用,但它也可以用作更通用的同步技術。Linux和許多其他UNIX實現還允許fcntl()記錄鎖成為強制性的,這意味著將檢查任何文件I/O操作,以確保它與文件上其他進程持有的鎖兼容。
中斷管理:在處理SIGGTSTP信號時,正確的方法是在信號處理程序中觸發更多的SIGGTSTP信號來停止進程。如果信號處理程序需要在重新建立處理程序后執行一些其他操作(例如保存或恢復全局變量中的值),但在返回之前,它需要再次阻塞SIGTSTP信號。
最小化延遲和抖動:盡管定時器精度受到軟件時鐘的限制,但在現代Linux內核上,高分辨率定時器不受此限制,它可以達到硬件允許的最高精度,通常達到微秒級。