Linux Kernel Development 學習

處理器的活動能夠分爲3類:linux

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

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

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

 

包含了全部狀況,邊邊角角也不例外。例如CPU空閒時,內核就運行一個空進程,處於進程上下文,但運行於內核空間windows

 

微內核架構(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的冪

相關文章
相關標籤/搜索