《Linux內核設計與實現》筆記

《Linux內核設計與實現》筆記

參考:《Linux內核設計與實現》讀書筆記

第一章 Linux內核簡介

1. 單內核和微內核

原理 優點 劣勢
單內核 總體上做爲一個單獨的大過程來實現,整個內核都在一個大內核地址空間上運行。 1. 簡單。 2. 高效:全部內核都在一個大的地址空間上,因此內核各個功能之間的調用和調用函數相似,幾乎沒有性能開銷。 一個功能的崩潰會致使整個內核沒法使用。
微內核 內核按功能被劃分紅多個獨立的過程。每一個過程獨立的運行在本身的地址空間上。 1. 安全:內核的各類服務獨立運行,一種服務掛了不會影響其餘服務。 內核各個服務之間經過進程間通訊互通消息,比較複雜且效率低。

由於 IPC 機制的開銷多於函數調用,又由於會涉及內核空間與用戶空間的上下文切換,所以,消息傳遞須要必定的週期,而單內核中簡單的函數調用沒有這些開銷。html

Linux的內核雖然是基於單內核的,運行在單獨的內核地址空間上。可是通過這麼多年的發展,也具有微內核的一些特徵。(體現了Linux實用至上的原則)linux

主要有如下特徵:git

  1. 模塊化設計,支持動態加載內核模塊
  2. 支持對稱多處理(SMP)
  3. 內核能夠搶佔(preemptive),容許內核運行的任務有優先執行的能力
  4. 支持內核線程,不區分線程和進程

2. 內核版本號

內核的版本號主要有四個數組組成。好比版本號:2.6.26.1 其中:數組

2 - 主版本號緩存

6 - 從版本號或副版本號安全

26 - 修訂版本號網絡

1 - 穩定版本號併發

副版本號表示這個版本是穩定版(偶數)仍是開發版(奇數),上面例子中的版本號是穩定版。dom

穩定的版本可用於企業級環境。異步

修訂版本號的升級包括BUG修正,新的驅動以及新的特性的追加。

穩定版本號主要是一些關鍵性BUG的修改。

第二章 從內核出發

1. 獲取內核源碼

內核是開源的,全部獲取源碼特別方便,參照如下的網址,能夠經過git或者直接下載壓縮好的源碼包。

http://www.kernel.org

2. 內核源碼的結構

目錄 說明
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 虛擬化基礎結構

3. 編譯內核的方法

還未實際嘗試過手動編譯內核,只是用yum更新過內核。這部分等之後手動編譯過再補上。

安裝新的內核後,重啓時會提示進入哪一個內核。當屢次安裝新的內核後,啓動列表會很長(由於有不少版本的內核),顯得不是很方便。

下面介紹3種刪除那些不用的內核的方法:(是如何安裝的就選擇相應的刪除方法)

  • rpm 刪除法

    rpm -qa | grep kernel* (查找全部linux內核版本)
    rpm -e kernel-(想要刪除的版本)

  • yum 刪除法

    yum remove kernel-(要刪除的版本)

  • 手動刪除

    刪除/lib/modules/目錄下不須要的內核庫文件
    刪除/usr/src/kernel/目錄下不須要的內核源碼
    刪除/boot目錄下啓動的核心檔案禾內核映像
    更改grub的配置,刪除不須要的內核啓動列表

4. 內核開發的特色

4.1 無標準C庫

爲了保證內核的小和高效,內核開發中不能使用C標準庫,因此連最經常使用的printf函數也沒有,可是還好有個printk函數來代替。

4.2 使用GNU C

由於使用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)) {
        /* ... */
    }

4.3 沒有內存保護

由於內核是最低層的程序,因此若是內核訪問的非法內存,那麼整個系統都會掛掉!因此內核開發的風險比用戶程序開發的風險要大。

內核中的內存是不分頁的,每用一個字節的內存,物理內存就少一個字節。因此內核中使用內存必定要謹慎。

4.4 不使用浮點數

內核不能完美的支持浮點操做,使用浮點數時,須要人工保存和恢復浮點寄存器及其餘一些繁瑣的操做。

4.5 內核棧容積小且固定

內核棧的大小有編譯內核時決定的,對於不用的體系結構,內核棧的大小雖然不同,但都是固定的。

