IT168 技術文檔】在開始步入Linux設備
驅動程序的神祕世界以前,讓咱們從驅動程序開發人員的角度看幾個內核構成要素,熟悉一些基本的內核概念。咱們將學習內核定時器、同步機制以及
內存分配方法。不過,咱們仍是得從頭開始此次探索之旅。所以,本章要先瀏覽一下內核發出的啓動信息,而後再逐個講解一些有意思的點。
2.1 啓動過程php
圖2-1顯示了基於x86計算機Linux系統的啓動順序。第一步是BIOS從啓動設備中導入主引導記錄(MBR),接下來MBR中的代碼查看分區表並從活動分區讀取GRUB、LILO或SYSLINUX等引導裝入程序,以後引導裝入程序會加載壓縮後的內核映像並將控制權傳遞給它。內核取得控制權後,會將自身解壓縮並投入運轉。html
基於x86的處理器有兩種操做模式:實模式和保護模式。在實模式下,用戶僅可使用1 MB內存,而且沒有任何保護。保護模式要複雜得多,用戶可使用更多的高級功能(如分頁)。CPU必須中途將實模式切換爲保護模式。可是,這種切換是單向的,即不能從保護模式再切換回實模式。node
內核初始化的第一步是執行實模式下的彙編代碼,以後執行保護模式下init/main.c文件(上一章修改的源文件)中的start_kernel()函數。start_kernel()函數首先會初始化CPU子系統,以後讓內存和進程管理系統就位,接下來啓動外部總線和I/O設備,最後一步是激活初始化(init)程序,它是全部Linux進程的父進程。初始化進程執行啓動必要的內核服務的用戶空間腳本,而且最終派生控制檯終端程序以及顯示登陸(login)提示。linux
圖2-1 基於x86硬件上的Linux的啓動過程編程
本節內的3級標題都是圖2-2中的一條打印信息,這些信息來源於基於x86的筆記本電腦的Linux啓動過程。若是在其餘體系架構上啓動內核,消息以及語義可能會有所不一樣。緩存
2.1.1 BIOS-provided physical RAM map安全
內核會解析從BIOS中讀取到的系統內存映射,並率先將如下信息打印出來:網絡
BIOS-provided physical RAM map:數據結構
BIOS-e820: 0000000000000000 - 000000000009f000 (usable)多線程
...
BIOS-e820: 00000000ff800000 - 0000000100000000 (reserved)
實模式下的初始化代碼經過使用BIOS的int 0x15服務並執行0xe820號函數(即上面的BIOS-e820字符串)來得到系統的內存映射信息。內存映射信息中包含了預留的和可用的內存,內核將隨後使用這些信息建立其可用的內存池。在附錄B的B.1節,咱們會對BIOS提供的內存映射問題進行更深刻的講解。
圖2-2 內核啓動信息
2.1.2 758MB LOWMEM available
896 MB之內的常規的可被尋址的內存區域被稱做低端內存。內存分配函數kmalloc()就是從該區域分配內存的。高於896 MB的內存區域被稱爲高端內存,只有在採用特殊的方式進行映射後才能被訪問。
在啓動過程當中,內核會計算並顯示這些內存區內總的頁數。
2.1.3 Kernel command line: ro root=/dev/hda1
Linux的引導裝入程序一般會給內核傳遞一個命令行。命令行中的參數相似於傳遞給C程序中main()函數的argv[]列表,惟一的不一樣在於它們是傳遞給內核的。能夠在引導裝入程序的配置文件中增長命令行參數,固然,也能夠在運行過程當中修改引導裝入程序的提示行[1]。若是使用的是GRUB這個引導裝入程序,因爲發行版本的不一樣,其配置文件多是/boot/grub/grub.conf或者是/boot/grub/menu.lst。若是使用的是LILO,配置文件爲/etc/lilo.conf。下面給出了一個grub.conf文件的例子(增長了一些註釋),看了緊接着title kernel 2.6.23的那行代碼以後,你會明白前述打印信息的由來。
default 0 #Boot the 2.6.23 kernel by default
timeout 5 #5 second to alter boot order or parameters
title kernel 2.6.23 #Boot Option 1
#The boot image resides in the first partition of the first disk
#under the /boot/ directory and is named vmlinuz-2.6.23. 'ro'
#indicates that the root partition should be mounted read-only.
kernel (hd0,0)/boot/vmlinuz-2.6.23 ro root=/dev/hda1
#Look under section "Freeing initrd memory:387k freed"
initrd (hd0,0)/boot/initrd
#...
命令行參數將影響啓動過程當中的代碼執行路徑。舉一個例子,假設某命令行參數爲bootmode,若是該參數被設置爲1,意味着你但願在啓動過程當中打印一些調試信息並在啓動結束時切換到runlevel的第3級(初始化進程的啓動信息打印後就會了解runlevel的含義);若是bootmode參數被設置爲0,意味着你但願啓動過程相對簡潔,而且設置runlevel爲2。既然已經熟悉了init/main.c文件,下面就在該文件中增長以下修改:
static unsigned int bootmode = 1;
static int __init
is_bootmode_setup(char *str)
{
get_option(&str, &bootmode);
return 1;
}
/* Handle parameter "bootmode=" */
__setup("bootmode=", is_bootmode_setup);
if (bootmode) {
/* Print verbose output */
/* ... */
}
/* ... */
/* If bootmode is 1, choose an init runlevel of 3, else
switch to a run level of 2 */
if (bootmode) {
argv_init[++args] = "3";
} else {
argv_init[++args] = "2";
}
/* ... */
請從新編譯內核並嘗試運行新的修改。
2.1.4 Calibrating delay...1197.46 BogoMIPS (lpj=2394935)
在啓動過程當中,內核會計算處理器在一個jiffy時間內運行一個內部的延遲循環的次數。jiffy的含義是系統定時器2個連續的節拍之間的間隔。正如所料,該計算必須被校準到所用CPU的處理速度。校準的結果被存儲在稱爲loops_per_jiffy的內核變量中。使用loops_per_jiffy的一種狀況是某設備驅動程序但願進行小的微秒級別的延遲的時候。
爲了理解延遲—循環校準代碼,讓咱們看一下定義於init/calibrate.c文件中的calibrate_ delay()函數。該函數靈活地使用整型運算獲得了浮點的精度。以下的代碼片斷(有一些註釋)顯示了該函數的開始部分,這部分用於獲得一個loops_per_jiffy的粗略值:
loops_per_jiffy = (1 << 12); /* Initial approximation = 4096 */
printk(KERN_DEBUG 「Calibrating delay loop...「);
while ((loops_per_jiffy <<= 1) != 0) {
ticks = jiffies; /* As you will find out in the section, 「Kernel
Timers," the jiffies variable contains the
number of timer ticks since the kernel
started, and is incremented in the timer
interrupt handler */
while (ticks == jiffies); /* Wait until the start of the next jiffy */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
/* Did the wait outlast the current jiffy? Continue if it didn't */
ticks = jiffies - ticks;
if (ticks) break;
}
loops_per_jiffy >>= 1; /* This fixes the most significant bit and is
the lower-bound of loops_per_jiffy */
上述代碼首先假定loops_per_jiffy大於4096,這能夠轉化爲處理器速度大約爲每秒100萬條指令,即1 MIPS。接下來,它等待jiffy被刷新(1個新的節拍的開始),並開始運行延遲循環__delay(loops_per_jiffy)。若是這個延遲循環持續了1個jiffy以上,將使用之前的loops_per_jiffy值(將當前值右移1位)修復當前loops_per_jiffy的最高位;不然,該函數繼續經過左移loops_per_jiffy值來探測出其最高位。在內核計算出最高位後,它開始計算低位並微調其精度:
loopbit = loops_per_jiffy;
/* Gradually work on the lower-order bits */
while (lps_precision-- && (loopbit >>= 1)) {
loops_per_jiffy |= loopbit;
ticks = jiffies;
while (ticks == jiffies); /* Wait until the start of the next jiffy */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
if (jiffies != ticks) /* longer than 1 tick */
loops_per_jiffy &= ~loopbit;
}
上述代碼計算出了延遲循環跨越jiffy邊界時loops_per_jiffy的低位值。這個被校準的值可被用於獲取BogoMIPS(其實它是一個並不是科學的處理器速度指標)。可使用BogoMIPS做爲衡量處理器運行速度的相對尺度。在1.6G Hz 基於Pentium M的筆記本電腦上,根據前述啓動過程的打印信息,循環校準的結果是:loops_per_jiffy的值爲2394935。得到BogoMIPS的方式以下:
BogoMIPS = loops_per_jiffy * 1秒內的jiffy數*延遲循環消耗的指令數(以百萬爲單位)
= (2394935 * HZ * 2) / (1000000)
= (2394935 * 250 * 2) / (1000000)
= 1197.46(與啓動過程打印信息中的值一致)
在2.4節將更深刻闡述jiffy、HZ和loops_per_jiffy。
2.1.5 Checking HLT instruction
因爲Linux內核支持多種硬件平臺,啓動代碼會檢查體系架構相關的bug。其中一項工做就是驗證停機(HLT)指令。
x86處理器的HLT指令會將CPU置入一種低功耗睡眠模式,直到下一次硬件中斷髮生以前維持不變。當內核想讓CPU進入空閒狀態時(查看arch/x86/kernel/process_32.c文件中定義的cpu_idle()函數),它會使用HLT指令。對於有問題的CPU而言,命令行參數no-hlt能夠禁止HLT指令。若是no-hlt被設置,在空閒的時候,內核會進行忙等待而不是經過HLT給CPU降溫。
當init/main.c中的啓動代碼調用include/asm-your-arch/bugs.h中定義的check_bugs()時,會打印上述信息。
2.1.6 NET: Registered protocol family 2
Linux套接字(socket)層是用戶空間應用程序訪問各類網絡協議的統一接口。每一個協議經過include/linux/socket.h文件中定義的分配給它的獨一無二的系列號註冊。上述打印信息中的Family 2表明af_inet(互聯網協議)。
啓動過程當中另外一個常見的註冊協議系列是AF_NETLINK(Family 16)。網絡連接套接字提供了用戶進程和內核通訊的方法。經過網絡連接套接字可完成的功能還包括存取路由表和地址解析協議(ARP)表(include/linux/netlink.h文件給出了完整的用法列表)。對於此類任務而言,網絡連接套接字比系統調用更合適,由於前者具備採用異步機制、更易於實現和可動態連接的優勢。
內核中常常使能的另外一個協議系列是AF_Unix或Unix-domain套接字。X Windows等程序使用它們在同一個系統上進行進程間通訊。
2.1.7 Freeing initrd memory: 387k freed
initrd是一種由引導裝入程序加載的常駐內存的虛擬磁盤映像。在內核啓動後,會將其掛載爲初始根文件系統,這個初始根文件系統中存放着掛載實際根文件系統磁盤分區時所依賴的可動態鏈接的模塊。因爲內核可運行於各類各樣的存儲控制器硬件平臺上,把全部可能的磁盤驅動程序都直接放進基本的內核映像中並不可行。你所使用的系統的存儲設備的驅動程序被打包放入了initrd中,在內核啓動後、實際的根文件系統被掛載以前,這些驅動程序才被加載。使用mkinitrd命令能夠建立一個initrd映像。
2.6內核提供了一種稱爲initramfs的新功能,它在幾個方面較initrd更爲優秀。後者模擬了一個磁盤(於是被稱爲initramdisk或initrd),會帶來Linux塊I/O子系統的開銷(如緩衝);前者基本上如同一個被掛載的文件系統同樣,由自身獲取緩衝(所以被稱做initramfs)。
不一樣於initrd,基於頁緩衝創建的initramfs如同頁緩衝同樣會動態地變大或縮小,從而減小了其內存消耗。另外,initrd要求你的內核映像包含initrd所使用的文件系統(例如,若是initrd爲EXT2文件系統,內核必須包含EXT2驅動程序),然而initramfs不須要文件系統支持。再者,因爲initramfs只是頁緩衝之上的一小層,所以它的代碼量很小。
用戶能夠將初始根文件系統打包爲一個cpio壓縮包[1],並經過initrd=命令行參數傳遞給內核。固然,也能夠在內核配置過程當中經過INITRAMFS_SOURCE選項直接編譯進內核。對於後一種方式而言,用戶能夠提供cpio壓縮包的文件名或者包含initramfs的目錄樹。在啓動過程當中,內核會將文件解壓縮爲一個initramfs根文件系統,若是它找到了/init,它就會執行該頂層的程序。這種獲取初始根文件系統的方法對於嵌入式系統而言特別有用,由於在嵌入式系統中系統資源很是寶貴。使用mkinitramfs能夠建立一個initramfs映像,查看文檔Documentation/filesystems/ramfs- rootfs-initramfs.txt可得到更多信息。
在本例中,咱們使用的是經過initrd=命令行參數向內核傳遞初始根文件系統cpio壓縮包的方式。在將壓縮包中的內容解壓爲根文件系統後,內核將釋放該壓縮包所佔據的內存(本例中爲387 KB)並打印上述信息。釋放後的頁面會被分發給內核中的其餘部分以便被申請。
在嵌入式系統開發過程當中,initrd和initramfs有時候也可被用做嵌入式設備上實際的根文件系統。
2.1.8 io scheduler anticipatory registered (default)
I/O調度器的主要目標是經過減小磁盤的定位次數來增長系統的吞吐率。在磁盤定位過程當中,磁頭須要從當前的位置移動到感興趣的目標位置,這會帶來必定的延遲。2.6內核提供了4種不一樣的I/O調度器:Deadline、Anticipatory、Complete Fair Queuing以及NOOP。從上述內核打印信息能夠看出,本例將Anticipatory 設置爲了默認的I/O調度器。
2.1.9 Setting up standard PCI resources
啓動過程的下一階段會初始化I/O總線和外圍控制器。內核會經過遍歷PCI總線來探測PCI硬件,接下來再初始化其餘的I/O子系統。從圖2-3中咱們會看到SCSI子系統、USB控制器、視頻芯片(855北橋芯片組信息中的一部分)、串行端口(本例中爲8250 UART)、PS/2鍵盤和鼠標、軟驅、ramdisk、loopback設備、IDE控制器(本例中爲ICH4南橋芯片組中的一部分)、觸控板、以太網控制器(本例中爲e1000)以及PCMCIA控制器初始化的啓動信息。圖2-3中 符號指向的爲I/O設備的標識(ID)。
![](http://static.javashuo.com/static/loading.gif)
圖2-3 在啓動過程當中初始化總線和外圍控制器
本書會以單獨的章節討論大部分上述驅動程序子系統,請注意若是驅動程序以模塊的形式被動態連接到內核,其中的一些消息也許只有在內核啓動後纔會被顯示。
2.1.10 EXT3-fs: mounted filesystem
EXT3文件系統已經成爲Linux事實上的文件系統。EXT3在退役的EXT2文件系統基礎上增添了日誌層,該層可用於崩潰後文件系統的快速恢復。它的目標是不經由耗時的文件系統檢查(fsck)操做便可得到一個一致的文件系統。EXT2仍然是新文件系統的工做引擎,可是EXT3層會在進行實際的磁盤改變以前記錄文件交互的日誌。EXT3向後兼容於EXT2,所以,你能夠在你現存的EXT2文件系統上加上EXT3或者由EXT3返回到EXT2文件系統。
EXT3會啓動一個稱爲kjournald的內核輔助線程(在接下來的一章中將深刻討論內核線程)來完成日誌功能。在EXT3投入運轉之後,內核掛載根文件系統並作好「業務」上的準備:
EXT3-fs: mounted filesystem with ordered data mode
kjournald starting. Commit interval 5 seconds
VFS: Mounted root (ext3 filesystem).
2.1.11 INIT: version 2.85 booting
全部Linux進程的父進程init是內核完成啓動序列後運行的第1個程序。在init/main.c的最後幾行,內核會搜索一個不一樣的位置以定位到init:
if (ramdisk_execute_command) { /* Look for /init in initramfs */
run_init_process(ramdisk_execute_command);
}
if (execute_command) { /* You may override init and ask the kernel
to execute a custom program using the
"init=" kernel command-line argument. If
you do that, execute_command points to the
specified program */
run_init_process(execute_command);
}
/* Else search for init or sh in the usual places .. */
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
init會接受/etc/inittab的指引。它首先執行/etc/rc.sysinit中的系統初始化腳本,該腳本的一項最重要的職責就是激活對換(swap)分區,這會致使以下啓動信息被打印:
Adding 1552384k swap on /dev/hda6
讓咱們來仔細看看上述這段話的意思。Linux用戶進程擁有3 GB的虛擬地址空間(見2.7節),構成「工做集」的頁被保存在RAM中。可是,若是有太多程序須要內存資源,內核會釋放一些被使用了的RAM頁面並將其存儲到稱爲對換空間(swap space)的磁盤分區中。根據經驗法則,對換分區的大小應該是RAM的2倍。在本例中,對換空間位於/dev/hda6這個磁盤分區,其大小爲1 552 384 KB。
接下來,init開始運行/etc/rc.d/rcX.d/目錄中的腳本,其中X是inittab中定義的運行級別。runlevel是根據預期的工做模式所進入的執行狀態。例如,多用戶文本模式意味着runlevel爲3,X Windows則意味着runlevel爲5。所以,當你看到INIT: Entering runlevel 3這條信息的時候,init就已經開始執行/etc/rc.d/rc3.d/目錄中的腳本了。這些腳本會啓動動態設備命名子系統(第4章中將討論udev),並加載網絡、音頻、存儲設備等驅動程序所對應的內核模塊:
Starting udev: [ OK ]
Initializing hardware... network audio storage [Done]
...
最後,init發起虛擬控制檯終端,你如今就能夠登陸了。
2.2 內核模式和用戶模式
MS-DOS等操做系統在單一的CPU模式下運行,可是一些類Unix的操做系統則使用了雙模式,能夠有效地實現時間共享。在Linux機器上,CPU要麼處於受信任的內核模式,要麼處於受限制的用戶模式。除了內核自己處於內核模式之外,全部的用戶進程都運行在用戶模式之中。
內核模式的代碼能夠無限制地訪問全部處理器指令集以及所有內存和I/O空間。若是用戶模式的進程要享有此特權,它必須經過系統調用向設備驅動程序或其餘內核模式的代碼發出請求。另外,用戶模式的代碼容許發生缺頁,而內核模式的代碼則不容許。
在2.4和更早的內核中,僅僅用戶模式的進程能夠被上下文切換出局,由其餘進程搶佔。除非發生如下兩種狀況,不然內核模式代碼能夠一直獨佔CPU:
(1) 它自願放棄CPU;
(2) 發生中斷或異常。
2.6內核引入了內核搶佔,大多數內核模式的代碼也能夠被搶佔。
2.3 進程上下文和中斷上下文
內核能夠處於兩種上下文:進程上下文和中斷上下文。在系統調用以後,用戶應用程序進入內核空間,此後內核空間針對用戶空間相應進程的表明就運行於進程上下文。異步發生的中斷會引起中斷處理程序被調用,中斷處理程序就運行於中斷上下文。中斷上下文和進程上下文不可能同時發生。
運行於進程上下文的內核代碼是可搶佔的,但進程上下文則會一直運行至結束,不會被搶佔。所以,內核會限制中斷上下文的工做,不容許其執行以下操做:
(1) 進入睡眠狀態或主動放棄CPU;
(2) 佔用互斥體;
(3) 執行耗時的任務;
(4) 訪問用戶空間虛擬內存。
本書4.2節會對中斷上下文進行更深刻的討論。
2.4 內核定時器
內核中許多部分的工做都高度依賴於時間信息。Linux內核利用硬件提供的不一樣的定時器以支持忙等待或睡眠等待等時間相關的服務。忙等待時,CPU會不斷運轉。可是睡眠等待時,進程將放棄CPU。所以,只有在後者不可行的狀況下,才考慮使用前者。內核也提供了某些便利,能夠在特定的時間以後調度某函數運行。
咱們首先來討論一些重要的內核定時器變量(jiffies、HZ和xtime)的含義。接下來,咱們會使用Pentium時間戳計數器(TSC)測量基於Pentium的系統的運行次數。以後,咱們也分析一下Linux怎麼使用實時鐘(RTC)。
2.4.1 HZ和Jiffies
系統定時器能以可編程的頻率中斷處理器。此頻率即爲每秒的定時器節拍數,對應着內核變量HZ。選擇合適的HZ值須要權衡。HZ值大,定時器間隔時間就小,所以進程調度的準確性會更高。可是,HZ值越大也會致使開銷和電源消耗更多,由於更多的處理器週期將被耗費在定時器中斷上下文中。
HZ的值取決於體系架構。在x86系統上,在2.4內核中,該值默認設置爲100;在2.6內核中,該值變爲1000;而在2.6.13中,它又被下降到了250。在基於ARM的平臺上,2.6內核將HZ設置爲100。在目前的內核中,能夠在編譯內核時經過配置菜單選擇一個HZ值。該選項的默認值取決於體系架構的版本。
2.6.21內核支持無節拍的內核(CONFIG_NO_HZ),它會根據系統的負載動態觸發定時器中斷。無節拍系統的實現超出了本章的討論範圍,再也不詳述。
jiffies變量記錄了系統啓動以來,系統定時器已經觸發的次數。內核每秒鐘將jiffies變量增長HZ次。所以,對於HZ值爲100的系統,1個jiffy等於10ms,而對於HZ爲1000的系統,1個jiffy僅爲1ms。
爲了更好地理解HZ和jiffies變量,請看下面的取自IDE驅動程序(drivers/ide/ide.c)的代碼片斷。該段代碼會一直輪詢磁盤驅動器的忙狀態:
unsigned long timeout = jiffies + (3*HZ);
while (hwgroup->busy) {
/* ... */
if (time_after(jiffies, timeout)) {
return -EBUSY;
}
/* ... */
}
return SUCCESS;
若是忙條件在3s內被清除,上述代碼將返回SUCCESS,不然,返回-EBUSY。3*HZ是3s內的jiffies數量。計算出來的超時jiffies + 3*HZ將是3s超時發生後新的jiffies值。time_after()的功能是將目前的jiffies值與請求的超時時間對比,檢測溢出。相似函數還包括time_before()、time_before_eq()和time_after_eq()。
jiffies被定義爲volatile類型,它會告訴編譯器不要優化該變量的存取代碼。這樣就確保了每一個節拍發生的定時器中斷處理程序都能更新jiffies值,而且循環中的每一步都會從新讀取jiffies值。
對於jiffies向秒轉換,能夠查看USB主機控制器驅動程序drivers/usb/host/ehci-sched.c中的以下代碼片斷:
if (stream->rescheduled) {
ehci_info(ehci, "ep%ds-iso rescheduled " "%lu times in %lu
seconds\n", stream->bEndpointAddress, is_in? "in":
"out", stream->rescheduled,
((jiffies – stream->start)/HZ));
}
上述調試語句計算出USB端點流(見第11章)被從新調度stream->rescheduled次所耗費的秒數。jiffies-stream->start是從開始到如今消耗的jiffies數量,將其除以HZ就獲得了秒數值。
假定jiffies值爲1000,32位的jiffies會在大約50天的時間內溢出。因爲系統的運行時間能夠比該時間長許多倍,所以,內核提供了另外一個變量jiffies_64以存放64位(u64)的jiffies。連接器將jiffies_64的低32位與32位的jiffies指向同一個地址。在32位的機器上,爲了將一個u64變量賦值給另外一個,編譯器須要2條指令,所以,讀jiffies_64的操做不具有原子性。能夠將drivers/cpufreq/cpufreq_stats.c文件中定義的cpufreq_stats_update()做爲實例來學習。
2.4.2 長延時
在內核中,以jiffies爲單位進行的延遲一般被認爲是長延時。一種可能但非最佳的實現長延時的方法是忙等待。實現忙等待的函數有「佔着茅坑不拉屎」之嫌,它自己不利用CPU進行有用的工做,同時還不讓其餘程序使用CPU。以下代碼將佔用CPU 1秒:
unsigned long timeout = jiffies + HZ;
while (time_before(jiffies, timeout)) continue;
實現長延時的更好方法是睡眠等待而不是忙等待,在這種方式中,本進程會在等待時將處理器出讓給其餘進程。schedule_timeout()完成此功能:
unsigned long timeout = HZ;
schedule_timeout(timeout); /* Allow other parts of the kernel to run */
這種延時僅僅確保超時較低時的精度。因爲只有在時鐘節拍引起的內核調度纔會更新jiffies,因此不管是在內核空間仍是在用戶空間,都很難使超時的精度比HZ更大了。另外,即便你的進程已經超時並可被調度,可是調度器仍然可能基於優先級策略選擇運行隊列的其餘進程[1]。
用於睡眠等待的另2個函數是wait_event_timeout()和msleep(),它們的實現都基於schedule_timeout()。wait_event_timeout()的使用場合是:在一個特定的條件知足或者超時發生後,但願代碼繼續運行。msleep()表示睡眠指定的時間(以毫秒爲單位)。
這種長延時技術僅僅適用於進程上下文。睡眠等待不能用於中斷上下文,由於中斷上下文不容許執行schedule()或睡眠(4.2節給出了中斷上下文能夠作和不能作的事情)。在中斷中進行短期的忙等待是可行的,可是進行長時間的忙等則被認爲不可赦免的罪行。在中斷禁止時,進行長時間的忙等待也被看做禁忌。
爲了支持在未來的某時刻進行某項工做,內核也提供了定時器API。能夠經過init_timer()動態定義一個定時器,也能夠經過DEFINE_TIMER()靜態建立定時器。而後,將處理函數的地址和參數綁定給一個timer_list,並使用add_timer()註冊它便可:
#include <linux/timer.h>
struct timer_list my_timer;
init_timer(&my_timer); /* Also see setup_timer() */
my_timer.expire = jiffies + n*HZ; /* n is the timeout in number of seconds */
my_timer.function = timer_func; /* Function to execute after n seconds */
my_timer.data = func_parameter; /* Parameter to be passed to timer_func */
add_timer(&my_timer); /* Start the timer */
上述代碼只會讓定時器運行一次。若是想讓timer_func()函數週期性地執行,須要在timer_func()加上相關代碼,指定其在下次超時後調度自身:
static void timer_func(unsigned long func_parameter)
{
/* Do work to be done periodically */
/* ... */
init_timer(&my_timer);
my_timer.expire = jiffies + n*HZ;
my_timer.data = func_parameter;
my_timer.function = timer_func;
add_timer(&my_timer);
}
你可使用mod_timer()修改my_timer的到期時間,使用del_timer()取消定時器,或使用timer_pending()以查看my_timer當前是否處於等待狀態。查看kernel/timer.c源代碼,會發現schedule_timeout()內部就使用了這些API。
clock_settime()和clock_gettime()等用戶空間函數可用於得到內核定時器服務。用戶應用程序可使用setitimer()和getitimer()來控制一個報警信號在特定的超時後發生。
2.4.3 短延時
在內核中,小於jiffy的延時被認爲是短延時。這種延時在進程或中斷上下文均可能發生。因爲不可能使用基於jiffy的方法實現短延時,以前討論的睡眠等待將再也不能用於短的超時。這種狀況下,惟一的解決途徑就是忙等待。
實現短延時的內核API包括mdelay()、udelay()和ndelay(),分別支持毫秒、微秒和納秒級的延時。這些函數的實際實現取決於體系架構,並且也並不是在全部平臺上都被完整實現。
忙等待的實現方法是測量處理器執行一條指令的時間,爲了延時,執行必定數量的指令。從前文可知,內核會在啓動過程當中進行測量並將該值存儲在loops_per_jiffy變量中。短延時API就使用了loops_per_jiffy值來決定它們須要進行循環的數量。爲了實現握手進程中1微秒的延時,USB主機控制器驅動程序(drivers/usb/host/ehci-hcd.c)會調用udelay(),而udelay()會內部調用loops_per_jiffy:
do {
result = ehci_readl(ehci, ptr);
/* ... */
if (result == done) return 0;
udelay(1); /* Internally uses loops_per_jiffy */
usec--;
} while (usec > 0);
2.4.4 Pentium時間戳計數器
時間戳計數器(TSC)是Pentium兼容處理器中的一個計數器,它記錄自啓動以來處理器消耗的時鐘週期數。因爲TSC隨着處理器週期速率的比例的變化而變化,所以提供了很是高的精確度。TSC一般被用於剖析和監測代碼。使用rdtsc指令可測量某段代碼的執行時間,其精度達到微秒級。TSC的節拍能夠被轉化爲秒,方法是將其除以CPU時鐘速率(可從內核變量cpu_khz讀取)。
在以下代碼片斷中,low_tsc_ticks和high_tsc_ticks分別包含了TSC的低32位和高32位。低32位可能在數秒內溢出(具體時間取決於處理器速度),可是這已經用於許多代碼的剖析了:
unsigned long low_tsc_ticks0, high_tsc_ticks0;
unsigned long low_tsc_ticks1, high_tsc_ticks1;
unsigned long exec_time;
rdtsc(low_tsc_ticks0, high_tsc_ticks0); /* Timestamp before */
printk("Hello World\n"); /* Code to be profiled */
rdtsc(low_tsc_ticks1, high_tsc_ticks1); /* Timestamp after */
exec_time = low_tsc_ticks1 - low_tsc_ticks0;
在1.8 GHz Pentium 處理器上,exec_time的結果爲871(或半微秒)。
在2.6.21內核中,針對高精度定時器的支持(CONFIG_HIGH_RES_TIMERS)已經被融入了內核。它使用了硬件特定的高速定時器來提供對nanosleep()等API高精度的支持。在基於Pentium的機器上,內核藉助TSC實現這一功能。
2.4.5 實時鐘
RTC在非易失性存儲器上記錄絕對時間。在x86 PC上,RTC位於由電池供電[1]的互補金屬氧化物半導體(CMOS)存儲器的頂部。從第5章的圖5-1能夠看出傳統PC體系架構中CMOS的位置。在嵌入式系統中,RTC可能被集成處處理器中,也可能經過I2C或SPI總線在外部鏈接,見第8章。
使用RTC能夠完成以下工做:
(1) 讀取、設置絕對時間,在時鐘更新時產生中斷;
(2) 產生頻率爲2~8192 Hz之間的週期性中斷;
(3) 設置報警信號。
許多應用程序須要使用絕對時間[或稱牆上時間(wall time)]。jiffies是相對於系統啓動後的時間,它不包含牆上時間。內核將牆上時間記錄在xtime變量中,在啓動過程當中,會根據從RTC讀取到的目前的牆上時間初始化xtime,在系統停機後,牆上時間會被寫回RTC。你可使用do_gettimeofday()讀取牆上時間,其最高精度由硬件決定:
#include <linux/time.h>
static struct timeval curr_time;
do_gettimeofday(&curr_time);
my_timestamp = cpu_to_le32(curr_time.tv_sec); /* Record timestamp */
用戶空間也包含一系列能夠訪問牆上時間的函數,包括:
(1) time(),該函數返回日曆時間,或重新紀元(1970年1月1日00:00:00)以來經歷的秒數;
(2) localtime(),以分散的形式返回日曆時間;
(3) mktime(),進行localtime()函數的反向工做;
(4) gettimeofday(),若是你的平臺支持,該函數將以微秒精度返回日曆時間。
用戶空間使用RTC的另外一種途徑是經過字符設備/dev/rtc來進行,同一時刻只有一個進程容許返回該字符設備。
在第5章和第8章,本書將更深刻討論RTC驅動程序。另外,在第19章給出了一個使用/dev/rtc以微秒級精度執行週期性工做的應用程序示例。
2.5 內核中的併發
隨着多核筆記本電腦時代的到來,對稱多處理器(SMP)的使用再也不被限於高科技用戶。SMP和內核搶佔是多線程執行的兩種場景。多個線程可以同時操做共享的內核數據結構,所以,對這些數據結構的訪問必須被串行化。
接下來,咱們會討論併發訪問狀況下保護共享內核資源的基本概念。咱們以一個簡單的例子開始,並逐步引入中斷、內核搶佔和SMP等複雜概念。
2.5.1 自旋鎖和互斥體
訪問共享資源的代碼區域稱做臨界區。自旋鎖(spinlock)和互斥體(mutex,mutual exclusion的縮寫)是保護內核臨界區的兩種基本機制。咱們逐個分析。
自旋鎖能夠確保在同時只有一個線程進入臨界區。其餘想進入臨界區的線程必須不停地原地打轉,直到第1個線程釋放自旋鎖。注意:這裏所說的線程不是內核線程,而是執行的線程。
下面的例子演示了自旋鎖的基本用法:
#include <linux/spinlock.h>
spinlock_t mylock = SPIN_LOCK_UNLOCKED; /* Initialize */
/* Acquire the spinlock. This is inexpensive if there
* is no one inside the critical section. In the face of
* contention, spinlock() has to busy-wait.
*/
spin_lock(&mylock);
/* ... Critical Section code ... */
spin_unlock(&mylock); /* Release the lock */
與自旋鎖不一樣的是,互斥體在進入一個被佔用的臨界區以前不會原地打轉,而是使當前線程進入睡眠狀態。若是要等待的時間較長,互斥體比自旋鎖更合適,由於自旋鎖會消耗CPU資源。在使用互斥體的場合,多於2次進程切換時間均可被認爲是長時間,所以一個互斥體會引發本線程睡眠,而當其被喚醒時,它須要被切換回來。
所以,在不少狀況下,決定使用自旋鎖仍是互斥體相對來講很容易:
(1) 若是臨界區須要睡眠,只能使用互斥體,由於在得到自旋鎖後進行調度、搶佔以及在等待隊列上睡眠都是非法的;
(2) 因爲互斥體會在面臨競爭的狀況下將當前線程置於睡眠狀態,所以,在中斷處理函數中,只能使用自旋鎖。(第4章將介紹更多的關於中斷上下文的限制。)
下面的例子演示了互斥體使用的基本方法:
#include <linux/mutex.h>
/* Statically declare a mutex. To dynamically
create a mutex, use mutex_init() */
static DEFINE_MUTEX(mymutex);
/* Acquire the mutex. This is inexpensive if there
* is no one inside the critical section. In the face of
* contention, mutex_lock() puts the calling thread to sleep.
*/
mutex_lock(&mymutex);
/* ... Critical Section code ... */
mutex_unlock(&mymutex); /* Release the mutex */
爲了論證併發保護的用法,咱們首先從一個僅存在於進程上下文的臨界區開始,並如下面的順序逐步增長複雜性:
(1) 非搶佔內核,單CPU狀況下存在於進程上下文的臨界區;
(2) 非搶佔內核,單CPU狀況下存在於進程和中斷上下文的臨界區;
(3) 可搶佔內核,單CPU狀況下存在於進程和中斷上下文的臨界區;
(4) 可搶佔內核,SMP狀況下存在於進程和中斷上下文的臨界區。
舊的信號量接口
互斥體接口代替了舊的信號量接口(semaphore)。互斥體接口是從-rt樹演化而來的,在2.6.16內核中被融入主線內核。
儘管如此,可是舊的信號量仍然在內核和驅動程序中普遍使用。信號量接口的基本用法以下:
#include <asm/semaphore.h> /* Architecture dependent header */
/* Statically declare a semaphore. To dynamically
create a semaphore, use init_MUTEX() */
static DECLARE_MUTEX(mysem);
down(&mysem); /* Acquire the semaphore */
/* ... Critical Section code ... */
up(&mysem); /* Release the semaphore */
1. 案例1:進程上下文,單CPU,非搶佔內核
這種狀況最爲簡單,不須要加鎖,所以再也不贅述。
2. 案例2:進程和中斷上下文,單CPU,非搶佔內核
在這種狀況下,爲了保護臨界區,僅僅須要禁止中斷。如圖2-4所示,假定進程上下文的執行單元A、B以及中斷上下文的執行單元C都企圖進入相同的臨界區。
圖2-4 進程和中斷上下文進入臨界區
因爲執行單元C老是在中斷上下文執行,它會優先於執行單元A和B,所以,它不用擔憂保護的問題。執行單元A和B也沒必要關心彼此會被互相打斷,由於內核是非搶佔的。所以,執行單元A和B僅僅須要擔憂C會在它們進入臨界區的時候強行進入。爲了實現此目的,它們會在進入臨界區以前禁止中斷:
Point A:
local_irq_disable(); /* Disable Interrupts in local CPU */
/* ... Critical Section ... */
local_irq_enable(); /* Enable Interrupts in local CPU */
可是,若是當執行到Point A的時候已經被禁止,local_irq_enable()將產生反作用,它會從新使能中斷,而不是恢復以前的中斷狀態。能夠這樣修復它:
unsigned long flags;
Point A:
local_irq_save(flags); /* Disable Interrupts */
/* ... Critical Section ... */
local_irq_restore(flags); /* Restore state to what it was at Point A */
不論Point A的中斷處於什麼狀態,上述代碼都將正確執行。
3. 案例3:進程和中斷上下文,單CPU,搶佔內核
若是內核使能了搶佔,僅僅禁止中斷將沒法確保對臨界區的保護,由於另外一個處於進程上下文的執行單元可能會進入臨界區。從新回到圖2-4,如今,除了C之外,執行單元A和B必須提防彼此。顯而易見,解決該問題的方法是在進入臨界區以前禁止內核搶佔、中斷,並在退出臨界區的時候恢復內核搶佔和中斷。所以,執行單元A和B使用了自旋鎖API的irq變體:
unsigned long flags;
Point A:
/* Save interrupt state.
* Disable interrupts - this implicitly disables preemption */
spin_lock_irqsave(&mylock, flags);
/* ... Critical Section ... */
/* Restore interrupt state to what it was at Point A */
spin_unlock_irqrestore(&mylock, flags);
咱們不須要在最後顯示地恢復Point A的搶佔狀態,由於內核自身會經過一個名叫搶佔計數器的變量維護它。在搶佔被禁止時(經過調用preempt_disable()),計數器值會增長;在搶佔被使能時(經過調用preempt_enable()),計數器值會減小。只有在計數器值爲0的時候,搶佔才發揮做用。
4. 案例4:進程和中斷上下文,SMP機器,搶佔內核
如今假設臨界區執行於SMP機器上,並且你的內核配置了CONFIG_SMP和CONFIG_PREEMPT。
到目前爲止討論的場景中,自旋鎖原語發揮的做用僅限於使能和禁止搶佔和中斷,時間的鎖功能並未被徹底編譯進來。在SMP機器內,鎖邏輯被編譯進來,並且自旋鎖原語確保了SMP安全性。SMP使能的含義以下:
unsigned long flags;
Point A:
/*
- Save interrupt state on the local CPU
- Disable interrupts on the local CPU. This implicitly disables preemption.
- Lock the section to regulate access by other CPUs
*/
spin_lock_irqsave(&mylock, flags);
/* ... Critical Section ... */
/*
- Restore interrupt state and preemption to what it
was at Point A for the local CPU
- Release the lock
*/
spin_unlock_irqrestore(&mylock, flags);
在SMP系統上,獲取自旋鎖時,僅僅本CPU上的中斷被禁止。所以,一個進程上下文的執行單元(圖2-4中的執行單元A)在一個CPU上運行的同時,一箇中斷處理函數(圖2-4中的執行單元C)可能運行在另外一個CPU上。非本CPU上的中斷處理函數必須自旋等待本CPU上的進程上下文代碼退出臨界區。中斷上下文須要調用spin_lock()/spin_unlock():
spin_lock(&mylock);
/* ... Critical Section ... */
spin_unlock(&mylock);
除了有irq變體之外,自旋鎖也有底半部(BH)變體。在鎖被獲取的時候,spin_lock_bh()會禁止底半部,而spin_unlock_bh()則會在鎖被釋放時從新使能底半部。咱們將在第4章討論底半部。
-rt樹
實時(-rt)樹,也被稱做CONFIG_PREEMPT_RT補丁集,實現了內核中一些針對低延時的修改。該補丁集能夠從www.kernel.org/pub/linux/kernel/projects/rt下載,它容許內核的大部分位置可被搶佔,可是用自旋鎖代替了一些互斥體。它也合併了一些高精度的定時器。數個-rt功能已經被融入了主線內核。詳細的文檔見http://rt.wiki.kernel.org/。
爲了提升性能,內核也定義了一些針對特定環境的特定的鎖原語。使能適用於代碼執行場景的互斥機制將使代碼更高效。下面來看一下這些特定的互斥機制。
2.5.2 原子操做
原子操做用於執行輕量級的、僅執行一次的操做,例如修改計數器、有條件的增長值、設置位等。原子操做能夠確保操做的串行化,再也不須要鎖進行併發訪問保護。原子操做的具體實現取決於體系架構。
爲了在釋放內核網絡緩衝區(稱爲skbuff)以前檢查是否還有餘留的數據引用,定義於net/core/skbuff.c文件中的skb_release_data()函數將進行以下操做:
1 if (!skb->cloned ||
2 /* Atomically decrement and check if the returned value is zero */
3 !atomic_sub_return(skb->nohdr ? (1 << SKB_DATAREF_SHIFT) + 1 :
4 1,&skb_shinfo(skb)->dataref)) {
5 /* ... */
6 kfree(skb->head);
7 }
當skb_release_data()執行的時候,另外一個調用skbuff_clone()(也在net/core/skbuff.c文件中定義)的執行單元也許在同步地增長數據引用計數值:
/* ... */
/* Atomically bump up the data reference count */
atomic_inc(&(skb_shinfo(skb)->dataref));
/* ... */
原子操做的使用將確保數據引用計數不會被這兩個執行單元「蹂躪」。它也消除了使用鎖去保護單一整型變量的爭論。
內核也支持set_bit()、clear_bit()和test_and_set_bit()操做,它們可用於原子地位修改。查看include/asm-your-arch/atomic.h文件能夠看出你所在體系架構所支持的原子操做。
2.5.3 讀—寫鎖
另外一個特定的併發保護機制是自旋鎖的讀—寫鎖變體。若是每一個執行單元在訪問臨界區的時候要麼是讀要麼是寫共享的數據結構,可是它們都不會同時進行讀和寫操做,那麼這種鎖是最好的選擇。容許多個讀線程同時進入臨界區。讀自旋鎖能夠這樣定義:
rwlock_t myrwlock = RW_LOCK_UNLOCKED;
read_lock(&myrwlock); /* Acquire reader lock */
/* ... Critical Region ... */
read_unlock(&myrwlock); /* Release lock */
可是,若是一個寫線程進入了臨界區,那麼其餘的讀和寫都不容許進入。寫鎖的用法以下:
rwlock_t myrwlock = RW_LOCK_UNLOCKED;
write_lock(&myrwlock); /* Acquire writer lock */
/* ... Critical Region ... */
write_unlock(&myrwlock); /* Release lock */
net/ipx/ipx_route.c中的IPX路由代碼是使用讀—寫鎖的真實示例。一個稱做ipx_routes_lock的讀—寫鎖將保護IPX路由表的併發訪問。要經過查找路由表實現包轉發的執行單元須要請求讀鎖。須要添加和刪除路由表中入口的執行單元必須獲取寫鎖。因爲經過讀路由表的狀況比更新路由表的狀況多得多,使用讀—寫鎖提升了性能。
和傳統的自旋鎖同樣,讀—寫鎖也有相應的irq變體:read_lock_irqsave()、read_unlock_ irqrestore()、write_lock_irqsave()和write_unlock_irqrestore()。這些函數的含義與傳統自旋鎖相應的變體類似。
2.6內核引入的順序鎖(seqlock)是一種支持寫多於讀的讀—寫鎖。在一個變量的寫操做比讀操做多得多的狀況下,這種鎖很是有用。前文討論的jiffies_64變量就是使用順序鎖的一個例子。寫線程沒必要等待一個已經進入臨界區的讀,所以,讀線程也許會發現它們進入臨界區的操做失敗,所以須要重試:
u64 get_jiffies_64(void) /* Defined in kernel/time.c */
{
unsigned long seq;
u64 ret;
do {
seq = read_seqbegin(&xtime_lock);
ret = jiffies_64;
} while (read_seqretry(&xtime_lock, seq));
return ret;
}
寫者會使用write_seqlock()和write_sequnlock()保護臨界區。
2.6內核還引入了另外一種稱爲讀—複製—更新(RCU)的機制。該機制用於提升讀操做遠多於寫操做時的性能。其基本理念是讀線程不須要加鎖,可是寫線程會變得更加複雜,它們會在數據結構的一份副本上執行更新操做,並代替讀者看到的指針。爲了確保全部正在進行的讀操做的完成,原子副本會一直被保持到全部CPU上的下一次上下文切換。使用RCU的狀況很複雜,所以,只有在確保你確實須要使用它而不是前文的其餘原語的時候,才適宜選擇它。include/linux/ rcupdate.h文件中定義了RCU的數據結構和接口函數,Documentation/RCU/*提供了豐富的文檔。
fs/dcache.c文件中包含一個RCU的使用示例。在Linux中,每一個文件都與一個目錄入口信息(dentry結構體)、元數據信息(存放在inode中)和實際的數據(存放在數據塊中)關聯。每次操做一個文件的時候,文件路徑中的組件會被解析,相應的dentry會被獲取。爲了加速將來的操做,dentry結構體被緩存在稱爲dcache的數據結構中。任什麼時候候,對dcache進行查找的數量都遠多於dcache的更新操做,所以,對dcache的訪問適宜用RCU原語進行保護。
2.5.4 調試
因爲難於重現,併發相關的問題一般很是難調試。在編譯和測試代碼的時候使能SMP(CONFIG_SMP)和搶佔(CONFIG_PREEMPT)是一種很好的理念,即使你的產品將運行在單CPU、禁止搶佔的狀況下。在Kernel hacking下有一個稱爲Spinlock and rw-lock debugging的配置選項(CONFIG_DEBUG_SPINLOCK),它能幫助你找到一些常見的自旋鎖錯誤。Lockmeter(http://oss.sgi. com/projects/lockmeter/)等工具可用於收集鎖相關的統計信息。
在訪問共享資源以前忘記加鎖就會出現常見的併發問題。這會致使一些不一樣的執行單元雜亂地「競爭」。這種問題(被稱做「競態」)可能會致使一些其餘的行爲。
在某些代碼路徑裏忘記了釋放鎖也會出現併發問題,這會致使死鎖。爲了理解這個問題,讓咱們分析以下代碼:
spin_lock(&mylock); /* Acquire lock */
/* ... Critical Section ... */
if (error) { /* This error condition occurs rarely */
return -EIO; /* Forgot to release the lock! */
}
spin_unlock(&mylock); /* Release lock */
if (error)語句成立的話,任何要獲取mylock的線程都會死鎖,內核也可能所以而凍結。
若是在寫完代碼的數月或數年之後首次出現了問題,回過頭來調試它將變得更爲棘手。(在21.3.3節有一個相關的調試例子。)所以,爲了不遭遇這種不快,在設計軟件架構的時候,就應該考慮併發邏輯。
2.6 proc文件系統
proc文件系統(procfs)是一種虛擬的文件系統,它建立內核內部的視窗。瀏覽procfs時看到的數據是在內核運行過程當中產生的。procfs中的文件可被用於配置內核參數、查看內核結構體、從設備驅動程序中收集統計信息或者獲取通用的系統信息。
procfs是一種虛擬的文件系統,這意味着駐留於procfs中的文件並不與物理存儲設備如硬盤等關聯。相反,這些文件中的數據由內核中相應的入口點按需動態建立。所以,procfs中的文件大小都顯示爲0。procfs一般在啓動過程當中掛載在/proc目錄,經過運行mount命令能夠看出這一點。
爲了瞭解procfs的能力,請查看/proc/cpuinfo、/proc/meminfo、/proc/interrupts、/proc/tty/driver /serial、/proc/bus/usb/devices和/proc/stat的內容。經過寫/proc/sys/目錄中的文件能夠在運行時修改某些內核參數。例如,經過向/proc/sys/kernel/printk文件回送一個新的值,能夠改變內核printk日誌的級別。許多實用程序(如ps)和系統性能監視工具(如sysstat)就是經過駐留於/proc中的文件來獲取信息的。
2.6內核引入的seq文件簡化了大的procfs操做。附錄C對此進行了描述。
2.7 內存分配
一些設備驅動程序必須意識到內存區的存在,另外,許多驅動程序須要內存分配函數的服務。本節咱們將簡要地討論這兩點。
內核會以分頁形式組織物理內存,而頁大小則取決於具體的體系架構。在基於x86的機器上,其大小爲4096B。物理內存中的每一頁都有一個與之對應的struct page(定義在include/linux/ mm_types.h文件中):
在32位x86系統上,默認的內核配置會將4 GB的地址空間分紅給用戶空間的3 GB的虛擬內存空間和給內核空間的1 GB的空間(如圖2-5所示)。這致使內核能處理的處理內存有1 GB的限制。現實狀況是,限制爲896 MB,由於地址空間的128 MB已經被內核數據結構佔據。經過改變3 GB/1 GB的分割線,能夠放寬這個限制,可是因爲減小了用戶進程虛擬地址空間的大小,在內存密集型的應用程序中可能會出現一些問題。
![](http://static.javashuo.com/static/loading.gif)
圖2-5 32位PC系統上默認的地址空間分佈
內核中用於映射低於896 MB物理內存的地址與物理地址之間存在線性偏移;這種內核地址被稱做邏輯地址。在支持「高端內存」的狀況下,在經過特定的方式映射這些區域產生對應的虛擬地址後,內核將能訪問超過896 MB的內存。全部的邏輯地址都是內核虛擬地址,而全部的虛擬地址並不是必定是邏輯地址。
所以,存在以下的內存區。
(1) ZONE_DMA(小於16 MB),該區用於直接內存訪問(DMA)。因爲傳統的ISA設備有24條地址線,只能訪問開始的16 MB,所以,內核將該區獻給了這些設備。
(2) ZONE_NORMAL(16~896 MB),常規地址區域,也被稱做低端內存。用於低端內存頁的struct page結構中的「虛擬」字段包含了對應的邏輯地址。
(3) ZONE_HIGH(大於896 MB),僅僅在經過kmap()映射頁爲虛擬地址後才能訪問。(經過kunmap()可去除映射。)相應的內核地址爲虛擬地址而非邏輯地址。若是相應的頁未被映射,用於高端內存頁的struct page結構體的「虛擬」字段將指向NULL。
kmalloc()是一個用於從ZONE_NORMAL區域返回連續內存的內存分配函數,其原型以下:
void *kmalloc(int count, int flags);
count是要分配的字節數,flags是一個模式說明符。支持的全部標誌列在include/linux./gfp.h文件中(gfp是get free page的縮寫),以下爲經常使用標誌。
(1) GFP_KERNEL,被進程上下文用來分配內存。若是指定了該標誌,kmalloc()將被容許睡眠,以等待其餘頁被釋放。
(2) GFP_ATOMIC,被中斷上下文用來獲取內存。在這種模式下,kmalloc()不容許進行睡眠等待,以得到空閒頁,所以GFP_ATOMIC分配成功的可能性比用GFP_KERNEL低。
因爲kmalloc()返回的內存保留了之前的內容,將它暴露給用戶空間可到會致使安全問題,所以咱們可使用kzalloc()得到被填充爲0的內存。
若是須要分配大的內存緩衝區,並且也不要求內存在物理上有聯繫,能夠用vmalloc()代替kmalloc():
void *vmalloc(unsigned long count);
count是要請求分配的內存大小。該函數返回內核虛擬地址。
vmalloc()須要比kmalloc()更大的分配空間,可是它更慢,並且不能從中斷上下文調用。另外,不能用vmalloc()返回的物理上不連續的內存執行DMA。在設備打開時,高性能的網絡驅動程序一般會使用vmalloc()來分配較大的描述符環行緩衝區。
內核還提供了一些更復雜的內存分配技術,包括後備緩衝區(look aside buffer)、slab和mempool;這些概念超出了本章的討論範圍,再也不細述。
2.8 查看源代碼
內存啓動始於執行arch/x86/boot/目錄中的實模式彙編代碼。查看arch/x86/kernel/setup_32.c文件能夠看出保護模式的內核怎樣獲取實模式內核收集的信息。
第一條信息來自於init/main.c中的代碼,深刻挖掘init/calibrate.c能夠對BogoMIPS校準理解得更清楚,而include/asm-your-arch/bugs.h則包含體系架構相關的檢查。
內核中的時間服務由駐留於arch/your-arch/kernel/中的體系架構相關的部分和實現於kernel/timer.c中的通用部分組成。從include/linux/time*.h頭文件中能夠獲取相關的定義。
jiffies定義於linux/jiffies.h文件中。HZ的值與處理器相關,能夠從include/asm-your-arch/ param.h找到。
內存管理源代碼存放在頂層mm/目錄中。
表2-1給出了本章中主要的數據結構以及其在源代碼樹中定義的位置。表2-2則列出了本章中主要內核編程接口及其定義的位置。
表2-1 數據結構小結
表2-2 內核編程接口小結