一文看懂進程和線程調度

進程線程調度

對於進程線程調度大家肯定都不陌生,都能夠說上兩句,比如什麼進程是資源分配的基本單位,線程是調度的基本單位,進程有獨立的地址空間,線程沒有,線程與進程裏面其他的線程共享資源,再有就是花樣百出的調度策略。但是可能很多人對進程線程調度的內部情況還是不太清楚,只是說對這知識很熟悉,產生了理所當然的感覺。

本文就從一個簡單的線程進程調度設計上來幫助大家理清進程線程的區別,縷清調度這條線。

線程

我們先來看看線程,在 POSIX 定義的線程標準中,線程是這樣創建的,函數原型如下:

圖片

第一個參數:thread,用來儲存線程的 id

第二個參數:attr,表示創建線程的類型,一般默認設爲 NULL 就好

第三個參數:start_routine,是一個函數指針

第四個參數:arg,函數參數,傳給上述函數指針指向的函數。因爲只有這一個形參作爲 start_routine 的參數,所以一般將參數封裝成一個結構體再傳過去。

私以爲這四個參數的核心應該是後面兩個參數,函數以及它的參數,有了它倆,線程纔有意義對吧。從這兒其實就已經能夠看出,線程也就是執行一個函數。只是這個 “函數” 有些特別,它可以單獨上 CPU 運行,而不是像其他的普通函數只是附帶地被能夠上 CPU 的線程調用執行。

**所以線程就像是一個載體,它能載着函數單獨地上 CPU。**那是什麼原因造成線程如此特別,任務同樣是執行某個函數,爲什麼線程就能夠被調度而單獨上 CPU 呢?舉個可能不太恰當的例子,一堆木頭和一個木車有什麼區別?就在於木車有特殊的結構組織,所以木車就可以單獨地行駛,而木頭呢?只能裝在木車上然後被運到某個地方。同理呢,線程相較於函數就特殊在它有特殊的結構組織,比如 PCB,棧,寄存器組等。

瞭解了線程的本質後,我們來具體設計一個很簡單很簡單的內核線程,看看線程內部的具體情況,從創建到運行經歷了哪些過程,下面正式開始定義線程的一些數據結構:

1

PCB(task_struct)

PCB,Process Control Block,程序控制塊,簡單的一句話來說,PCB 裏面集合了一個進程 / 線程運行過程當中所有必要的信息,所以 PCB 就是進程 / 線程的標誌,他們之間是一一對應的關係。

而在 Linux 裏面進程線程的 PCB 都是用結構體 task_struct 來定義的,所以其實 Linux 裏其實沒有嚴格的進程和線程之分,線程也稱爲輕量級進程。

PCB 裏面會存放哪些信息呢?在這兒引用以下《操作系統精髓與設計原理》的內容,PCB 裏面會存放以下信息:

如今的 Linux 裏面的 task_struct 結構體定義有好幾百行代碼,而且裏面還包含了好多結構體,全部擴展開來的話都上千行了。而我們這裏只定義幾個元素,能弄清楚線程最基本的原理即可。第一版 task_struct 本定義如下:

圖片

我們定義了線程的棧,狀態,名字。應該都好理解,線程也有自己的狀態,狀態定義如下:

圖片

本文裏面用不到這麼多狀態,只會用到 RUNNABLE 和 RUNNING。

然後用名字來標識線程,還有就是定義了一個棧,每個線程都有自己單獨的棧,而且注意棧指針是線程 PCB 的第一個元素,這兒是有特殊用處的,後面會說,這兒只是提一句。

另外我們的 PCB 信息很少,只是示範性展示原理,棧也不用太大,所以直接將內核棧和 PCB 放在同一頁,上面作爲棧用,下面作爲 PCB。佈局如下圖所示:

圖片

2

棧的定義

前面定義了線程的 PCB,比較簡單,難就難在棧的定義,準確說來是線程創建時棧的區域劃分。線程的創建跟運行是息息相關的,線程的運行可以分爲兩種情況:第一次運行和非第一運行。非第一次運行時線程所有的數據結構,資源等都是既在的,不需要我們手動去定義。而第一次運行需要的信息是要我們手動去設置的,爲了一致性,我們創建時線程時應該模仿着非第一次運行的情況來創建結構信息。

