內核基礎
一、linux內核主要是由進程調度、內存管理、虛擬文件系統(字符設備驅動和塊設備驅動)、網絡接口(網絡設備驅動)和進程通訊5個子系統組成的。
1)進程調度控制系統中的多個進程對CPU的訪問,使得多個進程能在CPU中"微觀串行,宏觀並行"地執行。
2)內存管理的主要做用就是控制多個進程安全的共享主內存區域,當CPU提供內存管理單元時,linux內存管理完成爲每一個進程進行虛擬內存到物理內存的轉換。通常而言,linux的每個進程享有4GB的內存空間。0~3GB爲用戶空間,3~4GB爲內核空間,這1GB的內核空間又被劃分爲物理內存映射區,虛擬內存分配區、高端頁面映射區和系統保留映射區。物理內存映射區最大長度爲896MB,系統物理內存0~896MB就映射到這個物理內存映射區。系統物理內存中大於896MB的數據屬於高端內存,會被映射到高端頁面映射區。因此,在3~4GB的內核空間中,從低地址到高地址依次爲:物理內存映射區----->隔離帶------>虛擬內存分配區------>隔離帶------>高端內存映射區------>專用頁面映射區------>保留區。
3)虛擬文件系統隱藏各類硬件的具體細節,爲全部的設備提供一套統一的接口。
4)網絡接口提供了對各類網絡標準的存取和各類網絡硬件的支持。
5)進程通訊支持提供進程之間的通訊,包括信號量、共享內存、管道等。
二、進程管理:徹底公平調度算法(CFS)
(1)多任務系統分爲搶佔式和非搶佔式,linux提供搶佔式多任務模式,進程在被搶佔以前可以運行的時間叫作進程的時間片。
(2)進程能夠分爲I/O消耗型和處理器消耗型。I/O消耗型是指進程的大部分時間用來提交I/O請求或是等待I/O請求;處理器消耗型是指進程把事件大多數用在代碼執行上。 linux中更傾向於I/O消耗型進程。
(3)調度程序老是選擇時間片未用盡並且優先級最高的進程運行。
(4)上下文切換就是說從一個可執行進程切換到另外一個可執行進程。
(5)進程的上下文: 當一個進程在執行時,CPU的全部寄存器中的值、進程的狀態以及堆棧中的內容被稱爲該進程的上下文。當內核須要切換到另外一個進程時,它須要保存當前進程的全部狀態,即保存當前進程的上下文,以便在再次執行該進程時,可以獲得切換時的狀態執行下去。在LINUX中,當前進程上下文均保存在進程的任務數據結構中。在發生中斷時,內核就在被中斷進程的上下文中,在內核態下執行中斷服務例程。但同時會保留全部須要用到的資源,以便中繼服務結束時能恢復被中斷進程的執行。
(6)系統調用:系統調用是用戶空間訪問內核的惟一手段,除異常和陷入外。它們是內核惟一的合法入口。通常應用程序中的API調用c庫,c庫再調用內核中的系統調用,在UNIX中,最流行的應用編程接口是基於POSIX標準的。
(7)因爲內核駐留在受保護的地址空間中,因此用戶空間程序沒法直接訪問內核空間。其中通知內核的機制是靠軟中斷來實現的,這個軟中斷都是經過執行"init $0x80"指令觸發的,經過引起一個異常去促使系統切換到內核態去執行異常處理程序,這個異常處理程序就是系統調用處理程序:system_call.
(8)內核提供了兩種方式來完成內核空間和用戶空間的數據拷貝:copy_to_user和copy_from_user,這兩個函數都會引發阻塞。
三、中斷處理:中斷處理程序是被內核調用來響應中斷的,它運行在中斷上下文。
補充: 對Linux內核中進程上下文和中斷上下文的理解
內核空間和用戶空間是操做系統理論的基礎之一,即內核功能模塊運行在內核空間,而應用程序運行 在用戶空間。現代的CPU都具備不一樣的操做模式,表明不一樣的級別,不一樣的級別具備不一樣的功能,在較低 的級別中將禁止某些操做。Linux系統設計時利用了這種硬件特性,使用了兩個級別,最高級別和最低級 別,內核運行在最高級別(內核態),這個級別能夠進行全部操做,而應用程序運行在較低級別(用戶態), 在這個級別,處理器控制着對硬件的直接訪問以及對內存的非受權訪問。內核態和用戶態有本身的內存映 射,即本身的地址空間。 正是有了不一樣運行狀態的劃分,纔有了上下文的概念。用戶空間的應用程序,若是想要請求系統服務, 好比操做一個物理設備,或者映射一段設備空間的地址到用戶空間,就必須經過系統調用來(操做系統提 供給用戶空間的接口函數)實現。以下圖所示:
經過系統調用,用戶空間的應用程序就會進入內核空間,由內核表明該進程運行於內核空間,這就涉 及到上下文的切換,用戶空間和內核空間具備不一樣的地址映射,通用或專用的寄存器組,而用戶空間的進 程要傳遞不少變量、參數給內核,內核也要保存用戶進程的一些寄存器、變量等,以便系統調用結束後回 到用戶空間繼續執行,所謂的進程上下文,就是一個進程在執行的時候,CPU的全部寄存器中的值、進程 的狀態以及堆棧中的內容,當內核須要切換到另外一個進程時,它須要保存當前進程的全部狀態,即保存當 前進程的進程上下文,以便再次執行該進程時,可以恢復切換時的狀態,繼續執行。
同理,硬件經過觸發信號,致使內核調用中斷處理程序,進入內核空間。這個過程當中,硬件的一些變 量和參數也要傳遞給內核,內核經過這些參數進行中斷處理,中斷上下文就能夠理解爲硬件傳遞過來的這 些參數和內核須要保存的一些環境,主要是被中斷的進程的環境。 Linux內核工做在進程上下文或者中斷上下文。
提供系統調用服務的內核代碼表明發起系統調用的應 用程序運行在進程上下文;另外一方面,中斷處理程序,異步運行在中斷上下文。中斷上下文和特定進程無 關。
運行在進程上下文的內核代碼是能夠被搶佔的(Linux2.6支持搶佔)。可是一箇中斷上下文,一般都 會始終佔有CPU(固然中斷能夠嵌套,但咱們通常不這樣作),不能夠被打斷。正由於如此,運行在中斷 上下文的代碼就要受一些限制,不能作下面的事情:
1)、睡眠或者放棄CPU。
這樣作的後果是災難性的,由於內核在進入中斷以前會關閉進程調度,一旦睡眠或者放棄CPU,這時
內核沒法調度別的進程來執行,系統就會死掉
2)、嘗試得到信號量
若是得到不到信號量,代碼就會睡眠,會產生和上面相同的狀況
3)、執行耗時的任務
中斷處理應該儘量快,由於內核要響應大量服務和請求,中斷上下文佔用CPU時間太長會嚴重影響
系統功能。
4)、訪問用戶空間的虛擬地址
由於中斷上下文是和特定進程無關的,它是內核表明硬件運行在內核空間,因此在終端上下文沒法訪
問用戶空間的虛擬地址
小結:進程上下文就是應用程序那邊向內核發來的中斷信號;而中斷上下文就是硬件向內核發來的終端信號,前者能夠被搶佔,後者通常不可以被搶佔。
四、併發同步:若是有一臺支持多處理器(SMP)的機器,那麼兩個進程就能夠真正在臨界區域中同時執行,這就叫作真併發;反之稱爲僞併發。
併發產生的緣由有:
(1)中斷
(2)軟中斷和tasklet;
(3)內核搶佔;
(4)睡眠及用戶空間的同步;
(5)對稱多處理
避免死鎖的方式有:
死鎖和數據爭奪只能儘可能避免
通常來講,若是系統資源充足,進程的資源請求都可以獲得知足,死鎖出現的可能性就很低,不然就會因爭奪有限的資源而陷入死鎖。
另外死鎖有4個必要條件(要發生缺一不可)
(1) 互斥條件:一個資源每次只能被一個進程使用。
(2) 請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。
(3) 不剝奪條件:進程已得到的資源,在末使用完以前,不能強行剝奪。
(4) 循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。
經過使用較好的資源分配算法,就能夠儘量地破壞死鎖的必要條件,從而儘量地避免死鎖
解決併發同步的方法:
(1)原子操做
(2)自旋鎖
(3)自旋鎖和下半部的問題
(4)信號量
(5)互斥體
(6)完成量
(7)禁止搶佔
五、內存管理:塊----->扇區------>頁------>字節
1)內核把物理頁做爲內存管理的基本單位,MMU(內存管理單元,管理內存並把虛擬地址轉換爲物理地址的硬件)一般以頁爲單位進行處理。大多數32位體系結構的處理器支持4KB的頁。
2)內存除了管理自己的內存(物理內存)外,還必須管理用戶空間中的進程的內存(虛擬內存),這個內存就叫作進程地址空間。儘管一個進程能夠尋址4GB的虛擬內存,可是這並不表明它就有權訪問全部的虛擬內存,這些能夠被訪問的地址空間稱爲內存區域。
3)當應用程序訪問一個虛擬地址時,首先必須將虛擬地址轉換爲物理地址,而後處理器才能解析地址訪問請求。地址轉換須要將虛擬地址分段,使每段虛擬地址都做爲一個索引指向頁表,linux使用三級頁表完成地址轉換。
六、處理器:中央處理器體系架構能夠分爲馮諾伊曼結構和哈佛結構。
從指令的角度中央處理器分爲RISC(精簡指令集系統)和CISC(複雜指令集系統),中央處理器按照應用領域能夠分爲通用處理器(GPP),數字信號處理器(DSP)、專用處理器(ASIC).其中GPP包括MCU(微控制器,即單片機)和MPU(微處理器);DSP包括定點DSP和浮點DSP。
驅動概述
一、驅動看做是硬件的靈魂,這一比喻再恰當不過。設備驅動程序能夠說至關於硬件的接口,操做系統只有經過這個接口才可以控制硬件的工做。
簡單的說,驅動程序就至關於硬件和操做系統之間的橋樑,翻譯官,接口;驅動協調操做系統和硬件之間的關係。
二、設備驅動的分類:字符設備驅動、塊設備驅動和網絡設備驅動。
注意:字符設備是指那些能一個字節一個字節讀取數據的設備,內核爲字符設備對應一個文件,字符設備文件與普通文件沒有太大的區別,差異之處是字符設備通常不支持尋址,尋址的意思就是說對一個硬件中的一塊寄存器進行隨機的訪問,不支持尋址就是說只能對硬件中的寄存器進行順序的讀取,讀取數據後,由驅動程序本身來分析須要哪一部分數據。
網絡設備實現了一種套接字接口,任何網絡數據傳輸均可以經過套接字來完成。
三、linux操做系統與驅動的關係:操做系統包括:內核+應用程序(linux系統提供的API,包括shell等);而驅動又包含在內核中。
四、linux驅動程序開發:用戶態和內核態
驅動程序與底層的硬件交互,工做在內核態;內核態大部分時間在完成與硬件的交互。注意的是:用戶態和內核態是能夠相互轉換的,當應用程序執行系統調用或者是被硬件中斷掛起時,linux操做系統都會從用戶態切換到內核態。當系統調用完成或者是中斷處理完成後操做系統會從內核態返回給用戶態,繼續執行應用程序。
五、內核的模塊性(裁剪性)機制:靜態裝載和動態裝載。
六、不能使用C庫開發驅動程序,內核程序中所包含的頭文件是指內核代碼樹中的內核頭文件,不是指開發應用時的外部頭文件。在內核中實現的庫函數中的打印函數printk(),它是c庫函數printf()的內核版本,二者具備基本相同的用法和功能。
七、由於內核要求使用常駐的內存空間,所以要求儘可能少的佔用常駐內存,而儘可能多的留出內存提供給用戶程序使用,所以內核棧的長度是固定大小的,不可動態增加的,32位機的內核棧是8KB,64位機的內核棧是16KB。
八、linux內核子系統:進程管理、內存管理、文件管理、設備管理和網絡管理。
九、linux的kernel下源代碼結構目錄分析:
(1)arch目錄:該目錄包含與體系結構有關的代碼
(2)drivers目錄:該目錄包含了linux內核支持的大部分驅動程序,每種驅動程序都佔用一個子目錄。
(3)fs目錄:該目錄包含了Linux所支持的全部文件系統相關的代碼,每個子目錄中包含一種文件系統。
(4)makefile文件:用來組織內核的各個模塊,記錄了各個模塊之間的相互聯繫,編譯器根據這個文件來編譯內核。
其餘目錄:
十、內核配置:配置文件的組織關係和編譯過程
(0)make x210ii_qt_defconfig
(1)主目錄中包含不少子目錄,同時包含Kbulid和Makefile文件,各個子目錄中也包括其餘子目錄和Kbulid和Makefile文件。
當執行make menuconfig命令時,配置程序會依次從目錄由淺入深查找每個Kbuikld文件,依照這個文件中的數據來生成一個配置菜單,在配置菜單中根據須要配置完成後會在主目錄下生成一個.config文件,此文件中保存了配置信息。
(2)進行make命令,會依賴生成的.config文件,來肯定哪些功能將會被編譯到內核中去,哪些功能不會被編譯進去,而後遞歸進入每個目錄,尋找Makefile文件,編譯相應的代碼。
十一、嵌入式文件系統:
(1)linux支持多種文件系統,爲了對各種文件系統進行統一的管理,linux引入了虛擬文件系統VFS,爲各種文件系統提供一個統一的操做界面和應用編程接口。
(2)linux文件系統結構由4層構成,分別是用戶層、內核層、驅動層和硬件層。
(3)根文件系統以樹形結構來組織目錄和文件的結構,系統啓動後,根文件系統被掛載到根目錄"/"上,根文件系統應該包含的目錄遵循FHS(文件系統層次)標準。
(4)根文件系統目錄結構:
(5)Busybox構建根文件系統。
十二、構建第一個驅動程序
(1)內核:內核是一個提供硬件抽象層,磁盤及文件系統控制、多任務等功能的系統軟件。
(2)廠商發行版內核和標準版內核的驅動程序是不相互兼容的。
(3)使用uname -r命令來查看內核的版本。
(4)第一個hello world程序:
一:驅動模塊程序的組成:
(1)頭文件(必選)
#include <linux/module.h>
#include <linux/init.h>
(2)模塊參數(可選)
在驅動模塊加載時,須要傳遞給驅動模塊的參數,好比一個驅動模塊須要完成兩種功能,那麼就能夠經過模塊參數選擇使用哪種功能。(步進電機的驅動)
(3)模塊功能函數(可選)
(4)其餘(可選)
(5)模塊加載函數(必選)
模塊加載時須要執行的函數,這是模塊的初始化函數,就如同main()函數同樣。
(6)模塊卸載函數(必選)
模塊卸載時須要執行的函數,這裏清除了加載函數裏分配的資源。
(7)模塊許可聲明(必選)
表示模塊受內核支持的程度。
2、Hello World模塊.
1三、字符設備驅動框架
(1)字符設備是指那些只能一個字節一個字節讀寫數據的設備,不能隨機讀取設備內存中的某一數據,其讀取數據須要按照前後順序。字符設備是面向數據流的設備。
(2)塊設備:
應用程序能夠隨機訪問設備數據,程序可自行肯定讀取數據的位置。硬盤是典型的塊設備,應用程序能夠尋址磁盤上的任何位置,並由此讀取數據。此外,數據的讀寫只能以塊(一般是512B)的倍數進行。與字符設備不一樣,塊設備並不支持基於字符的尋址。
兩種設備自己並沒用嚴格的區分,主要是字符設備和塊設備驅動程序提供的訪問接口(file I/O API)是不同的。
(3)每個字符設備或者塊設備都在/dev 目錄下對應一個設備文件,使用命令: cd /dev ,而後ls -l能夠查看具體信息,其中第五、六、個字段表明主、此設備號。eg: crw-rw-rw- 1 root dialout 4, 65 Aug16 00:00 ttyS1
(4)一個字符設備或者塊設備都有一個主設備號和次設備號,主設備號和次設備號統稱爲設備號,主設備號用來表示一個特定的驅動程序(表示是哪一種驅動程序),次設備號用來表示使用該驅動程序的各個具體子設備。
(5)
(6)靜態分配設備號和動態分配設備號(推薦):
靜態分配設備號就是驅動程序開發者靜態地指定一個設備號,可是這種方式容易產生衝突。咱們在指定前通常先查看,使用: cat /proc/devices命令來查看,該文件下包含字符設備和塊設備的設備號。
動態分配設備號(推薦):使用alloc_chrdev_region()函數;
(7)
1四、設備驅動開發中的併發控制:內核須要提供併發控制機制,對公用資源進行保護。
(0)一、什麼是臨界區? 答:每一個進程中訪問臨界資源的那段程序稱爲臨界區(臨界資源是一次僅容許一個進程使用的共享資源)。每次只准許一個進程進入臨界區,進入後不容許其餘進程進入。 二、進程進入臨界區的調度原則是: ①若是有若干進程要求進入空閒的臨界區,一次僅容許一個進程進入。②任什麼時候候,處於臨界區內的進程不可多於一個。如已有進程進入本身的臨界區,則其它全部試圖進入臨界區的進程必須等待。③進入臨界區的進程要在有限時間內退出,以便其它進程能及時進入本身的臨界區。④若是進程不能進入本身的臨界區,則應讓出CPU,避免進程出現「忙等」現象。
(1)現代操做系統的三大特性:中斷處理、多任務處理和多處理器(SMP)。
(2)內核的併發控制機制有:(1)原子變量操做(2)自旋鎖(3)信號量(4)完成量
一:原子變量操做(通常用匯編語言實現),只能作計數操做。
原子操做須要硬件的支持跟架構相關。
在linux中定義了兩種原子變量操做方法,一種是原子整型操做,另外一種是原子位操做。
原子整型操做:
typedef struct
{
volatile int counter;
}atomic_t;
atomic_t 類型的變量只能經過linux內核中定義的專用函數來操做。
操做函數:
1)定義atomic_t類型的變量並初始化值的宏:atomic_t count = ATOMIC_INIT(0);
2)設置atomic_t類型的變量的值:atomic_read(v,i)宏,其中v爲要設置值的變量,i爲要設置的值。
3)讀取atomic_t類型的變量的值:atomic_read(v)宏 .
4)原子變量的加減法函數:atomic_add(int include,volatile atomic_t *v)函數用來加i的值;atomic_sub(int include,volatile atomic_t *v )函數用來減去i的值。
原子位操做:根據數據的每一位進行單獨的操做。
操做函數的參數是一個指針和一個位號。
原子位操做定義在文件中。使人感到奇怪的是位操做函數是對普通的內存地址進行操做的。原子位操做在多數狀況下是對一個字長的內存地址訪問,於是位號該位於0-31之間(在64位機器上是0-63之間),可是對位號的範圍沒有限制。
原子操做中的位操做部分函數以下:
void set_bit(int nr, void *addr) 原子設置addr所指的第nr位
void clear_bit(int nr, void *addr) 原子的清空所指對象的第nr位
void change_bit(nr, void *addr) 原子的翻轉addr所指的第nr位
int test_bit(nr, void *addr) 原子的返回addr位所指對象nr位
int test_and_set_bit(nr, void *addr) 原子設置addr所指對象的第nr位,並返回原先的值
int test_and_clear_bit(nr, void *addr) 原子清空addr所指對象的第nr位,並返回原先的值
int test_and_change_bit(nr, void *addr) 原子翻轉addr所指對象的第nr位,並返回原先的值
代碼示例:
unsigned long word = 0;
set_bit(0, &word); /*第0位被設置*/
set_bit(1, &word); /*第1位被設置*/
clear_bit(1, &word); /*第1位被清空*/
change_bit(0, &word); /*翻轉第0位*/
二:自旋鎖(類型位:struct spinlock_t )自旋鎖只容許短期鎖定
linux中能夠認爲有兩種鎖(對臨界資源進行併發控制):自旋鎖和信號量。
對自旋鎖的操做和使用:
一、定義和初始化自旋鎖
spinlock_t lock;//一個自旋鎖必須初始化才能使用,使用spin_lock_init(spinlock_t lock)函數進行動態的初始化;
二、鎖定自旋鎖
spin_lock(lock)宏來鎖定。
三、釋放自旋鎖
當再也不使用臨界區(經過臨界區來訪問臨界資源)時,使用spin_unlock(lock)宏來釋放自旋鎖。
綜合代碼示例:
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
臨界資源
spin_unlock(&lock);
三:信號量
linux中提供了兩種信號量,一種是基於內核程序中的,一種是基於應用程序中的。
linux中,信號量的類型爲struct semaphore,
一.什麼是信號量
信號量的使用主要是用來保護共享資源,使得資源在一個時刻只有一個進程(線程)
所擁有。
信號量的值爲正的時候,說明它空閒。所測試的線程能夠鎖定而使用它。若爲0,說明
它被佔用,測試的線程要進入睡眠隊列中,等待被喚醒。
二.信號量的分類
在學習信號量以前,咱們必須先知道——Linux提供兩種信號量:
(1) 內核信號量,由內核控制路徑使用
(2) 用戶態進程使用的信號量,這種信號量又分爲POSIX信號量和SYSTEM
V信號量。
POSIX信號量又分爲有名信號量和無名信號量。
有名信號量,其值保存在文件中, 因此它能夠用於線程也能夠用於進程間的同步。無名
信號量,其值保存在內存中。
假若對信號量沒有以上的全面認識的話,你就會很快發現本身在信號量的森林裏迷
失了方向。
三.內核信號量
1.內核信號量的構成
內核信號量相似於自旋鎖,由於當鎖關閉着時,它不容許內核控制路徑繼續進行。然而,
當內核控制路徑試圖獲取內核信號量鎖保護的忙資源時,相應的進程就被掛起。只有在資源
被釋放時,進程纔再次變爲可運行。
只有能夠睡眠的函數才能獲取內核信號量;中斷處理程序和可延遲函數都不能使用內
核信號量。
內核信號量是struct semaphore類型的對象,它在<asm/semaphore.h>中定義
一、定義並初始化信號量
struct semaphore sema;//定義一個 信號量
void sema_init (struct semaphore *sem, int val);
sema_init(sema,1); //將sem的值置爲1,表示資源空閒
sema_init(sema,0); //將sem的值置爲0,表示資源忙
二、鎖定信號量
void down(struct semaphore * sem); // 可引發睡眠
int down_interruptible(struct semaphore * sem); // down_interruptible能被信號打斷
int down_trylock(struct semaphore * sem); // 非阻塞函數,不會睡眠。沒法鎖定資源則立刻返回
三、釋放信號量
void up(struct semaphore * sem);
代碼示例:
ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{
//鎖定信號量
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}
//將用戶空間的數據複製到內核空間的global_var
if (copy_from_user(&global_var, buf, sizeof(int)))
{
up(&sem);
return - EFAULT;
}
//釋放信號量
up(&sem);
return sizeof(int);
}
四:完成量
完成量的主要機制就是說實現一個線程發送一個信號通知另外一個線程開始執行某個任務,就是說告訴一個線程某個事件已經發生,能夠在此事件基礎上作你想作的另外一個事件了。
完成量的使用:完成量的類型是:struct completion
一、定義和初始化完成量
struct completion com;//定義一個完成量
init_completion(&com);//初始化一個完成量
二、等待完成量(鎖定完成量) 該函數會阻塞調用進程,若是所等待的完成量沒有被喚醒,那就一直阻塞下去,並且不會被信號打斷;
wait_for_completion(&com)函數 //得到完成量
三、釋放完成量
當須要同步的任務完成以後,可使用complete(&com)函數來喚醒完成量,當喚醒以後,wait_for_completion()函數以後的代碼才能夠繼續執行。該函數只喚醒一個正在等待完成量comp的執行單元void complete_all(struct completion* comp):該函數喚醒全部正在等待同一個完成量comp的執行單元;
代碼示例:
1五、設備驅動中的阻塞和同步機制:
1六、內核中的中斷處理:
1七、內外存訪問: