1. 程式人生 > >關於“核心執行緒”、“使用者執行緒”概念的理解

關於“核心執行緒”、“使用者執行緒”概念的理解

今天偶然談起了程序的相關概念,發現其中有許多不清晰的地方,現就以上的概念做一些研究,所參考的資料全部來自於網路,所以對於其中不正確的地方,歡迎大家給我指正,讓我能夠對以上概念更加清晰。

好,首先理清以上幾個概念,先來看“核心執行緒”。此處要申明的一點是,從我所看到的資料來看(《Linux核心設計與實現》、《深入理解Linux核心》以及網上的諸多文章),只有“核心執行緒”的概念,不存在所謂的“核心程序”。此處對於核心執行緒的具體定義我並沒有找到,不過可以從它的功能出發,窺探一二。

核心執行緒的作用主要有:

  1.  週期性的將dirty記憶體頁同步到磁碟裝置上。 比如 bpflush執行緒週期性的把dirty資料寫回磁碟
  2.  記憶體頁很少的情況下,把記憶體page 交換到磁碟空間。 比如kswapd,系統會為每一個NUMA建立一個kswapd程序,但是在非NUMA系統上,則僅有一個kswapd
  3.  管理延時動作
  4.  實現檔案系統的事物日誌

主要包括兩種型別的核心執行緒:

  1. 執行緒按週期性間隔執行,檢測特定資源的使用,在用量超出或者低於預置的限制時採取行動
  2. 線上程啟動後則一直等待,直到核心執行緒請求執行某一特定的操作。

同時核心執行緒是由核心自己建立的執行緒,也叫做守護執行緒(deamon)。在終端上用命令”ps -Al”列出的所有程序中,名字以k開關以d結尾的往往都是核心執行緒,比如kthreadd、kswapd。

核心執行緒與使用者執行緒的相同點是:

  1. 都由do_fork()建立,每個執行緒都有獨立的task_struct和核心棧;
  2. 都參與排程,核心執行緒也有優先順序,會被排程器平等地換入換出

不同之處在於:

  1. 核心執行緒只工作在核心態中;而使用者執行緒則既可以執行在核心態(執行系統呼叫時),也可以執行在使用者態;
  2. 核心執行緒沒有使用者空間,所以對於一個核心執行緒來說,它的0~3G的記憶體空間是空白的,它的current->mm是空的,與核心使用同一張頁表;而使用者執行緒則可以看到完整的0~4G記憶體空間。

在Linux核心啟動的最後階段,系統會建立兩個核心執行緒,一個是init,一個是kthreadd。其中init執行緒的作用是執行檔案系統上的一系列”init”指令碼,並啟動shell程序,所以init執行緒稱得上是系統中所有使用者程序的祖先,它的pid是1。kthreadd執行緒是核心的守護執行緒,在核心正常工作時,它永遠不退出,是一個死迴圈,它的pid是2。

通過上述不同之處的對比可以發現,核心執行緒沒有使用者記憶體空間,與之相對的是使用者程序(注意,此處不是使用者執行緒,使用者執行緒記憶體空間與使用者程序空間之間存在的一定差異,具體差異可以參考我之前的博文),使用者程序同時具備核心空間與使用者空間,在進行系統呼叫時使用者程序會由使用者記憶體空間陷入核心記憶體空間。之所以此處採用核心執行緒與使用者程序(而非使用者執行緒)進行對比,是由於核心執行緒(kernel thread)是“獨立執行在核心空間的標準程序”(以上這句話摘自《linux核心設計與實現》),因此從功能上看核心執行緒一方面具有程序的概念特點——具有獨立功能的程式關於某個資料集合上的一次執行活動,是系統進行資源分配的單位,同時核心執行緒又具有執行緒的概念特點——程序內的一個可排程實體。