那麼非第一運行時線程的結構組織是怎樣的呢?線程要上 CPU,那肯定是發生了調度,誰又來觸發調度呢?我們這兒只考慮時鐘中斷引起的調度。那麼線程的內核棧結構組織應該很明確了,從上倒下應該是:中斷棧幀,期間函數調用的參數返回地址,調度程序的上下文。目前感覺很模糊抽象沒關係,有個大概印象,看完本文後會感覺豁然開朗。

中斷棧幀

圖片

這就是中斷棧幀,中斷時上下文信息就保存在這兒,前文已經詳細講過中斷,只是沒說這些寄存器保存到哪,如今清楚了,保存到內核棧裏面。這兒我們只是爲了方便統一,所以把所有的寄存器全部定義了,實際上爲了效率中斷時只會壓入必要的寄存器。不熟悉的可以翻翻前文,中斷,時鐘,鍵盤這三篇文章,在此就不贅述了。

參數 地址

接下來定義第二部分,參數和地址,這可是線程能運行的關鍵。第二部分和線程第一次運行緊密相關,所以得先說說線程第一次怎麼運行的。線程的運行就是執行函數,某個函數被執行說明執行流發生了變化,執行流發生了變化說明 cs:eip 的值發生了改變。而改變 cs:eip 的值就只有 call, ret, jmp 三種指令,一般的函數調用我們使用 call 指令,而這裏爲了與後面調度一致,運行函數我們使用 ret 指令。

來仔細瞭解一下 ret 指令,**ret 指令其實等價於 popl %eip ** 所以過程分爲兩步:

  1. 修改 eip,將棧頂的值存放到 eip 寄存器中。eip 中存放的是下條指令地址,所以使用 ret 指令的時候,棧頂一定會是條指令的地址,否則會出錯,通常爲主調函數 call 指令下一條指令地址。

  2. esp 加 4。

因此我們可以模仿函數調用,自己在棧裏設置好棧幀,把要執行的函數地址放到棧頂,然後 ret,就能返回執行函數了。

那函數調用時的棧幀什麼樣的?我們來複習複習,這與調用約定有關,c 語言默認的調用約定是 cdecl,舉個簡單的例子來看看 c 中怎麼調用函數的,比如說函數 funcA 調用 funcB(param1, param2),發生的過程如下:

圖片

而棧中情況如下:

圖片

所以綜上所述:函數調用時要先從右至左壓入被調用函數需要的參數,再留下主調函數的返回地址 (call 指令下一條指令的地址),然後跳轉去執行被調函數。函數執行完後返回,由主調函數來清理參數佔用的棧空間。正是由於調用約定,被調函數就知到返回地址的上面就是參數,運行時就會去相應地方拿取參數然後執行。

這就是函數執行的過程,而我們的線程第一次運行就是要模擬這個過程。也就是說我們要在棧中先放好線程函數的參數,再**留下一個啞地址,這是一個用不到的地址,只是佔位用。**前面說過,返回地址前就是參數,我們也得按照這個約定來,雖然用不上這個返回地址,但也得設置來形成正確的棧幀。隨後再放置一個函數指針,ret 時它就是棧頂元素,然後就會去執行這個函數,線程就運行起來了。這兒一定要注意 ret 時棧頂是線程函數指針而不是我們設置的那個返回地址

線程一般傳的函數原型爲:

圖片

所以棧中第二部分需要定義的數據結構差不多也就明瞭了,定義如下:

圖片

這個結構體只有線程第一次運行會用,後面都不會再用,因爲我們這是在模仿函數調用,而運行起來後是真的存在函數調用,不需要再用到這個結構,雖然結構可能不太一樣,就在於比起函數調用多了一個元素——函數指針 eip。

調度程序上下文

終於來到了第三部分,這部分是要定義調度程序的上下文。爲什麼要定義這個,與調度相關。所以要提前說明以下調度,不管什麼操作系統,調度程序做的事情都可以歸結爲兩步:

  1. 選一個線程

  2. 切換線程

我們的調度程序爲 schedule() 函數,切換函數爲 swtch(cur, next)。切換線程涉及到底層操作,直接使用匯編指令編寫更加方便,而寫彙編就是所有的指令都得親歷親爲,不像用高級語言編寫程序,有編譯器爲你編譯。編譯器編譯是要遵循一定規則編譯的,例如上述所說的調用約定。我們寫的切換函數會在 c 文件裏面被調用,那麼能夠正確被調用要求我們自己寫的彙編也得按照約定來。