查看內核棧大小的方法:

ulimit -a | grep "stack size"

4.6 同步和併發

Linux是多用戶的操做系統,因此必須處理好同步和併發操做,防止因競爭而出現死鎖。

內核很容易產生競爭條件。和單線程的用戶空間程序不一樣,內核的許多特性都要求可以併發地訪問共享數據,這就要求有同步機制以保證不出現競爭條件,特別是:

  • Linux 是搶佔多任務操做系統。內核的進程調度程序即興對進程進行調度和從新調度。內核必須和這些任務同步。
  • Linux 內核支持對稱多處理器系統 (SMP) 。因此,若是沒有適當的保護,同時在兩個或兩個以上的處理器上執行的內核代碼極可能會同時訪問共享的同一個資源。
  • 中斷是異步到來的,徹底不顧及當前正在執行的代碼。也就是說,若是不加以適當的保護,中斷徹底有可能在代碼訪問資源的時候到來,這樣,中段處理程序就有可能訪問同一資源。
  • Linux 內核能夠搶佔。因此,若是不加以適當的保護,內核中一段正在執行的代碼可能會被另一段代碼搶佔,從而有可能致使兒段代碼同時訪問相同的資源。

經常使用的解決競爭的辦法是自旋鎖和信號量

4.7 可移植性

Linux內核可用於不用的體現結構,支持多種硬件。因此開發時要時刻注意可移植性,儘可能使用體系結構無關的代碼。

第三章 進程管理

1. 進程

程序自己並非進程,進程是處於執行期的程序以及相關的資源的總稱。

  • 可能存在兩個或多個不一樣的進程執行的是同一個程序。
  • 兩個或兩個以上並存的進程還能夠共享許多諸如打開的文件、地址空間之類的資源。

進程和線程是程序運行時狀態,是動態變化的,進程和線程的管理操做(好比,建立,銷燬等)都由內核來實現的。

Linux中不嚴格區分進程和線程,對Linux而言線程不過是一種特殊的進程。

現代操做系統中,進程提供2種虛擬機制:虛擬處理器和虛擬內存

  • 虛擬處理器給進程一種假象,讓這些進程以爲本身在獨享處理器。
  • 虛擬內存讓進程在分配和管理內存時以爲本身擁有整個系統的全部內存資源。

每一個進程有獨立的虛擬處理器和虛擬內存

在進程中的各個線程之間能夠共享虛擬內存,但每一個都擁有各自的虛擬處理器。

進程的建立與退出:

  1. 進程在建立它的時刻開始存活,在 Linux 系統中,這一般是調用 fork()系統的結果,該系統調用經過複製一個現有進程來建立一個全新的進程。調用 fork()的進程稱爲父進程,新產生的進程稱爲子進程。在該調用結束時,在返回點這個相同位置上,父進程恢復執行,子進程開始執行。 fork()系統調用從內核返回兩次:一次回到父進程,另外一次回到新產生的子進程。
  2. 建立新的進程都是爲了當即執行新的、不一樣的程序,而接着調用 exec() 這組函數就能夠建立新的地址空間,並把新的程序載入其中。在現代 Linux 內核中, fork()其實是由clone()系統調用實現的。
  3. 最終,程序經過 exit()系統調用退出執行。這個函數會終結進程並將其佔用的資源釋放掉。進程退出執行後被設置爲僵死狀態,直到它的父進程調用 wait()或 waitpid()爲止。

內核中進程的信息主要保存在task_struct中(include/linux/sched.h)

進程標識PID和線程標識TID對於同一個進程或線程來講都是相等的。

Linux中能夠用ps命令查看全部進程的信息:

ps -eo pid,tid,ppid,comm

2. 進程描述符及任務結構

