天天看點

Linux驅動開發必看

詳解神秘核心

  <b>2.1 啟動過程</b>

   圖2-1顯示了基于x86計算機Linux系統的啟動順序。第一步是BIOS從啟動裝置中導入主引導記錄(MBR),接下來MBR中的代碼檢視分區表并 從活動分區讀取GRUB、LILO或SYSLINUX等引導裝入程式,之後引導裝入程式會加載壓縮後的核心映像并将控制權傳遞給它。核心取得控制權後,會 将自身解壓縮并投入運轉。

   核心初始化的第一步是執行實模式下的彙編代碼,之後執行保護模式下init/main.c檔案(上一章修改的源檔案)中的start_kernel() 函數。start_kernel()函數首先會初始化CPU子系統,之後讓記憶體和程序管理系統就位,接下來啟動外部總線和I/O裝置,最後一步是激活初始 化(init)程式,它是所有Linux程序的父程序。初始化程序執行啟動必要的核心服務的使用者空間腳本,并且最終派生控制台終端程式以及顯示登入 (login)提示。

Linux驅動開發必看

圖2-1 基于x86硬體上的Linux的啟動過程

  <b>2.1.1 BIOS-provided physical RAM map</b>

  核心會解析從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提供的記憶體映射問題進行更深入的講解。

Linux驅動開發必看