這裏我們就需要遵循一個規則,**ABI,Application Binary Interface,程序二進制接口。不同於熟悉的 API,它是更加底層的一套規則,是編譯方面的約定。例如上述的調用約定就屬於 ABI。**ABI 裏面規定了哪些爲被調用者保存寄存器,有 ebx, ebp, esi, edi, esp。意思是說這 5 個寄存器歸主調函數所用,爲了不破壞主調函數的寄存器結構,被調用函數需要將這些寄存器保存下來。

因此有了如下的上下文結構:

圖片

再這裏我們只定義了 4 個寄存器,並沒有 esp,這是因爲 esp 會由調用約定來保證,再者 esp 還會保存到 kstack 裏面,後面再詳述。

到此終於把線程創建需要的結構定義好了,下面就來具體創建線程,僞碼如下:

圖片

看個詳細流程圖幫助理解:

圖片

僞碼配上流程圖應該很清晰了,就不再解釋說明了。

下面來看線程怎麼運行,也就是 thread_exec(thread) 函數,其實這裏面應該沒這函數的,因爲線程是要經過調度才能上 CPU,才能運行的。這裏只是先提前讓大家看看我們的線程是怎麼運行的,畢竟說了那麼多次,線程第一次運行很特殊,靠返回運行,僞碼如下:

圖片

先將線程內核棧的棧頂賦給 esp 寄存器,然後彈出 context 裏定義的 4 個寄存器,再 ret 就返回執行線程函數了。

**ret 時棧頂元素是線程函數的地址,將其賦給 eip 之後就改變了執行流,執行線程函數。**由於函數調用約定,線程函數知道目前的棧頂爲返回地址,返回地址前面爲參數。當然這是在剛運行函數的時候,通常函數里面有個 pushl %ebp 的步驟,那棧頂就不是返回地址了。總之由於約定,函數能夠找到自己需要的參數然後執行。不熟悉的可以再仔細看看 32 位機器函數調用方面的知識。

調度

說完線程的創建運行,接下來說任務的調度,進程方面的問題放在後面,因爲進程就是比線程多了一些東西,所以進程是可以在線程的基礎上實現的。因此只要搞清楚了線程的執行和調度,進程也就好理解很多。

廢話不多說,前面說過調度 schedule() 函數主要做兩件事

  1. 選取下一個線程

  2. 調用切換線程函數 switch(cur, next)

1

挑選一個線程

先說第一件事:挑選出一個線程。這又可以引發兩個問題:

  1. 去哪兒挑選?

  2. 挑選的原則?

PCB 就是線程的標識,它記錄了線程所有需要的信息,如果我們把所有線程的 PCB 集合起來,不就有地方挑選了?這裏我們採用隊列的形式將所有線程的 PCB 串起來,將這個隊列設爲 ready_queue。這需要在 task_struct 裏添加一個隊列結點。

**挑選的原則就是調度策略了,我們用 “帶優先級的時間片輪轉法” 來舉例。**因爲我感覺更像時間片輪轉法,但優先級又會稍加影響,所以取這名字。這裏需要向 PCB 中添加 priority 表示優先級,添加 ticks 表示還可以運行的時鐘滴答數。

所以現在的 PCB 如下所示:

圖片

我們只來討論時鐘中斷情況下觸發的線程調度。時鐘中斷就不具體說了,不太熟悉的可以看看前文講的時間管理大師一文。前文只簡略說了一下時鐘中斷處理函數,本文來往裏面添點料。

我們的調度策略如下:priority 表示每次線程上 CPU 時能夠運行的滴答數,ticks 記錄該線程還能繼續運行的滴答數,每次時鐘中斷 ticks 減 1,如果減到零了,調用調度程序。僞代碼如下:

圖片

接下來就是 schedule 函數,先直接看僞碼吧,我們的調度策略很簡單,而且只討論時間片用完的情況,直接看碼差不多也就懂了

圖片

流程應該很清楚明瞭,只說兩點,函數 get_cur_thread() 和 函數 tag2thread(),這兩個函數都是要得到 task_struct,實現原理都是一樣的。我們分配內存時都是一頁一頁 4KB 的分配,task_struct* 類型的元素是個地址值,它指向這一頁內存的底部,所以它的低三位應該是爲零的。

