本文從linux中的程式、執行緒實現原理開始,擴充套件到linux執行緒模型,最後簡單解釋執行緒切換的成本。
剛開始學習,不一定對,好心人們快來指正我啊啊啊!!!
linux中的程式與執行緒
首先明確程式與程式的基本概念:
- 程式是資源分配的基本單位
- 執行緒是CPU排程的基本單位
- 一個程式下可能有多個執行緒
- 執行緒共享程式的資源
基本原理
linux使用者態的程式、執行緒基本滿足上述概念,但核心態不區分程式和執行緒。可以認為,核心中統一執行的是程式,但有些是“普通程式”(對應程式process),有些是“輕量級程式”(對應執行緒pthread或npthread),都使用task_struct
結構體儲存儲存。
使用fork
建立程式,使用pthread_create
建立執行緒。兩個系統呼叫最終都都呼叫了do_dork
,而do_dork
完成了task_struct
結構體的複製,並將新的程式加入核心排程。
程式是資源分配的基本單位、執行緒共享程式的資源
普通程式需要深拷貝虛擬記憶體、檔案描述符、訊號處理等;而輕量級程式之所以“輕量”,是因為其只需要淺拷貝虛擬記憶體等大部分資訊,多個輕量級程式共享一個程式的資源。
執行緒是CPU排程的基本單位、一個程式下可能有多個執行緒
linux加入了執行緒組的概念,讓原有“程式”對應執行緒,“執行緒組”對應程式,實現“一個程式下可能有多個執行緒”:
- 作業系統中存在多個程式組
- 一個程式組下有多個程式(1:n)
- 一個程式對應一個執行緒組(1:1)
- 一個執行緒組下有多個執行緒(1:n)
task_struct
中,使用pgid標的程式組,tgid標的執行緒組,pid標的程式或執行緒。假設目前有一個程式組,則上述概念對應如下:
- 程式組中有一個主程式(父程式),pid等於程式組的pgid;程式組下的其他程式都是父程式的子程式,pid不等於pgid
- 每個程式對應一個執行緒組,pid等於tgid。
- 執行緒組中有一個“主執行緒”(勉強稱為“主執行緒”,位的是與主程式對應;語義上絕不能稱為“父執行緒”),pid等於該執行緒組的tgid;執行緒組下的其他執行緒都是與主執行緒平級,pid不等於tgid
因此,呼叫getpgid返回pgid,呼叫getpid應返回tgid,呼叫gettid應返回pid。使用的時候不要糊塗。
程式下除主執行緒外的其他執行緒是CPU排程的基本單位,這很好理解。而所謂主執行緒與所屬程式實際上是同一個task_struct
,也能被CPU排程,因此主執行緒也是CPU排程的基本單位。
tgid相同的所有執行緒組成了概念上的“程式”,只有主執行緒在建立時會實際分配資源,其他執行緒通過淺拷貝共享主執行緒的資源。結合前面介紹的普通執行緒與輕量級程式,實現“程式是資源分配的基本單位”。
舉個例子
pgid | tgid | pid |
---|---|---|
111 | 111 | 111 |
112 | 112 | 112 |
112 | 112 | 113 |
113 | 113 | 113 |
113 | 113 | 114 |
113 | 115 | 115 |
113 | 115 | 116 |
113 | 115 | 117 |
- 存在3個程式組111、112、113
- 程式組111下有1個父程式111,單獨分配資源
- 程式111下有1個執行緒111,共享程式111的資源
- 程式組112下有1個父程式112,單獨分配資源
- 程式112下有2個執行緒112、113,共享程式112的資源
- 程式組113下有1個父程式113,1個子程式115,各自單獨分配資源
- 程式113下有2個執行緒113、114,共享程式113的資源
- 程式115下有3個執行緒115、116、117,共享程式115的資源
- 程式組111下有1個父程式111,單獨分配資源
小結
現在再來理解linux中的程式與執行緒就容易多了:
- 程式是一個邏輯上的概念,用於管理資源,對應
task_struct
中的資源 - 每個程式至少有一個執行緒,用於具體的執行,對應
task_struct
中的任務排程資訊 - 以
task_struct
中的pid區分執行緒,tgid區分程式,pgid區分程式組
linux執行緒模型
一對一
LinuxThreads與NPTL均採用一對一
的執行緒模型,一個使用者執行緒對應一個核心執行緒。核心負責每個執行緒的排程,可以排程到其他處理器上面。Linux 2.6預設使用NPTL執行緒庫,一對一的執行緒模型。
優點:
- 實現簡單。
缺點:
- 對使用者執行緒的大部分操作都會對映到核心執行緒上,引起使用者態和核心態的頻繁切換。
- 核心為每個執行緒都對映排程實體,如果系統出現大量執行緒,會對系統效能有影響。
多對一
顧名思義,多對一
執行緒模型中,多個使用者執行緒對應到同一個核心執行緒上,執行緒的建立、排程、同步的所有細節全部由程式的使用者空間執行緒庫來處理。
優點:
- 使用者執行緒的很多操作對核心來說都是透明的,不需要使用者態和核心態的頻繁切換。使執行緒的建立、排程、同步等非常快。
缺點:
- 由於多個使用者執行緒對應到同一個核心執行緒,如果其中一個使用者執行緒阻塞,那麼該其他使用者執行緒也無法執行。
- 核心並不知道使用者態有哪些執行緒,無法像核心執行緒一樣實現較完整的排程、優先順序等
多對多
多對一執行緒模型是非常輕量的,問題在於多個使用者執行緒對應到固定的一個核心執行緒。多對多執行緒模型解決了這一問題:m個使用者執行緒對應到n個核心執行緒上,通常m>n
。由IBM主導的NGPT採用了多對多
的執行緒模型,不過現在已廢棄。
優點:
- 兼具多對一模型的輕量
- 由於對應了多個核心執行緒,則一個使用者執行緒阻塞時,其他使用者執行緒仍然可以執行
- 由於對應了多個核心執行緒,則可以實現較完整的排程、優先順序等
缺點:
- 實現複雜
執行緒切換
linux採用一對一的執行緒模型,使用者執行緒切換與核心執行緒切換之間的差別非常小。同時,如果忽略使用者主動放棄使用者執行緒的執行權(yield)帶來的開銷,則只需要考慮核心執行緒切換的開銷。
注意,這裡僅僅是為了幫助理解做出的簡化。實際上,使用者執行緒庫在使用者執行緒的排程、同步等過程中做了很多工作,這部分開銷不能忽略。
如JVM對Thread#yield()的解釋:如果底層OS不支援yield的語義,則JVM讓使用者執行緒自旋至時間片結束,執行緒被動切換,以達到相似的效果。
什麼引起執行緒切換
- 時間片輪轉
- 執行緒阻塞
- 執行緒主動放棄時間片
執行緒切換的開銷
直接開銷
直接開銷是執行緒切換本身引起的,無可避免,必然發生。
使用者態與核心態的切換
執行緒切換隻能在核心態完成,如果當前使用者處於使用者態,則必然引起使用者態與核心態的切換。(“使用者態與核心態的切換”具體帶來什麼成本???)
上下文切換
前面說執行緒(或者叫做程式都隨意)資訊需要用一個task_struct
儲存,執行緒切換時,必然需要將舊執行緒的task_struct
從核心切出,將新執行緒的切入,帶來上下文切換。除此之外,還需要切換暫存器、程式計數器、執行緒棧(包括操作棧、資料棧)等。
執行緒排程演算法
執行緒排程演算法需要管理執行緒的狀態、等待條件等,如果根據優先順序排程,則還需要維護優先順序佇列。如果執行緒切換比較頻繁,該成本不容小覷。
間接開銷
間接開銷是直接開銷的副作用,取決於系統實現和使用者程式碼實現。
快取缺失
切換程式,需要執行新邏輯。如果二者的訪問的地址空間不相近,則會引起快取缺失,具體影響範圍取決於系統實現和使用者程式碼實現。如果系統的快取較大,則能減小快取缺失的影響;如果使用者執行緒訪問資料的地址空間接近,則本身的快取缺失率也比較低。
對頁表等快慢表式結構同理。
參考:
本文連結:淺談linux執行緒模型和執行緒切換
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。