Linux Kernel Development 學習


處理器的活動可以分為3類:

運行於用戶空間,執行用戶進程

運行於內核空間,處於進程上下文,代表某個特定的進程進行

運行於內核空間,處於中斷上下文,與任何進程都無關,處理某個特定的中斷

 

包含了所有情況,邊邊角角也不例外。例如CPU空閑時,內核就運行一個空進程,處於進程上下文,但運行於內核空間

 

微內核架構(Micro kernel)和單內核架構(Monolithic kernel)的區別。

 

--微內核——

 

最常用的功能被設計在內核模式(x86上為 0權限下),其他不怎么重要的功能都作為單獨的進程運行在用戶模式下(3權限下),通過

消息傳遞進行通訊(windows采用進程間通信IPC機制,IPCInter Process Communication) 最基本的思想是盡量的小,通常微內核只

包括進程調度,內存管理和進程間通信這幾個基本功能。

 

好處:增加了靈活性,易於維護,易於移植。其他的核心功能模塊都只依賴於微內核模塊和其他模塊,並不直接依賴硬件。

由於模塊化設計,不包含在微內核內的驅動程序可以動態的加載或者卸載。

還具有的好處就是實時性、安全性較好,並且更適合於構建分布式操作系統和面向對象操作系統。

典型的操作系統中例子:Mach(非原生的分布式操作系統,被應用在Max OS X上)、IBM AIX、BeOS以及Windows NT

 

--單內核--

 

單內核是個很大的進程,內部又被分為若干的模塊(或層次,或其他),但在運行時,是一個單獨的大型二進制映像/因為在同一個進程

內,其模塊間的通訊是通過直接調用其他模塊中的函數實現的,而不是微內核中多個進程間的消息傳遞,運行效率上單內核有一定的好處。

 

典型的操作系統中的例子:大部分Linux、包括BSD在內的所有Linux(編譯過 Linux的人知道Linux內核有數十MB)

 

 

即:

IPC機制的開銷多於函數調用,又因為會涉及內核空間與用戶空間的上下文切換,因此消息傳遞需要一定的周期,而單內核中的函數調用則沒有這些開銷。

結果實際上基於微內核的系統都讓大部分或者全部服務器位於內核,這樣可以直接調用函數,消除頻繁的上下文切換。

 

Linux與傳統Unix不同的地方:

1. Linux支持動態的加載內核模塊。雖然是單內核的, 但是允許在需要的時候動態地卸載和加載部分內核代碼。

2. Linux支持對稱多處理(SMP)機制,傳統的Unix並不支持這種機制。

3. Linux內核可搶占。Linux內核具有允許在內核運行的任務優先執行的能力。

4.Linux對線程的支持:並不區分線程和其他的一般進程。

 

內核配置要么是2選1,要么是3選1:

2選1就是yes或no,3選1的話為yes, no和module

module代表被選定,但編譯時這部分的功能實現代碼是模塊,yes選項代表把代碼編譯進主內核模塊中,而不是作為一個模塊。

驅動程序一般選用3選1的配置。

 

Linux輸出重定向:

> 直接把內容生成到指定文件,會覆蓋源文件中的內容,還有一種用途是直接生成一個空白文件,相當於touch命令
>> 尾部追加,不會覆蓋掉文件中原有的內容

make程序能把編譯過程拆分成多個並行的作業。其中每個作業獨立並發的運行,這有助於極大地加快多處理器系統上的編譯過程,也有利於改善處理器的利用率。

 

內聯函數的定義: inline int add_int(int x, int y, int z){return x+y+z};

通常將對時間要求比較高,本身長度比較短的函數定義成內聯函數。若函數較大且會被反復調用,不贊成定義為內聯函數。

在程序中,調用其函數時,該函數在編譯時被替代(即將調用表達式用內聯函數體來替換),而不是在運行時被調用。

要注意:內聯函數內不允許用循環和開關語句。

有意思:#define MAX(a, b) (a) > (b) ? (a) : (b)

如果你在代碼中這樣寫:

int a = 10, b = 5;

// int max = MAX(++a, b); // a自增了兩次

// int max = MAX(++a, b+10); // a自增了一次

 

進程管理:

每個線程都有一個獨立的程序計數器,進程棧和一組進程寄存器。

內核調度的對象是線程。

 

--進程描述符和任務結構--

進城的列表放在任務隊列(task list)的雙向循環鏈表中,每一項的結構都是task_struct,稱為進程描述符的(process discriptor)結構,包含一個具體進程的所有信息。

task_struct相對較大,32位機器上大約有1.7KB,包括:所打開的文件,掛起的信號,進程的狀態等

 

Linux通過slab分配器分配task _struct結構,各個進程的task _struct存放在內核棧的尾端,只需在棧底(對於向下增長而言)創建一個新的結構  struct thread_info  (這個結構使得匯編中計算其偏移變得十分容易)

 

內核中大部分處理進程的代碼都是直接通過 task_struct 進行的。PowerPC中當前的 task_struct 專門保存在一個寄存器中,x86需要在尾端創建 thread_info 來計算偏移間接查找 task_struct 結構。

 

系統調用和異常處理程序是對內核明確定義的接口。進程只有通過這些接口才能陷入內核執行——對內核的訪問都需要經過這些接口。

 

內核經常需要在后台執行一些操作,這種任務可以通過內核線程(kernel thread)完成——獨立運行在內核空間的標准進程。和不同進程的區別在於:沒有獨立的地址空間(指向地址空間的mm指針被定為NULL),只在內核空間運行,從來不切換到用戶空間中去。內核進程和普通進程一樣,可以被調度和搶占。

 

進程的終結發生在進程調用exit()系統調用,可能顯式調用,也可能從某個程序的主函數返回。

在刪除進程描述符之前,進程存在的唯一目的就是向父進程提供信息。

即進程終結時所需的清理工作和進程描述符的刪除被分開執行。

最后的工作為從任務列表中刪除此進程,同時釋放進程內核棧和thread_info所占的頁,並且釋放task_struct所占的slab高速緩存。

 

在為沒有父進程的子進程尋找父進程時使用的兩個鏈表:子進程鏈表和ptrace子進程鏈表

當一個進程被跟蹤時,臨時父親會被設置為調試進程。如果此時他的父進程退出了,則系統會子進程和其兄弟找一個新的父進程。

以前處理這個過程需要遍歷系統來尋找子進程,現在在一個單獨的被ptrace跟蹤的子進程鏈表中搜索相關的兄弟進程——用兩個較小的鏈表減輕了遍歷帶來的消耗。

 

進程調度:

 

多任務系統分為兩類:非搶占式多任務(cooperative multitasking)和搶占式多任務(preemptive multitasking)

 

搶占式:進程被搶占之前能夠運行的時間是設定好的,叫做進程的時間片(timeslice)

非搶占式:進程主動掛起自己的操作稱為讓步(yielding)

 

Linux的2.5版本調度稱為O(1),但其對於調度那些響應時間敏感的程序卻有一些先天的不足,這些程序稱為交互進程。2.6版本為了提高對交互程序的調度性能引入了新的進程調度算法,最為著名的是“反轉樓梯最后期限調度算法”(Rotating Staircase Deadline scheduler)(RSDL),此算法吸收了隊列理論,將公平調度的概念引入了Linux調度程序。此刻完全替代了O(1)調度算法,被稱為“完全公平調度算法”,或者簡稱CFS。

 

 

--策略--

I/O消耗型和處理器消耗型的進程:

前者指的是進程的大部分時間都用來提交I/O請求或者是等待I/O請求。這樣的進程經常處於可運行狀態,但是都是運行短短的一會,因為等待請求時總會阻塞。

后者把時間用在執行代碼上,除非被搶占,通常都一直不斷的運行。從系統響應速度考慮,調度起不應該經常讓它們運行,策略通常是降低調度頻率,同時延長運行時間。(極限例子是無限循環的執行)

 

調度策略通常需要在兩者間找到平衡:進程響應迅速(響應時間短)和最大系統利用率(吞吐量)

 

總結:一個是等待I/O,一個是數學計算,Linux傾向於優先調度I/O消耗型

 

 

 

進程優先級:

Linux采用兩種優先級

  1. nice值。范圍從-20到+19,值越大優先級越低
  2. 實時優先級。從0到99,包括0和99,數值越高優先級越高

 

