轉載原文:http://blog.chinaunix.net/uid-12461657-id-3199784.html
Linux驅動開發必看
詳解神秘核心 轉載於http://www.it168.com 來源: Chinaunix 作者:Chinaunix
【IT168 技術文檔】在開始步入Linux裝置驅動程式的神秘世界之前,讓我們從驅動程式開發人員的角度看幾個核心構成要素,熟悉一些基本的核心概念。我們將學習核心定時器、同步機制以及記憶體配置方法。不過,我們還是得從頭開始這次探索之旅。因此,本章要先瀏覽一下核心發出的啟動資訊,然後再逐個講解一些有意思的點。
2.1 啟動過程
圖2-1顯示了基於x86電腦Linux系統的啟動順序。第一步是BIOS從啟動裝置中匯入主引導記錄(MBR),接下來MBR中的代碼查看分區表並 從使用中的磁碟分割讀取GRUB、LILO或SYSLINUX等引導裝入程式,之後引導裝入程式會載入壓縮後的核心映像並將控制權傳遞給它。核心取得控制權後,會 將自身解壓縮並投入運轉。
基於x86的處理器有兩種操作模式:實模式和保護模式。在實模式下,使用者僅可以使用1 MB記憶體,並且沒有任何保護。保護模式要複雜得多,使用者可以使用更多的進階功能(如分頁)。CPU必須中途將實模式切換為保護模式。但是,這種切換是單向的,即不能從保護模式再切換回實模式。
核心初始化的第一步是執行實模式下的彙編代碼,之後執行保護模式下init/main.c檔案(上一章修改的源檔案)中的start_kernel() 函數。start_kernel()函數首先會初始化CPU子系統,之後讓記憶體和進程管理系統就位,接下來啟動外部匯流排和I/O裝置,最後一步是啟用初始 化(init)程式,它是所有Linux進程的父進程。初始化進程執行啟動必要的核心服務的使用者空間指令碼,並且最終派生控制台終端程式以及顯示登入 (login)提示。
圖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)。
圖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;