好了通過以上分析基本可以分清“核心執行緒(kernel thread)”、“使用者程序”、“使用者執行緒”這幾個基本概念。這裡要補充的一點是以上幾個概念全部是在邏輯層面上的,從實現的角度來看linux核心本身並不支援執行緒這一概念,linux 將所有的執行緒都當作程序來實現。核心並沒有準備特別的排程演算法或是定義特別的資料結構來表徵執行緒。相反,執行緒僅僅被視為一個與其他程序(概念上應該是執行緒)共享某些資源的程序(概念上應該是執行緒)。每個執行緒都擁有唯一隸屬於自己的task_struct,所以在核心中,它看起來就像是一個普通的程序(只是執行緒和其他一些程序共享某些資源,如地址空間)。關於這一點可以通過系統呼叫clone的實現來驗證,無論是fork、vfork、kthread_create最後都是要呼叫do_fork,而do_fork就是根據不同的函式引數,對一個程序所需的資源進行分配。在linux2.6之前,核心並不支援執行緒的概念,僅通過輕量級程序(lightweight process)模擬執行緒,一個使用者執行緒對應一個核心執行緒(核心輕量級程序),這種模型最大的特點是執行緒排程由核心完成了,而其他執行緒操作(同步、取消)等都是核外的執行緒庫(LinuxThread)函式完成的。但這個問題還存在很多的問題,具體問題請見http://www.ibm.com/developerworks/cn/linux/kernel/l-thread/index.html 這篇文章中的第6小節。在linux2.6之後,為了完全相容posix標準,linux2.6首先對核心進行了改進,引入了執行緒組的概念(仍然用輕量級程序表示執行緒),有了這個概念就可以將一組執行緒組織稱為一個程序,如此通過這個改變,linux核心正式支援多執行緒特性。以上是邏輯上的改變,在實現上主要的改變就是在task_struct中加入tgid欄位,這個欄位就是用於表示執行緒組id的欄位。在使用者執行緒庫方面,也使用NPTL代替LinuxThread。不同調度模型上仍然採用“1對1”模型,關於執行緒的排程模型,接下來會詳細分析。

接下來來看看核心執行緒與使用者執行緒之間的對應關係。首先從原理上出發,對核心執行緒與使用者執行緒之間的對應關係進行分析。

現在在支援多執行緒的作業系統中一般採用三種排程模型,分別是“一對一模型”、“多對一模型”、“多對多模型”。

以上內容參考自下述兩篇blog

1)“一對一模型”

一對一模型中,每個使用者執行緒都對應各自的核心排程實體。核心會對每個執行緒進行排程,可以排程到其他處理器上面。當然由核心來排程的結果就是:執行緒的每次操作會在使用者態和核心態切換。另外,核心為每個執行緒都對映排程實體,如果系統出現大量執行緒,會對系統性能有影響。但該模型的實用性還是高於多對一的執行緒模型。LinuxThread與NPTL都是採用這種模型。

在linux中通過LWP(lightweight process)作為執行緒概念的支援,輕量級執行緒(LWP)是一種由核心支援的使用者執行緒。它是基於核心執行緒的高階抽象,因此只有先支援核心執行緒,才能有LWP。每一個程序有一個或多個LWPs,每個LWP由一個核心執行緒支援。這種模型實際上就是恐龍書上所提到的一對一執行緒模型。在這種實現的作業系統中,LWP就是使用者執行緒。

由於每個LWP都與一個特定的核心執行緒關聯,因此每個LWP都是一個獨立的執行緒排程單元。即使有一個LWP在系統呼叫中阻塞,也不會影響整個程序的執行。

輕量級程序具有侷限性。首先,大多數LWP的操作,如建立、析構以及同步,都需要進行系統呼叫。系統呼叫的代價相對較高:需要在user mode和kernel mode中切換。其次,每個LWP都需要有一個核心執行緒支援,因此LWP要消耗核心資源(核心執行緒的棧空間)。因此一個系統不能支援大量的LWP。圖也是盜的。

LWP.JPG

2)“多對一模型”

多對一執行緒模型中,執行緒的建立、排程、同步的所有細節全部由程序的使用者空間執行緒庫來處理。使用者態執行緒的很多操作對核心來說都是透明的,因為不需要核心來接管,這意味不需要核心態和使用者態頻繁切換。執行緒的建立、排程、同步處理速度非常快。當然執行緒的一些其他操作還是要經過核心,如IO讀寫。這樣導致了一個問題:當多執行緒併發執行時,如果其中一個執行緒執行IO操作時,核心接管這個操作,如果IO阻塞,使用者態的其他執行緒都會被阻塞,因為這些執行緒都對應同一個核心排程實體。在多處理器機器上,核心不知道使用者態有這些執行緒,無法把它們排程到其他處理器,也無法通過優先順序來排程。這對執行緒的使用是沒有意義的!

Uthread1.JPG

3)“多對多模型”

使用者執行緒庫還是完全建立在使用者空間中,因此使用者執行緒的操作還是很廉價,因此可以建立任意多需要的使用者執行緒。作業系統提供了 LWP 作為使用者執行緒和核心執行緒之間的橋樑。 LWP 還是和前面提到的一樣,具有核心執行緒支援,是核心的排程單元,並且使用者執行緒的系統呼叫要通過 LWP ,因此程序中某個使用者執行緒的阻塞不會影響整個程序的執行。使用者執行緒庫將建立的使用者執行緒關聯到 LWP 上, LWP 與使用者執行緒的數量不一定一致。當核心排程到某個 LWP 上時,此時與該 LWP 關聯的使用者執行緒就被執行。

Uthread2.JPG