而我們的內核棧和 task_struct 是在一個頁面內的,esp 指向棧頂,也是個地址值,看到這有沒有想到怎麼獲取當前線程結構體?很簡單,直接將 esp 值的低 3 位變成 0 就是當前的 task_struct thread*。tag 轉換成 thread 也是同理,有沒有很巧妙?我當時看到這的時候驚訝了半天,太簡單巧妙了,畢竟當時看 xv6 時獲取當前進程懵逼了好久好久,這個就簡單明瞭多了。

ready_queue,就緒隊列,裏面存放的全是 task_struct 裏面的結點元素,以此來將所有的狀態位 RUNNABLE 的 task_struct 串起來,這方面就是數據結構隊列的簡單應用,不再多說。這裏面只出現了一個就緒隊列,實際系統中應該會有多種隊列,比如全部進程 / 線程的隊列,根據調度策略不同可能還會有多級優先級隊列等等。

2

swtch(cur, next)

還是先來看代碼吧:

圖片

算是經典函數了,xv6 裏面也是這麼實現的,幾乎一模一樣,我們來仔細分析一下,先來看看調用 swtch 時棧中的情況:

圖片

**1、**前兩個 mov 指令 獲取 cur, next 存到 eax, edx 寄存器中

**2、**4 個 push 指令保存上下文,前文說過這是 ABI 的規則,需要被調用者保存這幾個寄存器

**3、**接着 movl %esp, (%eax) 保存當前線程的棧頂值,前文說過 esp 也是被調用者保存寄存器之一,除了調用約定保證之外,還有個保存地點就在這兒了,只是與其他 4 個寄存器保存方式不同而已。

仔細看看保存在哪?(%eax),eax 裏面存放的是線程 cur 的地址,而 task_struct 的第一個元素 kstack 的地址也是 eax,**那麼 (%eax) 就應該指的是元素 kstack,也就是說棧頂值保存在 kstack 裏,這也是 kstack 的意義所在,指向棧頂。**另外這就是 kstack 爲 task_struct 第一個元素的好處所在。

4、理解上面之後 movl (%edx), esp  也就理解了,(%edx) 是線程 next 的 kstack 的值,也就是 next 棧的棧頂值,然後將它賦給 esp 寄存器。這就是最爲關鍵的的換棧步驟。

**5、**然後 4 個 popl 指令,彈出 next 執行調度時候的上下文,使得 esp 指向 next 棧中的返回地址,最後 ret 返回,至此線程就切換了。

注意重點標註的 next ,第四步過後棧已經切換了,彈出的所有東西都是 next 的。

這個過程是不是與前面的線程執行函數很像,前面一直提到的一致性就在這裏體現,線程執行函數就是模仿 swtch 來寫的,所以以後線程要運行,只要添加到就緒隊列就行了,正式上 CPU 執行是靠調度程序調度來完成的。

再來看看內核棧的變化圖仔細縷縷:

圖片

上述就是線程切換調度的全過程,這下可以解開線程第一次運行爲什麼要那麼佈局的問題,就是爲了一致性方便操作。

進程

上述的線程的創建,運行,切換已經說完,下面說說進程,進程咱們在這兒略講,關於進程的知識還是很多的,包括特權級,內存佈局,用戶態等等,在這我們只是說說進程與線程有哪些實際上的區別。

1

單獨的地址空間

根據大多書上的理論知識,目前我們應該很清楚,進程就是比線程多了一些東西,而最重要的應該是多了自己的虛擬地址空間

線程擁有的資源很有限,只有個棧,然後在上面建立中斷棧幀,運行必要的上下文等,也就是通常書上所說的寄存器資源。而進程擁有整個虛擬地址空間,整整 4 GB,它能在上面建立的東西可就太多了,堆棧,數據代碼段等等,甚至還包括了線程所擁有的資源,這也就是爲什麼說線程要依賴於進程存在的原因。

所以可以說進程和線程最主要的區別就是有沒有自己的地址空間,進程的其他特有的機制都是建立在地址空間之上的,而有沒有地址空間就是有沒有自己的頁表,那麼進程的 task_struct 可以如下定義:

圖片

前面說過,進程依據線程實現,所以進程線程用的是同一個任務結構體,只是進程的 pgdir 有實際的值,而線程的 pgdir 設爲 NULL。

2

特權級 3 用戶態下工作

進程的另一特點就是運行在 3 特權級,進程的創建在內核態下實現,如果調度的是進程,需要從內核態返回用戶態,高特權級轉成低特權級。而 CPU 一般不會允許從高特權級轉回低特權級,只有一種情況例外,中斷返回。