圖2-2 核心啟動資訊

  <b>2.1.2 758MB LOWMEM available</b>

  896 MB以内的正常的可被尋址的記憶體區域被稱作低端記憶體。記憶體配置設定函數kmalloc()就是從該區域配置設定記憶體的。高于896 MB的記憶體區域被稱為高端記憶體,隻有在采用特殊的方式進行映射後才能被通路。

  在啟動過程中,核心會計算并顯示這些記憶體區内總的頁數。

  <b>2.1.3 Kernel command line: ro root=/dev/hda1</b>

   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(&amp;str, &amp;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 */

  argv_init[++args] = "3";

} else {

  argv_init[++args] = "2";

  請重新編譯核心并嘗試運作新的修改。

  <b>2.1.4 Calibrating delay...1197.46 BogoMIPS (lpj=2394935)</b>

   為了了解延遲—循環校準代碼,讓我們看一下定義于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 &gt;&gt;= 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-- &amp;&amp; (loopbit &gt;&gt;= 1)) {

  loops_per_jiffy |= loopbit;

ticks = jiffies;

  if (jiffies != ticks)        /* longer than 1 tick */

    loops_per_jiffy &amp;= ~loopbit;

BogoMIPS = loops_per_jiffy * 1秒内的jiffy數*延遲循環消耗的指令數(以百萬為機關)

= (2394935 * HZ * 2) / (1000000)

= (2394935 * 250 * 2) / (1000000)

= 1197.46(與啟動過程列印資訊中的值一緻)

  在2.4節将更深入闡述jiffy、HZ和loops_per_jiffy。

 <b> 2.1.5 Checking HLT instruction</b>

  由于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()時,會列印上述資訊。

 <b> 2.1.6 NET: Registered protocol family 2</b>

  核心中經常使能的另一個協定系列是AF_Unix或Unix-domain套接字。X Windows等程式使用它們在同一個系統上進行程序間通信。

  <b>2.1.7 Freeing initrd memory: 387k freed</b>

   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有時候也可被用作嵌入式裝置上實際的根檔案系統。

  <b>2.1.8 io scheduler anticipatory registered (default)</b>

   I/O排程器的主要目标是通過減少磁盤的定位次數來增加系統的吞吐率。在磁盤定位過程中,磁頭需要從目前的位置移動到感興趣的目标位置,這會帶來一定的 延遲。2.6核心提供了4種不同的I/O排程器:Deadline、Anticipatory、Complete Fair Queuing以及NOOP。從上述核心列印資訊可以看出,本例将Anticipatory 設定為了預設的I/O排程器。

  <b>2.1.9 Setting up standard PCI resources</b>

Linux驅動開發必看

圖2-3 在啟動過程中初始化總線和外圍控制器

  本書會以單獨的章節讨論大部分上述驅動程式子系統,請注意如果驅動程式以子產品的形式被動态連結到核心,其中的一些消息也許隻有在核心啟動後才會被顯示。

  <b>2.1.10 EXT3-fs: mounted filesystem</b>

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

<b>  2.1.11 INIT: version 2.85 booting</b>

  所有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發起虛拟控制台終端,你現在就可以登入了。

<b>  2.2 核心模式和使用者模式</b>

  核心模式的代碼可以無限制地通路所有處理器指令集以及全部記憶體和I/O空間。如果使用者模式的程序要享有此特權,它必須通過系統調用向裝置驅動程式或其他核心模式的代碼送出請求。另外,使用者模式的代碼允許發生缺頁,而核心模式的代碼則不允許。

  在2.4和更早的核心中,僅僅使用者模式的程序可以被上下文切換出局,由其他程序搶占。除非發生以下兩種情況,否則核心模式代碼可以一直獨占CPU:

  (1) 它自願放棄CPU;

  (2) 發生中斷或異常。

  2.6核心引入了核心搶占,大多數核心模式的代碼也可以被搶占。

 <b> 2.3 程序上下文和中斷上下文</b>

   核心可以處于兩種上下文:程序上下文和中斷上下文。在系統調用之後,使用者應用程式進入核心空間,此後核心空間針對使用者空間相應程序的代表就運作于程序上 下文。異步發生的中斷會引發中斷處理程式被調用,中斷處理程式就運作于中斷上下文。中斷上下文和程序上下文不可能同時發生。

  運作于程序上下文的核心代碼是可搶占的,但程序上下文則會一直運作至結束,不會被搶占。是以,核心會限制中斷上下文的工作,不允許其執行如下操作:

  (1) 進入睡眠狀态或主動放棄CPU;

  (2) 占用互斥體;

  (3) 執行耗時的任務;

  (4) 通路使用者空間虛拟記憶體。

  本書4.2節會對中斷上下文進行更深入的讨論。

 <b> 2.4 核心定時器</b>

   核心中許多部分的工作都高度依賴于時間資訊。Linux核心利用硬體提供的不同的定時器以支援忙等待或睡眠等待等時間相關的服務。忙等待時,CPU會不 斷運轉。但是睡眠等待時,程序将放棄CPU。是以,隻有在後者不可行的情況下,才考慮使用前者。核心也提供了某些便利,可以在特定的時間之後排程某函數運 行。

  我們首先來讨論一些重要的核心定時器變量(jiffies、HZ和xtime)的含義。接下來,我們會使用Pentium時間戳計數器(TSC)測量基于Pentium的系統的運作次數。之後,我們也分析一下Linux怎麼使用實時鐘(RTC)。

 <b> 2.4.1 HZ和Jiffies</b>

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-&gt;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-&gt;rescheduled) {

  ehci_info(ehci, "ep%ds-iso rescheduled " "%lu times in %lu

            seconds\n", stream-&gt;bEndpointAddress, is_in? "in":

            "out", stream-&gt;rescheduled,

            ((jiffies – stream-&gt;start)/HZ));

  上述調試語句計算出USB端點流(見第11章)被重新排程stream-&gt;rescheduled次所耗費的秒數。jiffies-stream-&gt;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()作為執行個體來學習。

 <b> 2.4.2 長延時</b>

  在核心中,以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&gt;

struct timer_list my_timer;

init_timer(&amp;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(&amp;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(&amp;my_timer);

  my_timer.expire   = jiffies + n*HZ;

  my_timer.data     = func_parameter;

  my_timer.function = timer_func;

  add_timer(&amp;my_timer);

   你可以使用mod_timer()修改my_timer的到期時間,使用del_timer()取消定時器,或使用timer_pending()以查 看my_timer目前是否處于等待狀态。檢視kernel/timer.c源代碼,會發現schedule_timeout()内部就使用了這些 API。

  clock_settime()和clock_gettime()等使用者空間函數可用于獲得核心定時器服務。使用者應用程式可以使用setitimer()和getitimer()來控制一個報警信号在特定的逾時後發生。

 <b> 2.4.3 短延時</b>

  在核心中,小于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 &gt; 0);

  <b>2.4.4 Pentium時間戳計數器</b>

   時間戳計數器(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實作這一功能。

<b>  2.4.5 實時鐘</b>

   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&gt;

static struct timeval curr_time;

do_gettimeofday(&amp;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以微秒級精度執行周期性工作的應用程式示例。

 <b> 2.5 核心中的并發</b>

  随着多核筆記本電腦時代的到來,對稱多處理器(SMP)的使用不再被限于高科技使用者。SMP和核心搶占是多線程執行的兩種場景。多個線程能夠同時操作共享的核心資料結構,是以,對這些資料結構的通路必須被串行化。

  接下來,我們會讨論并發通路情況下保護共享核心資源的基本概念。我們以一個簡單的例子開始,并逐漸引入中斷、核心搶占和SMP等複雜概念。

  <b>2.5.1 自旋鎖和互斥體</b>

  通路共享資源的代碼區域稱作臨界區。自旋鎖(spinlock)和互斥體(mutex,mutual exclusion的縮寫)是保護核心臨界區的兩種基本機制。我們逐個分析。

  自旋鎖可以確定在同時隻有一個線程進入臨界區。其他想進入臨界區的線程必須不停地原地打轉,直到第1個線程釋放自旋鎖。注意:這裡所說的線程不是核心線程,而是執行的線程。

  下面的例子示範了自旋鎖的基本用法:

#include linux/spinlock.h&gt;

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(&amp;mylock);

/* ... Critical Section code ... */

spin_unlock(&amp;mylock); /* Release the lock */

   與自旋鎖不同的是,互斥體在進入一個被占用的臨界區之前不會原地打轉,而是使目前線程進入睡眠狀态。如果要等待的時間較長,互斥體比自旋鎖更合适,因為 自旋鎖會消耗CPU資源。在使用互斥體的場合,多于2次程序切換時間都可被認為是長時間,是以一個互斥體會引起本線程睡眠,而當其被喚醒時,它需要被切換 回來。

  是以,在很多情況下,決定使用自旋鎖還是互斥體相對來說很容易:

  (1) 如果臨界區需要睡眠,隻能使用互斥體,因為在獲得自旋鎖後進行排程、搶占以及在等待隊列上睡眠都是非法的;

  (2) 由于互斥體會在面臨競争的情況下将目前線程置于睡眠狀态,是以,在中斷處理函數中,隻能使用自旋鎖。(第4章将介紹更多的關于中斷上下文的限制。)

  下面的例子示範了互斥體使用的基本方法:

#include linux/mutex.h&gt;

/* Statically declare a mutex. To dynamically

   create a mutex, use mutex_init() */

static DEFINE_MUTEX(mymutex);

/* Acquire the mutex. This is inexpensive if there

* contention, mutex_lock() puts the calling thread to sleep.

mutex_lock(&amp;mymutex);

mutex_unlock(&amp;mymutex);      /* Release the mutex */

  為了論證并發保護的用法,我們首先從一個僅存在于程序上下文的臨界區開始,并以下面的順序逐漸增加複雜性:

  (1) 非搶占核心,單CPU情況下存在于程序上下文的臨界區;

  (2) 非搶占核心,單CPU情況下存在于程序和中斷上下文的臨界區;

  (3) 可搶占核心,單CPU情況下存在于程序和中斷上下文的臨界區;

  (4) 可搶占核心,SMP情況下存在于程序和中斷上下文的臨界區。

 <b> 舊的信号量接口</b>

  互斥體接口代替了舊的信号量接口(semaphore)。互斥體接口是從-rt樹演化而來的,在2.6.16核心中被融入主線核心。

  盡管如此,但是舊的信号量仍然在核心和驅動程式中廣泛使用。信号量接口的基本用法如下:

#include asm/semaphore.h&gt;  /* Architecture dependent header */

/* Statically declare a semaphore. To dynamically

   create a semaphore, use init_MUTEX() */

static DECLARE_MUTEX(mysem);

down(&amp;mysem);    /* Acquire the semaphore */

up(&amp;mysem);      /* Release the semaphore */

  1. 案例1:程序上下文,單CPU,非搶占核心

  這種情況最為簡單,不需要加鎖,是以不再贅述。

  2. 案例2:程序和中斷上下文,單CPU,非搶占核心

  在這種情況下,為了保護臨界區,僅僅需要禁止中斷。如圖2-4所示,假定程序上下文的執行單元A、B以及中斷上下文的執行單元C都企圖進入相同的臨界區。

Linux驅動開發必看

圖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變體:

  /* Save interrupt state.

   * Disable interrupts - this implicitly disables preemption */

  spin_lock_irqsave(&amp;mylock, flags);

  /* Restore interrupt state to what it was at Point A */

  spin_unlock_irqrestore(&amp;mylock, flags);

   我們不需要在最後顯示地恢複Point A的搶占狀态,因為核心自身會通過一個名叫搶占計數器的變量維護它。在搶占被禁止時(通過調用preempt_disable()),計數器值會增加;在 搶占被使能時(通過調用preempt_enable()),計數器值會減少。隻有在計數器值為0的時候,搶占才發揮作用。

  4. 案例4:程序和中斷上下文,SMP機器,搶占核心

  現在假設臨界區執行于SMP機器上,而且你的核心配置了CONFIG_SMP和CONFIG_PREEMPT。

  /*

    - 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

   */

    - Restore interrupt state and preemption to what it

      was at Point A for the local CPU

    - Release the lock

   在SMP系統上,擷取自旋鎖時,僅僅本CPU上的中斷被禁止。是以,一個程序上下文的執行單元(圖2-4中的執行單元A)在一個CPU上運作的同時,一 個中斷處理函數(圖2-4中的執行單元C)可能運作在另一個CPU上。非本CPU上的中斷處理函數必須自旋等待本CPU上的程序上下文代碼退出臨界區。中 斷上下文需要調用spin_lock()/spin_unlock():

/* ... Critical Section ... */

spin_unlock(&amp;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/。

  為了提高性能,核心也定義了一些針對特定環境的特定的鎖原語。使能适用于代碼執行場景的互斥機制将使代碼更高效。下面來看一下這些特定的互斥機制。

<b>  2.5.2 原子操作</b>

  原子操作用于執行輕量級的、僅執行一次的操作,例如修改計數器、有條件的增加值、設定位等。原子操作可以確定操作的串行化,不再需要鎖進行并發通路保護。原子操作的具體實作取決于體系架構。

  為了在釋放核心網絡緩沖區(稱為skbuff)之前檢查是否還有餘留的資料引用,定義于net/core/skbuff.c檔案中的skb_release_data()函數将進行如下操作:

1 if (!skb-&gt;cloned ||

2   /* Atomically decrement and check if the returned value is zero */

3     !atomic_sub_return(skb-&gt;nohdr ? (1 SKB_DATAREF_SHIFT) + 1 :

4                        1,&amp;skb_shinfo(skb)-&gt;dataref)) {

5   /* ... */

6   kfree(skb-&gt;head);

7 }

  當skb_release_data()執行的時候,另一個調用skbuff_clone()(也在net/core/skbuff.c檔案中定義)的執行單元也許在同步地增加資料引用計數值:

/* Atomically bump up the data reference count */

atomic_inc(&amp;(skb_shinfo(skb)-&gt;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(&amp;myrwlock);     /* Acquire reader lock */

/* ... Critical Region ... */

read_unlock(&amp;myrwlock);   /* Release lock */

  但是,如果一個寫線程進入了臨界區,那麼其他的讀和寫都不允許進入。寫鎖的用法如下:

write_lock(&amp;myrwlock);    /* Acquire writer lock */

write_unlock(&amp;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(&amp;xtime_lock);

    ret = jiffies_64;

  } while (read_seqretry(&amp;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原語進行保護。

  <b>2.5.4 調試</b>

  由于難于重制,并發相關的問 題通常非常難調試。在編譯和測試代碼的時候使能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(&amp;mylock);     /* Acquire lock */

if (error) {            /* This error condition occurs rarely */

  return -EIO; /* Forgot to release the lock! */

spin_unlock(&amp;mylock);   /* Release lock */

  if (error)語句成立的話,任何要擷取mylock的線程都會死鎖,核心也可能是以而當機。

 <b> 2.6 proc檔案系統</b>

  proc檔案系統(procfs)是一種虛拟的檔案系統,它建立核心内部的視窗。浏覽procfs時看到的資料是在核心運作過程中産生的。procfs中的檔案可被用于配置核心參數、檢視核心結構體、從裝置驅動程式中收集統計資訊或者擷取通用的系統資訊。

   為了了解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對此進行了描述。

<b>  2.7 記憶體配置設定</b>

  一些裝置驅動程式必須意識到記憶體區的存在,另外,許多驅動程式需要記憶體配置設定函數的服務。本節我們将簡要地讨論這兩點。

  核心會以分頁形式組織實體記憶體,而頁大小則取決于具體的體系架構。在基于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的分割線,可以放寬這個限制,但是由于減少了使用者程序虛拟位址空間的大小,在記憶體密集型的應用程式中可能會出現一些問題。

Linux驅動開發必看

圖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;這些概念超出了本章的讨論範圍,不再細述。

  <b>2.8 檢視源代碼</b>

  記憶體啟動始于執行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 資料結構小結

Linux驅動開發必看

  表2-2 核心程式設計接口小結

Linux驅動開發必看
Linux驅動開發必看
Linux驅動開發必看

繼續閱讀