內核把進程的列表存放在叫作任務隊列 (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>

2.1 分配進程描述符

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 的指針。

2.2 進程描述符的存放

內核中大部分處理進程的代碼都是直接經過 task_struct 進行的。所以,經過 current 宏查找到當前正在運行進程的進程描述符的速度就顯得尤其重要。

硬件體系結構不一樣,該宏的實現也不一樣,它必須針對專門的硬件體系結構作處理。有的硬件體系結構能夠拿出一個專門寄存器來存放指向當前進程 task_struct 的指針,用千加快訪問速度。而有些像 x86 這樣的體系結構(其寄存器並不富餘),就只能在內核棧的尾端建立 thread_info 結構,經過計算偏移間接地查找 task_struct 結構。

2.3 進程狀態

進程描述符中的 state 域描述了進程的當前狀態 。系統中的每一個進程都必然處於五種進程狀態中的一種。

  • TASK_RUNNING (運行)一 進程是可執行的;它或者正在執行,或者在運行隊列中等待執行。這是進程在用戶空間中執行的惟一可能的狀態;這種狀態也能夠應用到內核空間中正在執行的進程。
  • TASK_INTERRUPTIBLE (可中斷)一 進程正在睡眠(也就是說它被阻塞),等待某些條件的達成。一且這些條件達成,內核就會把進程狀態設置爲運行。處千此狀態的進程也會由於接收到信號而提早被喚醒井隨時準備投入運行。
  • TASK_ UNINTERRUPTIBLE (不可中斷)一 除了就算是接收到信號也不會被喚醒或準備投入運行外,這個狀態與可打斷狀態相同。這個狀態一般在進程必須在等待時不受干擾或等待事件很快就會發生時出現。因爲處於此狀態的任務對信號不作響應,因此較之可中斷狀態 , 使用得較少。
  • TASK_TRACED (被跟蹤)— 被其餘進程跟蹤的進程,例如經過 ptrace 對調試程序進行跟蹤。
  • TASK_STOPPED (中止)— 進程中止執行;進程沒有投入運行也不能投人運行。一般這種狀態發生在接收到 SIGSTOP 、 SIGTSTP 、 SIGTTIN 、 SIGTTOU 等信號的時候。此外,在調試期間接收到任何信號,都會使進程進入這種狀態。

<img src="https://gitee.com//MrRen-sdhm/Images/raw/master/img/20200523211906.png" alt="image-20200523211853355" style="zoom:67%;" />

進程的各個狀態之間的轉化構成了進程的整個生命週期。

2.4 設置當前進程狀態

內核常常須要調整某個進程的狀態。這時最好使用 set_task_state(task, state) 函數:

set_task_state(task, state); /*將任務task的狀態設置爲state*/

該函數將指定的進程設置爲指定的狀態。必要的時候,它會設置內存屏障來強制其餘處理器做從新排序。不然,它等價於:

task->state = state;

2.5 進程上下文

通常程序在用戶空間執行。當一個程序執行了系統調用或者觸發了某個異常,它就陷入了內核空間。此時,咱們稱內核「表明進程執行」並處於進程上下文中。在此上下文中 current 宏是有效的。除非在此間隙有更高優先級的進程須要執行並由調度器作出了相應調整,不然在內核退出的時候,程序恢復在用戶空間會繼續執行。

系統調用和異常處理程序是對內核明肯定義的接口。進程只有經過這些接口才能陷入內核執行——對內核的全部訪間都必須經過這些接口。

2.6 進程家族樹

Linux 系統中進程之間存在一個明顯的繼承關係,全部的進程都是 PID 爲 1 的 init 進程的後代內核在系統啓動的最後階段啓動 init 進程

init 進程讀取系統的初始化腳本 (initscript) 並執行其餘的相關程序,最終完成系統啓動的整個過程。

系統中的每一個進程必有一個父進程,相應的,每一個進程也能夠擁有零個或多個子進程,每一個 task_struct 都
包含一個指向其父進程 tast_struct 的 parent 指針,還包含一個稱爲 children 的子進程鏈表

init 進程的進程描述符是做爲 init_task 靜態分配的。

3. 進程的建立

Linux中建立進程與其餘系統有個主要區別,Linux中建立進程分2步:fork() 和 exec(),而其餘系統一般提供spawn() 函數建立進程並讀入可執行文件,而後開始執行。

  • fork: 經過拷貝當前進程建立一個子進程
  • exec: 讀取可執行文件,將其載入到內存中運行

    • exec() 在這裏指全部 exec() 一族的函數。內核實現了 execve() 函數,在此基礎上,還實現了 execlp()、 execle()、execv() 和 execvp()。

3.1 寫時拷貝

傳統 fork() 系統調用:直接把全部的資源複製給新建立的進程。這種實現過於簡單而且效率低下,由於它拷貝的數據也許並不共享,更糟的狀況是,若是新進程打算當即執行一個新的映像,那麼全部的拷貝都將前功盡棄。

Linux 的 fork() 使用寫時拷貝 (copy-on-write) 頁實現

  • 寫時拷貝是一種能夠推遲甚至免除拷貝數據的技術。內核此時井不復制整個進程地址空間,而是讓父進程和子進程共享同一個拷貝。
  • 只有在須要寫入的時候,數據纔會被複制,從而使各個進程擁有各自的拷貝。資源的複製只有在須要寫入的時候才進行,在此以前,只是以只讀方式共享
  • 這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候才進行。在頁根本不會被寫入的狀況下(舉例來講,fork() 後當即調用 exec() 它們就無須複製了。

fork() 的實際開銷就是複製父進程的頁表以及給子進程建立惟一的進程描述符。在通常狀況下,進程建立後都會立刻運行一個可執行的文件,這種優化能夠避免拷貝大量根本就不會被使用的數據(地址空間裏經常包含數十兆的數據)。因爲 Unix 強調進程快速執行的能力,因此這個優化是很重要的。

3.2 fork()

Linux 經過 clone() 系統調用實現 fork()。這個調用經過一系列的參數標誌來指明父、子進程須要共享的資源。 fork()、 vfork()和_clone() 庫函數都根據各自須要的參數標誌去調用 clone(),而後由 clone() 去調用 do_fork()。

do_fork() 完成了建立中的大部分工做,它的定義在 kernel/fork.c 文件中。該函數調用 copy_process() 函數,而後讓進程開始運行。 copy_process() 函數完成的工做以下:

  1. 調用dup_task_struct() 爲新進程分配內核棧、thread_info 和 task_struct 等,其中的內容與父進程相同。此時,子進程和父進程的描述符是徹底相同的。
  2. check新進程(進程數目是否超出上限等)
  3. 清理新進程的信息(好比PID置0等),使之與父進程區別開。
  4. 新進程狀態置爲 TASK_UNINTERRUPTIBLE,以保證它不會投入運行。
  5. 調用 copy_ftags() 更新 task_struct 的 flags 成員。
  6. 調用 alloc_pid() 爲新進程分配一個有效的 PID
  7. 根據clone()的參數標誌,拷貝或共享相應的信息
  8. 作一些掃尾工做並返回指向新進程的指針

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內核是個單內核有關。

4. 進程的終止

和建立進程同樣,終結一個進程一樣有不少步驟:

子進程上的操做(do_exit)

  1. 設置task_struct中的標識成員設置爲PF_EXITING
  2. 調用del_timer_sync()刪除內核定時器, 確保沒有定時器在排隊和運行
  3. 調用exit_mm()釋放進程佔用的mm_struct
  4. 調用sem__exit(),使進程離開等待IPC信號的隊列
  5. 調用exit_files()和exit_fs(),釋放進程佔用的文件描述符和文件系統資源
  6. 把task_struct的exit_code設置爲進程的返回值
  7. 調用exit_notify()向父進程發送信號,並把本身的狀態設爲EXIT_ZOMBIE
  8. 切換到新進程繼續執行

子進程進入EXIT_ZOMBIE以後,雖然永遠不會被調度,關聯的資源也釋放掉了,可是它自己佔用的內存尚未釋放,
好比建立時分配的內核棧,task_struct結構等。這些由父進程來釋放。

父進程上的操做(release_task)

父進程受到子進程發送的exit_notify()信號後,將該子進程的進程描述符和全部進程獨享的資源所有刪除。

從上面的步驟能夠看出,必需要確保每一個子進程都有父進程,若是父進程在子進程結束以前就已經結束了會怎麼樣呢?

子進程在調用exit_notify()時已經考慮到了這點。

若是子進程的父進程已經退出了,那麼子進程在退出時,exit_notify()函數會先調用forget_original_parent(),而後再調用find_new_reaper()來尋找新的父進程。

find_new_reaper()函數先在當前線程組中找一個線程做爲父親,若是找不到,就讓init作父進程。(init進程是在linux啓動時就一直存在的)

相關文章
相關標籤/搜索