**線程運行是模仿函數調用然後返回執行的,而這裏我們進入 3 特權級則是模仿中斷然後返回到 3 特權級,實有異曲同工之妙。**關於中斷棧幀的結構體線程那一塊已經定義,這裏不再贅述,直接看進程的創建。

進程的創建

這裏進程的創建,我們只說第一個進程也就是 init 進程的大致創建過程,因爲後面所有的進程都是 init 進程的子進程,通過 fork,exec 等系統調用來實現的,涉及的內容挺多的,我後面單獨寫一篇關於創建新進程的文章。

init 進程的創建建立在線程之上,所以創建線程的步驟一個都不會少。而進程的 pgdir 是有實際值的,所以 thread_init 中得初始化 pgdir 。

剩下最大的不同在於填充中斷棧幀,我們上述實現的線程只是在內核中運行,沒有返回到用戶態這一步,所以沒有填充中斷棧幀。而**填充棧幀就像創建線程時填充函數指針,參數,返回地址一個道理,就是爲了裝的像,方便返回執行。**這裏我們將中斷棧幀填充到中斷棧,前面一直給中斷預留的有空間,用處就在這。

但還有一個問題,我們的進程在線程之上實現,想想線程是怎麼運行的?彈出調度程序的上下文後 ret 返回執行線程函數,如果我們不做改變,ret 後那麼執行流都跑其他地方去了,根本沒機會中斷返回。所以這裏需要將線程函數變爲填充中斷棧幀的函數,中斷棧幀也是有 eip 的,我們將程序的入口地址放在這裏。如此既保持了線程調度運行的一致性,又能填充中斷棧幀然後中斷返回,一舉兩得。

綜上所述我們來縷縷創建進程需要的步驟:

有個認識之後,來看看具體的僞碼:

圖片

再來看看流程圖幫助理解,流程圖只畫了一部分,另一部分是線程創建流程圖:

圖片

中斷棧幀創建好後就可以中斷返回了,這裏我們是直接跳轉到 intr_exit 去執行,這個函數前面的文章說過,它就是中斷時壓棧的逆過程。進程的創建就說到這兒,再來看看調度,需要做一些改變

圖片

改變得很少,進程有自己的頁表,所以需要切換頁表。而頁表的切換就是將頁目錄地址加載到 cr3 寄存器,詳細說明見前文的分頁地址轉換一文。

設計說明

關於進程線程調度的一個簡單設計到此也就結束了,這個設計思路主要來源於操作系統真象還原,融和了 xv6,linux0.11 的一些想法,以及自己的思考。這個設計思路應該算是最簡單的了吧,雖然簡單也足以讓我們瞭解進程線程調度的本質。

1

棧方面的說明

關於這個設計私以爲說的還是夠清楚了,如果沒看懂可以抓住棧,棧頂來看,不管看哪個系統源碼,我認爲,只要把其中棧的變化,棧頂的變化弄清楚了,問題就解決大半了。說到棧再多提幾句:

kstack

**這個設計的 kstack 元素,雖然我們的目的是讓它指向棧頂,但要清楚這是我們定義在內存裏面的一個變量,不可能時刻指向棧頂,只有 esp 寄存器才時刻指向棧頂,只有執行到修改 kstack 值的時候可能纔會指向棧頂。**主要是我自己在上面栽過跟頭,說明一下警示自己和希望大家不要像我犯這種低級錯誤。

內核態轉用戶態時要保存內核棧棧頂值

主要是說在上述中斷返回時需要將棧頂值記錄在 tss 結構中的 esp0 位置處,上述的僞碼中沒有涉及,因爲相關知識、細節需要說明的挺多,以後細說。在這簡單解釋一下,從用戶態陷入內核態時需要換成內核棧,這個內核棧的棧頂值就保存在 tss.esp0 中,CPU 會自動去取。

2

內核線程,輕量級進程,用戶線程

我們上述創建的線程就屬於內核線程,只運行在內核態,不受用戶態影響。

**輕量級進程是內核支持的用戶線程,是內核線程的抽象,每個輕量級進程都會與一個內核線程相關聯,以此來實現由內核支持的用戶線程。**這個關聯現在是 NPTL 來做的,實現了對 POSIX 標準的兼容。

而用戶線程從頭至尾的一切工作如創建調度等等,都是獨立於內核之外,僅在用戶態下實現,內核並不支持。

