| 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) { |