淺談linux執行緒模型和執行緒切換

monkeysayhi發表於2017-12-14

本文從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的資源

小結

現在再來理解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 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章