所謂內核支持與否,可以根據我們上述的設計這樣簡單理解,創建線程之後能被加進內核的就緒隊列然後被內核的調度器調度,那就說明內核是支持的。反之普通的用戶線程,內核是看不到的,內核看到的就只是整個進程,也只會把進程加進就緒隊列,調度進程之後用戶態下的調度器再調度用戶線程。

所以想要真正實現線程機制,內核線程是最基本的要求。內核支持的用戶線程——輕量級進程與內核線程是單射關係,每個輕量級進程都有一個內核線程與之相對應,這就是常說的一對一模型。而普通用戶線程的 CPU 調度實體還是進程,整個進程只對應的一個內核線程,即進程裏面的多個用戶線程也只對應一個內核線程,這就是多對一模型。

總結

看完這個簡單設計和說明,再來總結一番:線程就是運行某個函數,因爲多了 PCB,棧等結構組織所以可以被加進隊列然後被調度運行。進程在線程之上實現,最主要的區別就是有自己的虛擬地址空間,也就是頁表,然後是在 3 特權級用戶態下運行。

1

常見問題說明

進程是資源分配的最小單位,而線程是調度的最小單位

進程有自己的虛擬地址空間,這個空間裏麪包括了各種資源,例如堆,棧,各種段,甚至包括線程的一些必要資源,它們其實都是虛擬地址空間的一塊區域。所以說進程是資源分配的最小單位。

線程呢?線程就是一條執行流,用來執行某個函數,而且線程是依靠進程存在,因爲上面說了,資源都是進程管理的。所以如果進程只有一條執行流,那麼是可以看作線程的。如果裏面有多個線程且爲內核所支持,那麼每個線程是可以被內核單獨調度的。那如果是多個普通的用戶線程呢?這個我認爲看個人理解吧,從內核看調度的實體上的確是整個進程,從用戶態看呢,調度的又是進程裏面的某個線程。

關於普通用戶線程仁者見仁,智者見智吧,不要太陷入理論。能夠在腦海裏面大概地模擬出進程線程是怎麼創建的,怎麼調度的就行了。而且現今的操作系統內核大都支持線程的。所以呢總的來說進程的確是資源分配的最小單位,而線程也是調度的最小單位。

線程提速

線程能給程序運行提速的原理就在於,多個線程能夠並行執行,如果只有一個 CPU,那就是僞並行,也是能提速的。

舉個例子來說明:有 A B 兩個進程,A 有三個線程,a,b,c,B 只有它自己一個執行流。所以調度時,就緒隊列上 A 有三個結點能被調度,而 B 只有一個,這樣算下來 A 運行的速度肯定比 B 快。

但要清楚如果只有一個 CPU 的情況下,由於線程切換的開銷,多線程進程運行的總時間應該是多於單線程進程的。只是說開了多個線程就能夠去搶佔更多的時間片資源,運行的效率會更高。

**使用多線程還有一點能夠提速,就是遇到阻塞的情況,比如說 A 進程的 a 如果阻塞,bc 兩個線程還能正常運行。而如果是 B 運行到某個地方阻塞的話,B 整個進程就阻塞了。**當然這也是討論內核支持的用戶線程,如果是普通的用戶線程,某個線程阻塞也會導致整個進程阻塞,因爲內核是感覺不到多個線程。

2

最後總結

進程線程調度的問題如果只在上層研究,就比如操作系統課上學習的東西,應該還是比較好理解的。但是可能會覺得模糊抽象,要不就是知識點非常熟悉之後產生的理所當然的感覺。而要想真正有個清晰的認識還是得去看看碼,看看是怎麼設計的。

設計也不是那麼簡單吶,需要考慮到方方面面,前前後後的邏輯聯繫,就比如說本文這個設計的線程第一次運行的方式,進入 3 特權級的方式,都是模仿相應結構來設計,使系統的前後保持一致。

而本文呢就相當於是對前輩大佬們的設計再做了精簡的處理,隱去了許多的細節,某些地方也不太規範妥當,還望見諒。比如彙編跟 c 混寫,雖然的確有內聯彙編形式的支持但沒用,就完全當作僞代碼吧。進程線程調度算是三大塊息息相關的內容,不可能七千多字就能講述清楚的,但我想最基本的原理應該說的還是比較清楚的。

好啦,本文就講到這裏,如果哪有錯或不足,還請大家批評指正。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pKdQu74Sl3PIaKyDlIEExg