上一篇文章只是簡單的描述了一下 Linux 基本概念,經過幾個例子來講明 Linux 基本應用程序,而後以 Linux 基本內核構造來結尾。那麼本篇文章咱們就深刻理解一下 Linux 內核來理解 Linux 的基本概念之進程和線程。系統調用是操做系統自己的接口,它對於建立進程和線程,內存分配,共享文件和 I/O 來講都很重要。html
咱們將從各個版本的共性出發來進行探討。node
Linux 一個很是重要的概念就是進程,Linux 進程和咱們在linux
進程和線程 這篇文章算法
中探討的進程模型很是類似。每一個進程都會運行一段獨立的程序,而且在初始化的時候擁有一個獨立的控制線程。換句話說,每一個進程都會有一個本身的程序計數器,這個程序計數器用來記錄下一個須要被執行的指令。Linux 容許進程在運行時建立額外的線程。shell
Linux 是一個多道程序設計系統,所以系統中存在彼此相互獨立的進程同時運行。此外,每一個用戶都會同時有幾個活動的進程。由於若是是一個大型系統,可能有數百上千的進程在同時運行。編程
在某些用戶空間中,即便用戶退出登陸,仍然會有一些後臺進程在運行,這些進程被稱爲 守護進程(daemon)
。數組
Linux 中有一種特殊的守護進程被稱爲 計劃守護進程(Cron daemon)
,計劃守護進程能夠每分鐘醒來一次檢查是否有工做要作,作完會繼續回到睡眠狀態等待下一次喚醒。網絡
Cron 是一個守護程序,能夠作任何你想作的事情,好比說你能夠按期進行系統維護、按期進行系統備份等。在其餘操做系統上也有相似的程序,好比 Mac OS X 上 Cron 守護程序被稱爲
launchd
的守護進程。在 Windows 上能夠被稱爲計劃任務(Task Scheduler)
。數據結構
在 Linux 系統中,進程經過很是簡單的方式來建立,fork
系統調用會建立一個源進程的拷貝(副本)
。調用 fork 函數的進程被稱爲 父進程(parent process)
,使用 fork 函數建立出來的進程被稱爲 子進程(child process)
。父進程和子進程都有本身的內存映像。若是在子進程建立出來後,父進程修改了一些變量等,那麼子進程是看不到這些變化的,也就是 fork 後,父進程和子進程相互獨立。多線程
雖然父進程和子進程保持相互獨立,可是它們卻可以共享相同的文件,若是在 fork 以前,父進程已經打開了某個文件,那麼 fork 後,父進程和子進程仍然共享這個打開的文件。對共享文件的修改會對父進程和子進程同時可見。
那麼該如何區分父進程和子進程呢?子進程只是父進程的拷貝,因此它們幾乎全部的狀況都同樣,包括內存映像、變量、寄存器等。區分的關鍵在於 fork
函數調用後的返回值,若是 fork 後返回一個非零值,這個非零值便是子進程的 進程標識符(Process Identiier, PID)
,而會給子進程返回一個零值,能夠用下面代碼來進行表示
pid = fork(); // 調用 fork 函數建立進程
if(pid < 0){
error() // pid < 0,建立失敗
}
else if(pid > 0){
parent_handle() // 父進程代碼
}
else {
child_handle() // 子進程代碼
}
複製代碼
父進程在 fork 後會獲得子進程的 PID,這個 PID 即能表明這個子進程的惟一標識符也就是 PID。若是子進程想要知道本身的 PID,能夠調用 getpid
方法。當子進程結束運行時,父進程會獲得子進程的 PID,由於一個進程會 fork 不少子進程,子進程也會 fork 子進程,因此 PID 是很是重要的。咱們把第一次調用 fork 後的進程稱爲 原始進程
,一個原始進程能夠生成一顆繼承樹
Linux 進程間的通訊機制一般被稱爲 Internel-Process communication,IPC
下面咱們來講一說 Linux 進程間通訊的機制,大體來講,Linux 進程間的通訊機制能夠分爲 6 種
下面咱們分別對其進行概述
信號是 UNIX 系統最早開始使用的進程間通訊機制,由於 Linux 是繼承於 UNIX 的,因此 Linux 也支持信號機制,經過向一個或多個進程發送異步事件信號
來實現,信號能夠從鍵盤或者訪問不存在的位置等地方產生;信號經過 shell 將任務發送給子進程。
你能夠在 Linux 系統上輸入 kill -l
來列出系統使用的信號,下面是我提供的一些信號
進程能夠選擇忽略發送過來的信號,可是有兩個是不能忽略的:SIGSTOP
和 SIGKILL
信號。SIGSTOP 信號會通知當前正在運行的進程執行關閉操做,SIGKILL 信號會通知當前進程應該被殺死。除此以外,進程能夠選擇它想要處理的信號,進程也能夠選擇阻止信號,若是不阻止,能夠選擇自行處理,也能夠選擇進行內核處理。若是選擇交給內核進行處理,那麼就執行默認處理。
操做系統會中斷目標程序的進程來向其發送信號、在任何非原子指令中,執行均可以中斷,若是進程已經註冊了新號處理程序,那麼就執行進程,若是沒有註冊,將採用默認處理的方式。
例如:當進程收到 SIGFPE
浮點異常的信號後,默認操做是對其進行 dump(轉儲)
和退出。信號沒有優先級的說法。若是同時爲某個進程產生了兩個信號,則能夠將它們呈現給進程或者以任意的順序進行處理。
下面咱們就來看一下這些信號是幹什麼用的
SIGABRT 和 SIGIOT 信號發送給進程,告訴其進行終止,這個 信號一般在調用 C標準庫的abort()
函數時由進程自己啓動
當設置的時鐘功能超時時會將 SIGALRM 、 SIGVTALRM、SIGPROF 發送給進程。當實際時間或時鐘時間超時時,發送 SIGALRM。 當進程使用的 CPU 時間超時時,將發送 SIGVTALRM。 當進程和系統表明進程使用的CPU 時間超時時,將發送 SIGPROF。
SIGBUS 將形成總線中斷
錯誤時發送給進程
當子進程終止、被中斷或者被中斷恢復,將 SIGCHLD 發送給進程。此信號的一種常見用法是指示操做系統在子進程終止後清除其使用的資源。
SIGCONT 信號指示操做系統繼續執行先前由 SIGSTOP 或 SIGTSTP 信號暫停的進程。該信號的一個重要用途是在 Unix shell 中的做業控制中。
SIGFPE 信號在執行錯誤的算術運算(例如除以零)時將被髮送到進程。
當 SIGUP 信號控制的終端關閉時,會發送給進程。許多守護程序將從新加載其配置文件並從新打開其日誌文件,而不是在收到此信號時退出。
SIGILL 信號在嘗試執行非法、格式錯誤、未知或者特權指令時發出
當用戶但願中斷進程時,操做系統會向進程發送 SIGINT 信號。用戶輸入 ctrl - c 就是但願中斷進程。
SIGKILL 信號發送到進程以使其立刻進行終止。 與 SIGTERM 和 SIGINT 相比,這個信號沒法捕獲和忽略執行,而且進程在接收到此信號後沒法執行任何清理操做,下面是一些例外狀況
殭屍進程沒法殺死,由於殭屍進程已經死了,它在等待父進程對其進行捕獲
處於阻塞狀態的進程只有再次喚醒後纔會被 kill 掉
init
進程是 Linux 的初始化進程,這個進程會忽略任何信號。
SIGKILL 一般是做爲最後殺死進程的信號、它一般做用於 SIGTERM 沒有響應時發送給進程。
SIGPIPE 嘗試寫入進程管道時發現管道未鏈接沒法寫入時發送到進程
當在明確監視的文件描述符上發生事件時,將發送 SIGPOLL 信號。
SIGRTMIN 至 SIGRTMAX 是實時信號
當用戶請求退出進程並執行核心轉儲時,SIGQUIT 信號將由其控制終端發送給進程。
當 SIGSEGV 信號作出無效的虛擬內存引用或分段錯誤時,即在執行分段違規時,將其發送到進程。
SIGSTOP 指示操做系統終止以便之後進行恢復時
當 SIGSYS 信號將錯誤參數傳遞給系統調用時,該信號將發送到進程。
咱們上面簡單提到過了 SYSTERM 這個名詞,這個信號發送給進程以請求終止。與 SIGKILL 信號不一樣,該信號能夠被過程捕獲或忽略。這容許進程執行良好的終止,從而釋放資源並在適當時保存狀態。 SIGINT 與SIGTERM 幾乎相同。
SIGTSTP 信號由其控制終端發送到進程,以請求終端中止。
當 SIGTTIN 和SIGTTOU 信號分別在後臺嘗試從 tty 讀取或寫入時,信號將發送到該進程。
在發生異常或者 trap 時,將 SIGTRAP 信號發送到進程
當套接字具備可讀取的緊急或帶外數據時,將 SIGURG 信號發送到進程。
SIGUSR1 和 SIGUSR2 信號被髮送到進程以指示用戶定義的條件。
當 SIGXCPU 信號耗盡 CPU 的時間超過某個用戶可設置的預約值時,將其發送到進程
當 SIGXFSZ 信號增加超過最大容許大小的文件時,該信號將發送到該進程。
SIGWINCH 信號在其控制終端更改其大小(窗口更改)時發送給進程。
Linux 系統中的進程能夠經過創建管道 pipe 進行通訊
在兩個進程之間,能夠創建一個通道,一個進程向這個通道里寫入字節流,另外一個進程從這個管道中讀取字節流。管道是同步的,當進程嘗試從空管道讀取數據時,該進程會被阻塞,直到有可用數據爲止。shell 中的管線 pipelines
就是用管道實現的,當 shell 發現輸出
sort <f | head
複製代碼
它會建立兩個進程,一個是 sort,一個是 head,sort,會在這兩個應用程序之間創建一個管道使得 sort 進程的標準輸出做爲 head 程序的標準輸入。sort 進程產生的輸出就不用寫到文件中了,若是管道滿了系統會中止 sort 以等待 head 讀出數據
管道實際上就是 |
,兩個應用程序不知道有管道的存在,一切都是由 shell 管理和控制的。
兩個進程之間還能夠經過共享內存進行進程間通訊,其中兩個或者多個進程能夠訪問公共內存空間。兩個進程的共享工做是經過共享內存完成的,一個進程所做的修改能夠對另外一個進程可見(很像線程間的通訊)。
在使用共享內存前,須要通過一系列的調用流程,流程以下
(shmget())
(shmat())
(shmdt())
(shmctl())
先入先出隊列 FIFO 一般被稱爲 命名管道(Named Pipes)
,命名管道的工做方式與常規管道很是類似,可是確實有一些明顯的區別。未命名的管道沒有備份文件:操做系統負責維護內存中的緩衝區,用來將字節從寫入器傳輸到讀取器。一旦寫入或者輸出終止的話,緩衝區將被回收,傳輸的數據會丟失。相比之下,命名管道具備支持文件和獨特 API ,命名管道在文件系統中做爲設備的專用文件存在。當全部的進程通訊完成後,命名管道將保留在文件系統中以備後用。命名管道具備嚴格的 FIFO 行爲
寫入的第一個字節是讀取的第一個字節,寫入的第二個字節是讀取的第二個字節,依此類推。
一聽到消息隊列這個名詞你可能不知道是什麼意思,消息隊列是用來描述內核尋址空間內的內部連接列表。能夠按幾種不一樣的方式將消息按順序發送到隊列並從隊列中檢索消息。每一個消息隊列由 IPC 標識符惟一標識。消息隊列有兩種模式,一種是嚴格模式
, 嚴格模式就像是 FIFO 先入先出隊列似的,消息順序發送,順序讀取。還有一種模式是 非嚴格模式
,消息的順序性不是很是重要。
還有一種管理兩個進程間通訊的是使用 socket
,socket 提供端到端的雙相通訊。一個套接字能夠與一個或多個進程關聯。就像管道有命令管道和未命名管道同樣,套接字也有兩種模式,套接字通常用於兩個進程之間的網絡通訊,網絡套接字須要來自諸如TCP(傳輸控制協議)
或較低級別UDP(用戶數據報協議)
等基礎協議的支持。
套接字有如下幾種分類
順序包套接字(Sequential Packet Socket)
: 此類套接字爲最大長度固定的數據報提供可靠的鏈接。此鏈接是雙向的而且是順序的。數據報套接字(Datagram Socket)
:數據包套接字支持雙向數據流。數據包套接字接受消息的順序與發送者可能不一樣。流式套接字(Stream Socket)
:流套接字的工做方式相似於電話對話,提供雙向可靠的數據流。原始套接字(Raw Socket)
: 可使用原始套接字訪問基礎通訊協議。如今關注一下 Linux 系統中與進程管理相關的系統調用。在瞭解以前你須要先知道一下什麼是系統調用。
操做系統爲咱們屏蔽了硬件和軟件的差別,它的最主要功能就是爲用戶提供一種抽象,隱藏內部實現,讓用戶只關心在 GUI 圖形界面下如何使用便可。操做系統能夠分爲兩種模式
咱們常說的上下文切換
指的就是內核態模式和用戶態模式的頻繁切換。而系統調用
指的就是引發內核態和用戶態切換的一種方式,系統調用一般在後臺靜默運行,表示計算機程序向其操做系統內核請求服務。
系統調用指令有不少,下面是一些與進程管理相關的最主要的系統調用
fork 調用用於建立一個與父進程相同的子進程,建立完進程後的子進程擁有和父進程同樣的程序計數器、相同的 CPU 寄存器、相同的打開文件。
exec 系統調用用於執行駐留在活動進程中的文件,調用 exec 後,新的可執行文件會替換先前的可執行文件並得到執行。也就是說,調用 exec 後,會將舊文件或程序替換爲新文件或執行,而後執行文件或程序。新的執行程序被加載到相同的執行空間中,所以進程的 PID
不會修改,由於咱們沒有建立新進程,只是替換舊進程。可是進程的數據、代碼、堆棧都已經被修改。若是當前要被替換的進程包含多個線程,那麼全部的線程將被終止,新的進程映像被加載執行。
這裏須要解釋一下進程映像(Process image)
的概念
什麼是進程映像呢?進程映像是執行程序時所須要的可執行文件,一般會包括下面這些東西
又稱文本段,用來存放指令,運行代碼的一塊內存空間
此空間大小在代碼運行前就已經肯定
內存空間通常屬於只讀,某些架構的代碼也容許可寫
在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。
可讀可寫
存儲初始化的全局變量和初始化的 static 變量
數據段中數據的生存期是隨程序持續性(隨進程持續性) 隨進程持續性:進程建立就存在,進程死亡就消失
可讀可寫
存儲未初始化的全局變量和未初始化的 static 變量
bss 段中的數據通常默認爲 0
是可讀寫的,由於變量的值能夠在運行時更改。此段的大小也固定。
可讀可寫
存儲的是函數或代碼中的局部變量(非 static 變量)
棧的生存期隨代碼塊持續性,代碼塊運行就給你分配空間,代碼塊結束,就自動回收空間
可讀可寫
存儲的是程序運行期間動態分配的 malloc/realloc 的空間
堆的生存期隨進程持續性,從 malloc/realloc 到 free 一直存在
下面是這些區域的構成圖
exec 系統調用是一些函數的集合,這些函數是
下面來看一下 exec 的工做原理
等待子進程結束或終止
在許多計算機操做系統上,計算機進程的終止是經過執行 exit
系統調用命令執行的。0 表示進程可以正常結束,其餘值表示進程以非正常的行爲結束。
其餘一些常見的系統調用以下
系統調用指令 | 描述 |
---|---|
pause | 掛起信號 |
nice | 改變分時進程的優先級 |
ptrace | 進程跟蹤 |
kill | 向進程發送信號 |
pipe | 建立管道 |
mkfifo | 建立 fifo 的特殊文件(命名管道) |
sigaction | 設置對指定信號的處理方法 |
msgctl | 消息控制操做 |
semctl | 信號量控制 |
Linux 進程就像一座冰山,你看到的只是冰山一角。
在 Linux 內核結構中,進程會被表示爲 任務
,經過結構體 structure
來建立。不像其餘的操做系統會區分進程、輕量級進程和線程,Linux 統一使用任務結構來表明執行上下文。所以,對於每一個單線程進程來講,單線程進程將用一個任務結構表示,對於多線程進程來講,將爲每個用戶級線程分配一個任務結構。Linux 內核是多線程的,而且內核級線程不與任何用戶級線程相關聯。
對於每一個進程來講,在內存中都會有一個 task_struct
進程描述符與之對應。進程描述符包含了內核管理進程全部有用的信息,包括 調度參數、打開文件描述符等等。進程描述符從進程建立開始就一直存在於內核堆棧中。
Linux 和 Unix 同樣,都是經過 PID
來區分不一樣的進程,內核會將全部進程的任務結構組成爲一個雙向鏈表。PID 可以直接被映射稱爲進程的任務結構所在的地址,從而不須要遍歷雙向鏈表直接訪問。
咱們上面提到了進程描述符,這是一個很是重要的概念,咱們上面還提到了進程描述符是位於內存中的,這裏咱們省略了一句話,那就是進程描述符是存在用戶的任務結構中,當進程位於內存並開始運行時,進程描述符纔會被調入內存。
進程位於內存
被稱爲PIM(Process In Memory)
,這是馮諾伊曼體系架構的一種體現,加載到內存中並執行的程序稱爲進程。簡單來講,一個進程就是正在執行的程序。
進程描述符能夠歸爲下面這幾類
調度參數(scheduling parameters)
:進程優先級、最近消耗 CPU 的時間、最近睡眠時間一塊兒決定了下一個須要運行的進程內存映像(memory image)
:咱們上面說到,進程映像是執行程序時所須要的可執行文件,它由數據和代碼組成。信號(signals)
:顯示哪些信號被捕獲、哪些信號被執行寄存器
:當發生內核陷入 (trap) 時,寄存器的內容會被保存下來。系統調用狀態(system call state)
:當前系統調用的信息,包括參數和結果文件描述符表(file descriptor table)
:有關文件描述符的系統被調用時,文件描述符做爲索引在文件描述符表中定位相關文件的 i-node 數據結構統計數據(accounting)
:記錄用戶、進程佔用系統 CPU 時間表的指針,一些操做系統還保存進程最多佔用的 CPU 時間、進程擁有的最大堆棧空間、進程能夠消耗的頁面數等。內核堆棧(kernel stack)
:進程的內核部分可使用的固定堆棧其餘
: 當前進程狀態、事件等待時間、距離警報的超時時間、PID、父進程的 PID 以及用戶標識符等有了上面這些信息,如今就很容易描述在 Linux 中是如何建立這些進程的了,建立新流程實際上很是簡單。爲子進程開闢一塊新的用戶空間的進程描述符,而後從父進程複製大量的內容。爲這個子進程分配一個 PID,設置其內存映射,賦予它訪問父進程文件的權限,註冊並啓動。
當執行 fork 系統調用時,調用進程會陷入內核並建立一些和任務相關的數據結構,好比內核堆棧(kernel stack)
和 thread_info
結構。
關於 thread_info 結構能夠參考
這個結構中包含進程描述符,進程描述符位於固定的位置,使得 Linux 系統只須要很小的開銷就能夠定位到一個運行中進程的數據結構。
進程描述符的主要內容是根據父進程
的描述符來填充。Linux 操做系統會尋找一個可用的 PID,而且此 PID 沒有被任何進程使用,更新進程標示符使其指向一個新的數據結構便可。爲了減小 hash table 的碰撞,進程描述符會造成鏈表
。它還將 task_struct 的字段設置爲指向任務數組上相應的上一個/下一個進程。
task_struct : Linux 進程描述符,內部涉及到衆多 C++ 源碼,咱們會在後面進行講解。
從原則上來講,爲子進程開闢內存區域併爲子進程分配數據段、堆棧段,而且對父進程的內容進行復制,可是實際上 fork 完成後,子進程和父進程沒有共享內存,因此須要複製技術來實現同步,可是複製開銷比較大,所以 Linux 操做系統使用了一種 欺騙
方式。即爲子進程分配頁表,而後新分配的頁表指向父進程的頁面,同時這些頁面是隻讀的。當進程向這些頁面進行寫入的時候,會開啓保護錯誤。內核發現寫入操做後,會爲進程分配一個副本,使得寫入時把數據複製到這個副本上,這個副本是共享的,這種方式稱爲 寫入時複製(copy on write)
,這種方式避免了在同一塊內存區域維護兩個副本的必要,節省內存空間。
在子進程開始運行後,操做系統會調用 exec 系統調用,內核會進行查找驗證可執行文件,把參數和環境變量複製到內核,釋放舊的地址空間。
如今新的地址空間須要被建立和填充。若是系統支持映射文件,就像 Unix 系統同樣,那麼新的頁表就會建立,代表內存中沒有任何頁,除非所使用的頁面是堆棧頁,其地址空間由磁盤上的可執行文件支持。新進程開始運行時,馬上會收到一個缺頁異常(page fault)
,這會使具備代碼的頁面加載進入內存。最後,參數和環境變量被複制到新的堆棧中,重置信號,寄存器所有清零。新的命令開始運行。
下面是一個示例,用戶輸出 ls,shell 會調用 fork 函數複製一個新進程,shell 進程會調用 exec 函數用可執行文件 ls 的內容覆蓋它的內存。
如今咱們來討論一下 Linux 中的線程,線程是輕量級的進程,想必這句話你已經聽過不少次了,輕量級
體如今全部的進程切換都須要清除全部的表、進程間的共享信息也比較麻煩,通常來講經過管道或者共享內存,若是是 fork 函數後的父子進程則使用共享文件,然而線程切換不須要像進程同樣具備昂貴的開銷,並且線程通訊起來也更方便。線程分爲兩種:用戶級線程和內核級線程
用戶級線程避免使用內核,一般,每一個線程會顯示調用開關,發送信號或者執行某種切換操做來放棄 CPU,一樣,計時器能夠強制進行開關,用戶線程的切換速度一般比內核線程快不少。在用戶級別實現線程會有一個問題,即單個線程可能會壟斷 CPU 時間片,致使其餘線程沒法執行從而 餓死
。若是執行一個 I/O 操做,那麼 I/O 會阻塞,其餘線程也沒法運行。
一種解決方案是,一些用戶級的線程包解決了這個問題。可使用時鐘週期的監視器來控制第一時間時間片獨佔。而後,一些庫經過特殊的包裝來解決系統調用的 I/O 阻塞問題,或者能夠爲非阻塞 I/O 編寫任務。
內核級線程一般使用幾個進程表在內核中實現,每一個任務都會對應一個進程表。在這種狀況下,內核會在每一個進程的時間片內調度每一個線程。
全部可以阻塞的調用都會經過系統調用的方式來實現,當一個線程阻塞時,內核能夠進行選擇,是運行在同一個進程中的另外一個線程(若是有就緒線程的話)仍是運行一個另外一個進程中的線程。
從用戶空間 -> 內核空間 -> 用戶空間的開銷比較大,可是線程初始化的時間損耗能夠忽略不計。這種實現的好處是由時鐘決定線程切換時間,所以不太可能將時間片與任務中的其餘線程佔用時間綁定到一塊兒。一樣,I/O 阻塞也不是問題。
結合用戶空間和內核空間的優勢,設計人員採用了一種內核級線程
的方式,而後將用戶級線程與某些或者所有內核線程多路複用起來
在這種模型中,編程人員能夠自由控制用戶線程和內核線程的數量,具備很大的靈活度。採用這種方法,內核只識別內核級線程,並對其進行調度。其中一些內核級線程會被多個用戶級線程多路複用。
下面咱們來關注一下 Linux 系統的調度算法,首先須要認識到,Linux 系統的線程是內核線程,因此 Linux 系統是基於線程的,而不是基於進程的。
爲了進行調度,Linux 系統將線程分爲三類
實時先入先出線程具備最高優先級,它不會被其餘線程所搶佔,除非那是一個剛剛準備好的,擁有更高優先級的線程進入。實時輪轉線程與實時先入先出線程基本相同,只是每一個實時輪轉線程都有一個時間量,時間到了以後就能夠被搶佔。若是多個實時線程準備完畢,那麼每一個線程運行它時間量所規定的時間,而後插入到實時輪轉線程末尾。
注意這個實時只是相對的,沒法作到絕對的實時,由於線程的運行時間沒法肯定。它們相對分時系統來講,更加具備實時性
Linux 系統會給每一個線程分配一個 nice
值,這個值表明了優先級的概念。nice 值默認值是 0 ,可是能夠經過系統調用 nice 值來修改。修改值的範圍從 -20 - +19。nice 值決定了線程的靜態優先級。通常系統管理員的 nice 值會比通常線程的優先級高,它的範圍是 -20 - -1。
下面咱們更詳細的討論一下 Linux 系統的兩個調度算法,它們的內部與調度隊列(runqueue)
的設計很類似。運行隊列有一個數據結構用來監視系統中全部可運行的任務並選擇下一個能夠運行的任務。每一個運行隊列和系統中的每一個 CPU 有關。
Linux O(1)
調度器是歷史上很流行的一個調度器。這個名字的由來是由於它可以在常數時間內執行任務調度。在 O(1) 調度器裏,調度隊列被組織成兩個數組,一個是任務正在活動的數組,一個是任務過時失效的數組。以下圖所示,每一個數組都包含了 140 個鏈表頭,每一個鏈表頭具備不一樣的優先級。
大體流程以下:
調度器從正在活動數組中選擇一個優先級最高的任務。若是這個任務的時間片過時失效了,就把它移動到過時失效數組中。若是這個任務阻塞了,好比說正在等待 I/O 事件,那麼在它的時間片過時失效以前,一旦 I/O 操做完成,那麼這個任務將會繼續運行,它將被放回到以前正在活動的數組中,由於這個任務以前已經消耗一部分 CPU 時間片,因此它將運行剩下的時間片。當這個任務運行完它的時間片後,它就會被放到過時失效數組中。一旦正在活動的任務數組中沒有其餘任務後,調度器將會交換指針,使得正在活動的數組變爲過時失效數組,過時失效數組變爲正在活動的數組。使用這種方式能夠保證每一個優先級的任務都可以獲得執行,不會致使線程飢餓。
在這種調度方式中,不一樣優先級的任務所獲得 CPU 分配的時間片也是不一樣的,高優先級進程每每能獲得較長的時間片,低優先級的任務獲得較少的時間片。
這種方式爲了保證可以更好的提供服務,一般會爲 交互式進程
賦予較高的優先級,交互式進程就是用戶進程
。
Linux 系統不知道一個任務到底是 I/O 密集型的仍是 CPU 密集型的,它只是依賴於交互式的方式,Linux 系統會區分是靜態優先級
仍是 動態優先級
。動態優先級是採用一種獎勵機制來實現的。獎勵機制有兩種方式:獎勵交互式線程、懲罰佔用 CPU 的線程。在 Linux O(1) 調度器中,最高的優先級獎勵是 -5,注意這個優先級越低越容易被線程調度器接受,因此最高懲罰的優先級是 +5。具體體現就是操做系統維護一個名爲 sleep_avg
的變量,任務喚醒會增長 sleep_avg 變量的值,當任務被搶佔或者時間量過時會減小這個變量的值,反映在獎勵機制上。
O(1) 調度算法是 2.6 內核版本的調度器,最初引入這個調度算法的是不穩定的 2.5 版本。早期的調度算法在多處理器環境中說明了經過訪問正在活動數組就能夠作出調度的決定。使調度能夠在固定的時間 O(1) 完成。
O(1) 調度器使用了一種 啓發式
的方式,這是什麼意思?
在計算機科學中,啓發式是一種當傳統方式解決問題很慢時用來快速解決問題的方式,或者找到一個在傳統方法沒法找到任何精確解的狀況下找到近似解。
O(1) 使用啓發式的這種方式,會使任務的優先級變得複雜而且不完善,從而致使在處理交互任務時性能很糟糕。
爲了改進這個缺點,O(1) 調度器的開發者又提出了一個新的方案,即 公平調度器(Completely Fair Scheduler, CFS)
。 CFS 的主要思想是使用一顆紅黑樹
做爲調度隊列。
數據結構過重要了。
CFS 會根據任務在 CPU 上的運行時間長短而將其有序地排列在樹中,時間精確到納秒級。下面是 CFS 的構造模型
CFS 的調度過程以下:
CFS 算法老是優先調度哪些使用 CPU 時間最少的任務。最小的任務通常都是在最左邊的位置。當有一個新的任務須要運行時,CFS 會把這個任務和最左邊的數值進行對比,若是此任務具備最小時間值,那麼它將進行運行,不然它會進行比較,找到合適的位置進行插入。而後 CPU 運行紅黑樹上當前比較的最左邊的任務。
在紅黑樹中選擇一個節點來運行的時間能夠是常數時間,可是插入一個任務的時間是 O(loog(N))
,其中 N 是系統中的任務數。考慮到當前系統的負載水平,這是能夠接受的。
調度器只須要考慮可運行的任務便可。這些任務被放在適當的調度隊列中。不可運行的任務和正在等待的各類 I/O 操做或內核事件的任務被放入一個等待隊列
中。等待隊列頭包含一個指向任務鏈表的指針和一個自旋鎖。自旋鎖對於併發處理場景下用處很大。
下面來聊一下 Linux 中的同步機制。早期的 Linux 內核只有一個 大內核鎖(Big Kernel Lock,BKL)
。它阻止了不一樣處理器併發處理的能力。所以,須要引入一些粒度更細的鎖機制。
Linux 提供了若干不一樣類型的同步變量,這些變量既可以在內核中使用,也可以在用戶應用程序中使用。在地層中,Linux 經過使用 atomic_set
和 atomic_read
這樣的操做爲硬件支持的原子指令提供封裝。硬件提供內存重排序,這是 Linux 屏障的機制。
具備高級別的同步像是自旋鎖的描述是這樣的,當兩個進程同時對資源進行訪問,在一個進程得到資源後,另外一個進程不想被阻塞,因此它就會自旋,等待一下子再對資源進行訪問。Linux 也提供互斥量或信號量這樣的機制,也支持像是 mutex_tryLock
和 mutex_tryWait
這樣的非阻塞調用。也支持中斷處理事務,也能夠經過動態禁用和啓用相應的中斷來實現。
下面來聊一聊 Linux 是如何啓動的。
當計算機電源通電後,BIOS
會進行開機自檢(Power-On-Self-Test, POST)
,對硬件進行檢測和初始化。由於操做系統的啓動會使用到磁盤、屏幕、鍵盤、鼠標等設備。下一步,磁盤中的第一個分區,也被稱爲 MBR(Master Boot Record)
主引導記錄,被讀入到一個固定的內存區域並執行。這個分區中有一個很是小的,只有 512 字節的程序。程序從磁盤中調入 boot 獨立程序,boot 程序將自身複製到高位地址的內存從而爲操做系統釋放低位地址的內存。
複製完成後,boot 程序讀取啓動設備的根目錄。boot 程序要理解文件系統和目錄格式。而後 boot 程序被調入內核,把控制權移交給內核。直到這裏,boot 完成了它的工做。系統內核開始運行。
內核啓動代碼是使用彙編語言
完成的,主要包括建立內核堆棧、識別 CPU 類型、計算內存、禁用中斷、啓動內存管理單元等,而後調用 C 語言的 main 函數執行操做系統部分。
這部分也會作不少事情,首先會分配一個消息緩衝區來存放調試出現的問題,調試信息會寫入緩衝區。若是調試出現錯誤,這些信息能夠經過診斷程序調出來。
而後操做系統會進行自動配置,檢測設備,加載配置文件,被檢測設備若是作出響應,就會被添加到已連接的設備表中,若是沒有相應,就歸爲未鏈接直接忽略。
配置完全部硬件後,接下來要作的就是仔細手工處理進程0,設置其堆棧,而後運行它,執行初始化、配置時鐘、掛載文件系統。建立 init 進程(進程 1 )
和 守護進程(進程 2)
。
init 進程會檢測它的標誌以肯定它是否爲單用戶仍是多用戶服務。在前一種狀況中,它會調用 fork 函數建立一個 shell 進程,而且等待這個進程結束。後一種狀況調用 fork 函數建立一個運行系統初始化的 shell 腳本(即 /etc/rc)的進程,這個進程能夠進行文件系統一致性檢測、掛載文件系統、開啓守護進程等。
而後 /etc/rc 這個進程會從 /etc/ttys 中讀取數據,/etc/ttys 列出了全部的終端和屬性。對於每個啓用的終端,這個進程調用 fork 函數建立一個自身的副本,進行內部處理並運行一個名爲 getty
的程序。
getty 程序會在終端上輸入
login:
複製代碼
等待用戶輸入用戶名,在輸入用戶名後,getty 程序結束,登錄程序 /bin/login
開始運行。login 程序須要輸入密碼,並與保存在 /etc/passwd
中的密碼進行對比,若是輸入正確,login 程序以用戶 shell 程序替換自身,等待第一個命令。若是不正確,login 程序要求輸入另外一個用戶名。
整個系統啓動過程以下