實時優先級和nice優先級處於互不相交的兩個范疇,任何實時進程的優先級都高於普通的進程。

 

一般情況下默認的時間片都很短,通常為10ms。但是Linux的CFS調度器並沒有直接分配時間片到進程,而是將處理器的使用比划分給了進程。這個比例還會受到nice值的影響,nice值作為權重將調整進程所使用的處理器時間使用比。同時Linux中的搶占時機也取決於新的可運行程序消耗了多少處理器使用比。若比當前進程小,則立即投入,搶占當前進程,否則推遲。

 

通過文本編輯和視頻處理例子了解處理器使用比!

 

--Linux調度算法--

 

Linux的調度器是以模塊方式提供的,允許不同類型的進程有針對性的選擇調度算法。

這種模塊化結構稱為調度器類(scheduler classes)。基礎的調度器會按照優先級順序遍歷調度類

 

完全公平調度(CFS)是一個針對普通進程的調度類,在Linux中稱為SCHED_NORMAL(POSIX中稱為SCHED_OTHER)

 

 

基於nice值的調度算法可能出現的問題:

1.進程切換無法最優化:兩個同等低優先級的進程均只能獲得很短的時間片,需要進行大量上下文切換

2.根據優先級不同,獲得的處理器時間差異很大(99和100差1,1和2也差一,但是2倍)

3.涉及到時間片的映射,需要分配一個絕對時間片,時間片也可能會隨着定時器節拍的改變而改變。

4.優化喚醒時打破公平原則,獲得更多處理器時間的同時,損害其他進程的利益

 

 

公平調度:

每個進程能獲得1/n的處理器時間--n指的是可運行進程的數量。

不再采取給每個進程時間片的做法,而是在所有可運行進程的總數基礎上計算一個進程應該運行多久。不依靠nice來計算時間片,而是計算作為進程獲得的處理器運行比的權重。nice低,權重高

 

CFS為無限小小調度周期的近似值設定了目標,稱為“目標延遲”

小的調度周期帶來了好的交互性,但是必須承受高的切換代價和更差的系統總吞吐能力

 

假設目標延遲為20ms,若有2個相同優先級的任務,每個運行10ms,4個則為5ms

當任務趨於無限的時候,處理器使用比趨於0,為此引入了時間片底線,稱為“最小粒度”,默認1ms

即可運行進程的數量趨於無限時,每個進程最少獲得1ms的運行時間

 

CFS中幾個重要概念:

1. nice值越小,進程的權重越大。同時nice值的相差使得權重間程倍數關系。

2. CFS調度器的一個調度周期值是固定的,由sysctl_sched_latency變量保存。

3. 進程在一個調度周期中的運行時間為:

分配給進程的運行時間 = 調度周期 * 權重 / 所有進程的權重之和

    即權重越大,分配到的時間越多

4. 一個進程的實際運行時間和虛擬時間(vruntime)的關系

vruntime = 實際運行時間 * NICE_0_LOAD(1024)/ 進程權重

    進程權重越大,運行相同的實際時間,vruntime增長的越慢

5. 一個進程在一個調度周期內的虛擬運行時間大小代入前兩式可得

vruntime = 調度周期 * 1024 / 所有進程進程權重(定值!!)

    即所有進程的vruntime都是一樣的。

http://blog.csdn.net/liuxiaowu19911121/article/details/47070111

 

6. 紅黑樹中均為可執行的進程,若標記為休眠狀態,則從樹中移出。

------------------------------

內核通過need_resched標識來表明是否需要重新執行一次調度。每個進程都包含有這個標志,因為訪問進程描述符內的數值比訪問一個全局變量快(因為current宏速度很快並且描述符通常都在高速緩存中)

 

 

用戶搶占發生在:

  1. 從系統調用返回用戶空間時
  2. 從中斷處理程序返回用戶空間時

 

內核搶占:

只要沒有鎖,內核就可以進行搶占。

通過設置thread_info中的preempt_count計數器,有鎖的時候加1,釋放-1,數值為0的時候內核可以進行搶占。

