參考:《Linux內核設計與實現》讀書筆記
原理 | 優點 | 劣勢 | |
---|---|---|---|
單內核 | 總體上做爲一個單獨的大過程來實現,整個內核都在一個大內核地址空間上運行。 | 1. 簡單。 2. 高效:全部內核都在一個大的地址空間上,因此內核各個功能之間的調用和調用函數相似,幾乎沒有性能開銷。 | 一個功能的崩潰會致使整個內核沒法使用。 |
微內核 | 內核按功能被劃分紅多個獨立的過程。每一個過程獨立的運行在本身的地址空間上。 | 1. 安全:內核的各類服務獨立運行,一種服務掛了不會影響其餘服務。 | 內核各個服務之間經過進程間通訊互通消息,比較複雜且效率低。 |
由於 IPC 機制的開銷多於函數調用,又由於會涉及內核空間與用戶空間的上下文切換,所以,消息傳遞須要必定的週期,而單內核中簡單的函數調用沒有這些開銷。html
Linux的內核雖然是基於單內核的,運行在單獨的內核地址空間上。可是通過這麼多年的發展,也具有微內核的一些特徵。(體現了Linux實用至上的原則)linux
主要有如下特徵:git
內核的版本號主要有四個數組組成。好比版本號:2.6.26.1 其中:數組
2 - 主版本號緩存
6 - 從版本號或副版本號安全
26 - 修訂版本號網絡
1 - 穩定版本號併發
副版本號表示這個版本是穩定版(偶數)仍是開發版(奇數),上面例子中的版本號是穩定版。dom
穩定的版本可用於企業級環境。異步
修訂版本號的升級包括BUG修正,新的驅動以及新的特性的追加。
穩定版本號主要是一些關鍵性BUG的修改。
內核是開源的,全部獲取源碼特別方便,參照如下的網址,能夠經過git或者直接下載壓縮好的源碼包。
目錄 | 說明 |
---|---|
arch | 特定體系結構的代碼 |
block | 塊設備I/O層 |
crypo | 加密API |
Documentation | 內核源碼文檔 |
drivers | 設備驅動程序 |
firmware | 使用某些驅動程序而須要的設備固件 |
fs | VFS和各類文件系統 |
include | 內核頭文件 |
init | 內核引導和初始化 |
ipc | 進程間通訊代碼 |
kernel | 像調度程序這樣的核心子系統 |
lib | 一樣內核函數 |
mm | 內存管理子系統和VM |
net | 網絡子系統 |
samples | 示例,示範代碼 |
scripts | 編譯內核所用的腳本 |
security | Linux 安全模塊 |
sound | 語音子系統 |
usr | 早期用戶空間代碼(所謂的initramfs) |
tools | 在Linux開發中有用的工具 |
virt | 虛擬化基礎結構 |
還未實際嘗試過手動編譯內核,只是用yum更新過內核。這部分等之後手動編譯過再補上。
安裝新的內核後,重啓時會提示進入哪一個內核。當屢次安裝新的內核後,啓動列表會很長(由於有不少版本的內核),顯得不是很方便。
下面介紹3種刪除那些不用的內核的方法:(是如何安裝的就選擇相應的刪除方法)
rpm -qa | grep kernel* (查找全部linux內核版本)
rpm -e kernel-(想要刪除的版本)
yum remove kernel-(要刪除的版本)
刪除/lib/modules/目錄下不須要的內核庫文件
刪除/usr/src/kernel/目錄下不須要的內核源碼
刪除/boot目錄下啓動的核心檔案禾內核映像
更改grub的配置,刪除不須要的內核啓動列表
爲了保證內核的小和高效,內核開發中不能使用C標準庫,因此連最經常使用的printf函數也沒有,可是還好有個printk函數來代替。
由於使用GNU C,全部內核中常使用GNU C中的一些擴展:
內聯函數在編譯時會在它被調用的地方展開,減小了函數調用的開銷,性能較好。可是,頻繁的使用內聯函數也會使代碼變長,從而在運行時佔用更多的內存。
因此內聯函數使用時最好要知足如下幾點:函數較小,會被反覆調用,對程序的時間要求比較嚴格。
內聯函數示例:static inline void sample();
內聯彙編
內聯彙編用於偏近底層或對執行時間嚴格要求的地方。示例以下:
unsigned int low, high; asm volatile("rdtsc" : "=a" (low), "=d" (high)); /* low 和 high 分別包含64位時間戳的低32位和高32位 */
分支聲明
若是能事先判斷一個if語句時常常爲真仍是常常爲假,那麼能夠用unlikely和likely來優化這段判斷的代碼。
/* 若是error在絕大多數狀況下爲0(假) */ if (unlikely(error)) { /* ... */ } /* 若是success在絕大多數狀況下不爲0(真) */ if (likely(success)) { /* ... */ }
由於內核是最低層的程序,因此若是內核訪問的非法內存,那麼整個系統都會掛掉!因此內核開發的風險比用戶程序開發的風險要大。
內核中的內存是不分頁的,每用一個字節的內存,物理內存就少一個字節。因此內核中使用內存必定要謹慎。
內核不能完美的支持浮點操做,使用浮點數時,須要人工保存和恢復浮點寄存器及其餘一些繁瑣的操做。
內核棧的大小有編譯內核時決定的,對於不用的體系結構,內核棧的大小雖然不同,但都是固定的。
查看內核棧大小的方法:
ulimit -a | grep "stack size"
Linux是多用戶的操做系統,因此必須處理好同步和併發操做,防止因競爭而出現死鎖。
內核很容易產生競爭條件。和單線程的用戶空間程序不一樣,內核的許多特性都要求可以併發地訪問共享數據,這就要求有同步機制以保證不出現競爭條件,特別是:
經常使用的解決競爭的辦法是自旋鎖和信號量。
Linux內核可用於不用的體現結構,支持多種硬件。因此開發時要時刻注意可移植性,儘可能使用體系結構無關的代碼。
程序自己並非進程,進程是處於執行期的程序以及相關的資源的總稱。
進程和線程是程序運行時狀態,是動態變化的,進程和線程的管理操做(好比,建立,銷燬等)都由內核來實現的。
Linux中不嚴格區分進程和線程,對Linux而言線程不過是一種特殊的進程。
現代操做系統中,進程提供2種虛擬機制:虛擬處理器和虛擬內存
每一個進程有獨立的虛擬處理器和虛擬內存
在進程中的各個線程之間能夠共享虛擬內存,但每一個都擁有各自的虛擬處理器。
進程的建立與退出:
內核中進程的信息主要保存在task_struct中(include/linux/sched.h)
進程標識PID和線程標識TID對於同一個進程或線程來講都是相等的。
Linux中能夠用ps命令查看全部進程的信息:
ps -eo pid,tid,ppid,comm
內核把進程的列表存放在叫作任務隊列 (task list) 的雙向循環鏈表中。鏈表中的每一項都是類型爲 task_struct 稱爲進程描述符 (process descriptor) 的結構,該結構定義在 include/linux/sched.h文件中。進程描述符中包含一個具體進程的全部信息。
進程描述符中包含的數據能完整地描述一個正在執行的程序:它打開的文件,進程的地址空間,掛起的信號,進程的狀態,等等。
<div align="center"> <img src="https://gitee.com//MrRen-sdhm/Images/raw/master/img/20200523212758.png" alt="image-20200523212748578" style="zoom:67%;" /> </div>
Linux 經過 slab 分配器分配 task_struct 結構,這樣能達到對象複用和緩存着色 (cache coloring) 的目的。
在2.6版本以後用 slab 分配器動態生成 task_struct,因此只需在棧底(對於向下增加的棧來講)或棧頂(對於向上增加的棧來講)建立一個新的結構 struct thread_ info。
在 x86 上, struct thread_ info 在文件 <asm/tbread_info.b> 中定義以下:
struct thread_info { struct task_struct *task; struct exec_domain *exec_domain; __u32 flags; __u32 status; __u32 cpu; int preempt_count; mm_segment_t addr_limit; struct restart_block restart_block; void *sysenter_return; int uaccess err; } ;
<img src="https://gitee.com//MrRen-sdhm/Images/raw/master/img/20200523213612.png" alt="image-20200523213604586" style="zoom:67%;" />
每一個任務的 thread_info 結構在它的內核棧的尾端分配。結構中 task 域中存放的是指向該任務實際 task_struct 的指針。
內核中大部分處理進程的代碼都是直接經過 task_struct 進行的。所以,經過 current 宏查找到當前正在運行進程的進程描述符的速度就顯得尤其重要。
硬件體系結構不一樣,該宏的實現也不一樣,它必須針對專門的硬件體系結構作處理。有的硬件體系結構能夠拿出一個專門寄存器來存放指向當前進程 task_struct 的指針,用千加快訪問速度。而有些像 x86 這樣的體系結構(其寄存器並不富餘),就只能在內核棧的尾端建立 thread_info 結構,經過計算偏移間接地查找 task_struct 結構。
進程描述符中的 state 域描述了進程的當前狀態 。系統中的每一個進程都必然處於五種進程狀態中的一種。
<img src="https://gitee.com//MrRen-sdhm/Images/raw/master/img/20200523211906.png" alt="image-20200523211853355" style="zoom:67%;" />
進程的各個狀態之間的轉化構成了進程的整個生命週期。
內核常常須要調整某個進程的狀態。這時最好使用 set_task_state(task, state) 函數:
set_task_state(task, state); /*將任務task的狀態設置爲state*/
該函數將指定的進程設置爲指定的狀態。必要的時候,它會設置內存屏障來強制其餘處理器做從新排序。不然,它等價於:
task->state = state;
通常程序在用戶空間執行。當一個程序執行了系統調用或者觸發了某個異常,它就陷入了內核空間。此時,咱們稱內核「表明進程執行」並處於進程上下文中。在此上下文中 current 宏是有效的。除非在此間隙有更高優先級的進程須要執行並由調度器作出了相應調整,不然在內核退出的時候,程序恢復在用戶空間會繼續執行。
系統調用和異常處理程序是對內核明肯定義的接口。進程只有經過這些接口才能陷入內核執行——對內核的全部訪間都必須經過這些接口。
Linux 系統中進程之間存在一個明顯的繼承關係,全部的進程都是 PID 爲 1 的 init 進程的後代。內核在系統啓動的最後階段啓動 init 進程。
init 進程讀取系統的初始化腳本 (initscript) 並執行其餘的相關程序,最終完成系統啓動的整個過程。
系統中的每一個進程必有一個父進程,相應的,每一個進程也能夠擁有零個或多個子進程,每一個 task_struct 都
包含一個指向其父進程 tast_struct 的 parent 指針,還包含一個稱爲 children 的子進程鏈表。
init 進程的進程描述符是做爲 init_task 靜態分配的。
Linux中建立進程與其餘系統有個主要區別,Linux中建立進程分2步:fork() 和 exec(),而其餘系統一般提供spawn() 函數建立進程並讀入可執行文件,而後開始執行。
exec: 讀取可執行文件,將其載入到內存中運行
傳統 fork() 系統調用:直接把全部的資源複製給新建立的進程。這種實現過於簡單而且效率低下,由於它拷貝的數據也許並不共享,更糟的狀況是,若是新進程打算當即執行一個新的映像,那麼全部的拷貝都將前功盡棄。
Linux 的 fork() 使用寫時拷貝 (copy-on-write) 頁實現:
fork() 的實際開銷就是複製父進程的頁表以及給子進程建立惟一的進程描述符。在通常狀況下,進程建立後都會立刻運行一個可執行的文件,這種優化能夠避免拷貝大量根本就不會被使用的數據(地址空間裏經常包含數十兆的數據)。因爲 Unix 強調進程快速執行的能力,因此這個優化是很重要的。
Linux 經過 clone() 系統調用實現 fork()。這個調用經過一系列的參數標誌來指明父、子進程須要共享的資源。 fork()、 vfork()和_clone() 庫函數都根據各自須要的參數標誌去調用 clone(),而後由 clone() 去調用 do_fork()。
do_fork() 完成了建立中的大部分工做,它的定義在 kernel/fork.c 文件中。該函數調用 copy_process() 函數,而後讓進程開始運行。 copy_process() 函數完成的工做以下:
copy_process() 函數執行完成後,返回到do_fork() 函數。若copy_process() 函數成功返回,新建立的子進程被喚醒井讓其投入運行。
內核有意選擇子進程首先執行 。由於通常子進程都會立刻調用 exec() 函數,這樣能夠避免寫時拷貝的額外開銷。
建立進程的fork()函數實際上最終是調用clone()函數。
建立線程和進程的步驟同樣,只是最終傳給clone()函數的參數不一樣。
好比,經過一個普通的fork來建立進程,至關於:clone(SIGCHLD, 0)
建立一個和父進程共享地址空間,文件系統資源,文件描述符和信號處理程序的進程,即一個線程:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
在內核中建立的內核線程與普通的進程之間還有個主要區別在於:內核線程沒有獨立的地址空間,它們只能在內核空間運行。
這與以前提到的Linux內核是個單內核有關。
和建立進程同樣,終結一個進程一樣有不少步驟:
子進程上的操做(do_exit)
子進程進入EXIT_ZOMBIE以後,雖然永遠不會被調度,關聯的資源也釋放掉了,可是它自己佔用的內存尚未釋放,
好比建立時分配的內核棧,task_struct結構等。這些由父進程來釋放。
父進程上的操做(release_task)
父進程受到子進程發送的exit_notify()信號後,將該子進程的進程描述符和全部進程獨享的資源所有刪除。
從上面的步驟能夠看出,必需要確保每一個子進程都有父進程,若是父進程在子進程結束以前就已經結束了會怎麼樣呢?
子進程在調用exit_notify()時已經考慮到了這點。
若是子進程的父進程已經退出了,那麼子進程在退出時,exit_notify()函數會先調用forget_original_parent(),而後再調用find_new_reaper()來尋找新的父進程。
find_new_reaper()函數先在當前線程組中找一個線程做爲父親,若是找不到,就讓init作父進程。(init進程是在linux啓動時就一直存在的)