| 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[]列表,唯一的不同在于它們是傳遞給內核的??梢栽谝龑аb入程序的配置文件中增加命令行參數,當然,也可以在運行過程中修改引導裝入程序的提示行[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的操作不具備原子性??梢詫rivers/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的限制?,F實情況是,限制為896 MB,因為地址空間的128 MB已經被內核數據結構占據。通過改變3 GB/1 GB的分割線,可以放寬這個限制,但是由于減少了用戶進程虛擬地址空間的大小,在內存密集型的應用程序中可能會出現一些問題。 圖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 內核編程接口小結 |