內核搶占發生在:

  1. 中斷處理程序在執行,且返回內核空間之前
  2. 內核代碼再一次具有可搶占性的時候
  3. 內核任務顯示調用schedule()
  4. 內核中的任務阻塞(導致調用schedule())

Linux中的實時調度策略:SCHED_FIFO和SCHED_RR,非實時的調度策略時SCHED_NORMAL

實時策略並不被CFS管理,而是用一個特殊的實時調度器管理

 

SCHED_FIFO不是用時間片,可運行態的SCHED_FIFO比任何SCHED_NORMAL都先得到調度

只要其在運行,較低級別的進程只有等待其變成不可運行狀態后才有機會執行。

 

SCHED_RR是帶有時間片的SCHED_FIFO,只有消耗事先分配的時間片后就不能繼續執行

 

實時優先級范圍從0-99,SCHED_NORMAL進程的nice值共享了這個取值空間,即其-19到20為100到139的實時優先級范圍

 

 

 

 

 

 

Linux 的5個段:

BSS段:(bss segment) 通常用來存放程序中未初始化的全局變量的一塊內存區域。BSS是Block Started by Symbol 的簡稱,BSS段屬於靜態內存分配。

數據段:(data segment):通常用來存放程序中已初始化的全局變量的一塊內存區域,屬於靜態分配

代碼段:(code segment):通常指的是存放程序執行代碼的一塊內存區域。區域的大小在程序執行前就已經確定,內存區域屬於只讀。(某些架構支持可寫,即允許修改程序)

堆:(heap):用於存放進程運行中被動態分配的內存段,大小不確定,可以動態的擴張或縮減。調用malloc等函數分配內存時,新分配的內存被動態添加到堆上;free程序釋放內存時,被釋放的內存從堆中被剔除。(堆的位置在BSS的后面,並從其后開始增長)

棧:(stack):用戶存放程序臨時創建的變量,即函數{}中存放的變量(但不包括static聲明的變量,其存放在數據段中);同時函數被調用時,參數也會被壓入被調用的進程棧中。是由操作系統分配的,內存的申請和回收都由OS管理。

 

PS:

 

bss段(未手動初始化的段)並不給該段分配空間,只是記錄數據所需空間的大小

data段(已手動初始化的數據)為數據分配空間,數據段包括經過初始化的全局變量以及它們的值。BSS的大小可以從可執行文件中得到,然后鏈接器得到這個大小的內存塊,緊跟在數據段后面。包括數據塊和BSS段的整個區段成為數據區。

 

 

 

定時器和時間管理:

 

默認的系統定時器頻率根據體系結構的不同會有所不同,但基本上都是100HZ

 

更高的時鍾中斷頻度和更高的准確度使得依賴定時值執行的系統調用,譬如poll(), select()能夠以更高的精度運行;對諸如資源消耗和系統運行時間的測量會有更精細的解析度;提高進程搶占的准確度。

劣勢在於:時鍾中斷的頻率越高,系統的負擔越重,中斷處理程序占用的處理器的時間越多。

 

jiffies:全局變量jiffies用來記錄自系統啟動以來產生的節拍的總數。啟動時,內核將該變量初始化為0,此后每次時鍾中斷處理程序就會增加改變該變量的值。

把時鍾作為秒經常會用在內核和用戶空間進程交互的時候:

unsigned long next_stick = jiffies + 5 * HZ     /*從現在開始5s*/

注意:jiffies的類型為無符號長整形(unsigned long)(32位),用其他任何類型存放它都不正確

 

jiffies的回繞(wrap around),超過最大范圍后就會發生溢出。

內核提供了4個宏來幫忙比較節拍計數。

#define time_after(unknown, known)   ——   ( (long)(known) - (long)(unknown) < 0 )

#define time_before(unknown, known)   ——   ( (long)(unknown) - (long)(known) < 0 )

其中

time_after(unknown, known)當unknown超過指定的known時,返回真,否則返回假;

time_before相反 

 

實時時鍾(RTC)用來持久存放系統時間的設備。在PC體系結構中,RTC和CMOS集成在一起,而且RTC的運行和BIOS的保存設置都是通過同一個電池供電的。RTC最主要的作用是啟動時初始化xtime變量。

 

牆上時間代表了當前的實際時間。

 

內核對於進程進行時間計數時,時根據中斷發生時處理器所處的模式進行分類統計的。

x86機器上時鍾中斷處理程序每秒執行100次或者1000次

 

定時器結構由timer_list表示,內核提供了一組和定時器相關的接口來簡化定時器操作,並不需要深入了解其數據結構:

創建時定義:struct timer_list my_timer;

 

初始化定時器數據結構內部的值

init_timer(&my_timer);

 

填充數據結構中的值:

my_timer.expires = jiffies + delay;  /*定時器超時時的節拍數*/

my_timer.data = 0; /*給定時器處理函數傳入0值*/

my_timer.function = my_function; /*定時器超時時調用的函數*/

 

data參數可以利用同一個處理函數注冊多個定時器

 

最后激活定時器:

add_timer(&my_timer);

 

 

 

--延時執行的方法--

1.忙等待:

  unsigned long timeout = jiffies + 10;

  while (time_before(jiffies, timeout))

          ;

處理器只能原地等待,不會去處理其他任務,所以基本不采用此方法。

 

2.更好的方法應該是在等待時允許內核重新調度執行其他任務:

  unsigned long delay = jiffies + 5*HZ;

  while(time_before(jiffies,delay))

  cond_resched();

 

cond_resched()函數講調度一個新程序投入運行,但只有設置完成need_resched標志才能生效

另外由於其需要調用調度程序,所以不能在中斷上下文中使用--只能在進程上下文中使用。

 

我們要求jiffies在每次循環時都必須重新裝載,因為在后台jieffies會隨着時鍾中斷的發生而不斷增加。為了解決這個問題,<linux/jiffies.h>中的jiffies變量被標記為關鍵字volatile, 指示編譯器在每次訪問變量時都能重新從主內存中獲得,而不是通過寄存器中的變量別名來訪問,從而確保循環中的jieffies每次被讀取都會重新載入。

 

短延遲

jiffies的節拍間隔可能超過10ms,不能用於短延遲。

內核提供了3個延遲函數,void udelay(), void ndelay() 以及void mdelay()。

 

系統調用:

 

內核提供了用戶進程與內核進行交互的一組接口,應用程序提供各種請求,而內核負責滿足這些請求。

 

作用:

  1. 為用戶空間提供了一種硬件的抽象接口。例如讀寫文件時無需考慮磁盤類型和介質等
  2. 保證了系統的安全和穩定。內核可以基於權限,用戶類型等對訪問進行裁決

 

Linux 中,系統調用是用戶空間訪問內核的唯一手段;除異常和陷入外,它們是內核唯一的合法入口。

 

應用程序通過用戶空間實現的應用編程接口(API)而不是直接通過系統調用來編程

一個API定義了一組應用程序使用的編程接口,可通過一個或多個甚至不使用系統調用來實現

 

Unix接口設計格言:提供機制而非策略

機制:mechanism(需要提供什么樣的功能) 策略:policy(怎么實現這樣的功能)

 

 

--系統調用--

 

線程組leader的PID(也就是線程組中頭一個輕量級線程的PID)被線程共享,保存在thread_info->tgid中。 getpid()返回tgid的值,而不是PID值。對thread group leader來說,tgid = pid

 

Linux中每個系統調用被賦予一個系統調用號。系統調用號一旦分配就不能再有任何變更;如果被刪除,占用的系統號也不允許被回收利用。

系統中所有注冊過的系統調用,儲存在sys_call_table中。

 

系統調用的性能:Linux 的系統調用比其他許多操作系統執行的要快。Linux很短的上下文切換時間是一個重要原因;同時系統調用處理程序和每個系統調用本身也都非常簡潔。

 

用關空間的程序無法直接執行內核代碼,需要通知內核自己需要執行一個系統調用。

實現方法是軟中斷:通過引發一個異常來促使系統切換到內核態去執行異常處理程序,此時的異常處理就是系統調用處理程序。

x86系統上預定義的軟中斷是中斷號128,處理程序的名字叫system_call()

同時系統調用號是通過eax寄存器傳遞給內核的。

 

寫系統調用時,要時刻注意可移植性和健壯性。

 

參數驗證:系統調用在內核空間執行,如果有不合法的輸入傳遞給內核,系統的安全和穩定將面臨極大的考驗。

與I/O相關的系統調用必須檢查文件描述符是否有效,與進程相關的函數必須檢查提供的PID是否有效。進程不應當讓內核去訪問那些它無權訪問的資源。

最重要的檢查是檢查用戶提供的指針是否有效,內核必須保證:

 

  • 指針指向的內存區域屬於用戶空間,不允許讀取內核空間的數據
  • 指針指向的內存區域在進程的地址空間里。絕不能哄騙內核去讀其他進程的數據
  • 如果為讀/寫/執行,該內存應該被標記為可讀/可寫/可執行。不能繞過內存訪問限制

 

內核提供了兩個方法來完成必須的檢查,以及內核空間和用戶空間之間數據的來回拷貝。

1.寫數據提供了copy_to_user(),讀數據提供了copy_from_user()

均需要三個參數,進程空間中的內存地址,內核空間的原地址,需要拷貝的數據長度(字節數)

 

2.最后一項檢查是否具有合法權限。

老版本Linux內核需要超級用戶權限的系統調用來調用suser()函數來完成檢查。(這個函數只檢查是否為超級用戶)

 

新的系統允許檢查針對特定資源的特殊權限:

調用者可以使用capable()函數來檢查是否有權能對指定的資源進行操作,返回非0則有權操作,返回0則無權操作。

例如capable(CAP_SYS_NICE)可以檢查調用者是否有權改變其他進程的nice值。

參見<linux/capability.h>,其中包含一份所有這些權能和其對應的權限的列表。

 

建立一個新的系統調用十分容易,但是不提倡這么做。

替代方法:實現一個設備節點,對此實現read()和write()。使用ioctl()對特定的設置進行操作或者對特定的信息進行檢索。

 

 

內核數據結構:

 

1.鏈表數據結構

Linux內核方式與眾不同,它不是將數據結構塞入鏈表,而是將鏈表節點塞入數據結構!

即將現有的數據結構改造成鏈表,通過塞入鏈表節點!

 

內核提供的鏈表操作例程,比如list_add()方法加入一個新節點到鏈表中。這些方法都有一個特點:只接受list_head結構作為參數

 

使用宏container_of()我們可以很方便地從鏈表指針找到父結構中包含的任何變量。因為C語言中,一個給定結構的變量偏移在編譯時地址就被ABI固定下來了。

使用container,定義list_entry(),就可以返回包含list_head的父類型的結構體

同時內核提供了創建,操作以及其他鏈表管理的各種例程。

 

內核鏈表的特性在於,所有的struct節點都是無差別的--每一個包含一個list_head(),即可以從任何一個節點遍歷鏈表。不過有時需要一個特殊指針索引。

內核提供的函數都是用C語言以內聯函數的方式實現的,原型存在於文件<linux/list.h>

 

指向一個鏈表結構的指針通常是無用的;我們需要的是一個指向包含list_head的結構體的指針。

 

 

2.隊列

Linux內核通用隊列實現稱為kfifo,提供兩個主要的操作:enqueue(入隊列)和dequeue(出隊列)

kfifo對象維護了兩個偏移量:入口偏移和出口偏移

入口偏移指的是入隊列的位置,出口偏移指的是出隊列的位置。

出口偏移總是小於入口偏移。

 

enqueue操作拷貝數據到入口偏移位置,動作完成后,入口偏移加上推入的元素數目;

dequeue操作從隊列出口偏移拷貝數據,動作完成后,出口偏移減去摘取的元素數目。

 

--創建隊列--

動態創建:

int kfifo_alloc(struct kfifo *fifo, unsigned int size, gfp_t, gfp_mask);

該函數會初始化一個大小為size的kfifo,內核使用gfp_mask標識分配隊列,成功返回0,失敗返回錯誤碼

struct kfifo fifo;

int ret;

 

ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);

if ( ret )

return ret;

若自己想分配緩沖,可以調用:

void kfifo_init(struct knife *fifo, void *buffer, unsigned int size);

該函數創建並初始化一個kfifo對象,它將使用由buffer指向的size字節大小的內存,對於kfifo_alloc()和kfifo_init(),size必須是2的冪

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM