Linux 核心研究室

Linux 2.4 核心內部: 開機

1.1 建置 Linux 核心執行檔

本節說明在編譯 Linux 核心及取出的每一個步驟。

在此強調,我們只考慮建立 Linux/x86 平台的核心。

當使用者輸入 'make zImage' 或者 'make bzImage' 之後, 會在下面的路徑中找到可開機的核心檔案 (kernel image)。

arch/i386/boot/zImage 或者 arch/i386/boot/bzImage

下面說明如何建造核心檔案:

  1. C 語言或組合語言的原始程式檔案經過編譯/組譯後變成 ELF 可重定位的目的碼檔 (.o), 還有一些則依邏輯用 ar(1) 歸類為紀錄碼檔 (archives) (.a)。
  2. 使用 ld(1), 將上面的 .o 檔和 .a 檔連結成 vmlinux。 這是一個靜態連結檔,尚未被改成 ELF 32-bit LSB 80386 可執行檔。
  3. System.map 產生 nm vmlinux,去掉不恰當及不合宜的符號 (symbols)。
  4. 跳至 arch/i386/boot 目錄。
  5. 啟動磁區 (Bootsector) 組合語言程式 bootsect.S-D__BIG_KERNEL__ 來當要或不要弄成 Big Kernel 的預備動作, 依賴目的檔是 bzImage 或者 zImage,決定產生 bbootsect.s 或是 bootsect.s。 (譯註: 由於核心都經過壓縮編碼,當欲編譯的核心太大時,用 zip 演算法的 zImage 無法處理。此時需要用 Big Kernel 模式,用 bzip2 演算法來做更有效的壓縮。)
  6. bbootsect.s 是組合組言程式,轉換成 '生二進位' (raw binary) 格式的 bbootsect 檔。 而 bootsect.S 組合語言程式轉成 bootsect 供 zImage 使用。
  7. 啟始程式碼 (Setup Code) setup.S (setup.S 包括 video.S) 是 bzImage 格式的 bsetup.s 檔或是 zImage 格式的 setup.s 檔的前處理程式。 和啟動磁區相同地,用 D__BIG_KERNEL__ 表示編譯成 bzImage 程式。 傳回的檔案 bsetup 也是經過生二進位轉換所產生的。
  8. 跳到目錄 arch/i386/boot/compressed 並轉換 /usr/src/linux/vmlinux 到 $tmppiggy (臨時檔案名) 為生二進位檔格式,並刪除 .note.comment ELF 區段。
  9. gzip -9 < $tmppiggy > $tmppiggy.gz
  10. 連結 $tmppiggy.gz 到 ELF 重定位 (ld -r) piggy.o
  11. 編譯壓縮程式 head.Smisc.c (都在 arch/i386/boot/compressed 目錄中) 到 ELF 格式目的檔 head.omisc.o
  12. head.omisc.opiggy.o 連結到 bvmlinux (zImage 的話則連結到 vmlinux,但不要將它與 /usr/src/linux/vmlinux 搞混!)。 注意下面二個不同之處 - vmlinux 在編譯時的選項 -Ttext 指到 0x1000 的偏移值 (Offset);bvmlinux-Ttext 在 0x100000, 換言之 bzImage 壓縮放在高的載入點 (high-loaded)。
  13. bvmlinux 轉成生二進位檔 bvmlinux.out 刪掉 .note.comment 的 ELF 區段。
  14. 跳回 arch/i386/boot 目錄,並利用 tools/build.c 程式, 將 bbootsectbsetupcompressed/bvmlinux.out 利用 cat 連到 bzImage 中。 (若為 zImage 則去掉前面的 b 字母)。 這是為了將重要的變數,諸如 setup_sectsroot_dev 能放在啟動磁區中。
啟動磁區 (Bootsector) 的大小永遠是 512 位元組 (bytes)。 啟始區段 (setup) 必需在第 4 磁區後,同時大小不能超過 12K,公式為:

0x4000 bytes >= 512 + setup_sects * 512 + 執行 bootsector/setup 所要的堆疊 (stack) 空間。

我們稍後可以看到這種限制的由來。

bzImage 核心大小上限為 2.5M,同時 LILO 及 0xFFFF 處 (paragraphs) (0xFFFF0 = 1048560 bytes) 要放置啟動生程式 (booting raw image)。 例如: 磁片或是光碟 (El-Torito 模擬模式) 就會用到。

注意當 tools/build 在啟動磁區生效後,核心檔接在啟始區 (setup) 後面, 它並不會檢查前面所記的啟始區大小。 因此若是在 setup.S 程式後面加入大的 ".space" 的話,容易建出一個壞的核心。

1.2 開機: 概觀

開機的程序細節是特殊的結構,所以我們焦點放在 IBM PC/IA32 結構上。 由於設計上都是往前相容,因此仍用舊式的 PC 韌體的開機方式。 可以將它分成六個邏輯階段:

  1. BIOS 選擇開機的磁碟機。
  2. BIOS 載入該開機碟的啟動磁區 (Bootsector)。
  3. 啟動磁區載入啟始程式 (setup),並用來解開被壓縮過的核心檔。
  4. 解壓後的核心執行,並進入保護模式。
  5. 依組合語言寫的程式做低階初始化。
  6. 依 C 語言寫的程式做高階初始化。

1.3 開機: BIOS 自我測試程序

  1. 電源開啟,依系統時鐘產生並送出 #POWERGOOD 訊號到匯流排。
  2. 中央處理器執行 #RESET 程式。(此時位於 8086 真實模式中)
  3. 設定 %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000, %eip = 0x0000FFF0 (唯讀記 BIOS 自我檢查碼)。
  4. 執行所有的開機檢查 (POST checks) 動作,並關閉所有中斷。
  5. 中斷向量表 (IVT, Interrupt Vector Table) 初始化並將位址設為 0。
  6. BIOS 開機程式經由 int 0x19 中斷被喚起,包括依 %dl 暫存器獲知開機磁碟機代號。 之後載入第零軌 (track 0),第一磁區 (sector 1) 上的實體位址 0x7c00 (0x07C0:0000)。

1.4 開機: 啟動磁區和啟始程式

啟動磁區可選擇下列的不同方式來啟動 Linux 核心:

  • Linux 啟動磁區 (arch/i386/boot/bootsect.S)。
  • 在啟動磁區中的 LILO 程式 (或其它啟動載入程式)。
  • 不使用啟動磁區 (例如在 PC/MS DOS 中執行 loadlin 等)。

我們審視一下 Linux 啟動磁區 (bootsector) 的細節。 (譯註: 即 arch/i386/boot/bootsect.S 的組合語言程式)。 最初幾行依下面的片斷的數值作初始化。


29 SETUPSECS = 4                /* 啟始磁區 (setup-sectors) 的內定值 */
30 BOOTSEG   = 0x07C0           /* 開機磁區的初始位置 */
31 INITSEG   = DEF_INITSEG      /* 不尋常地,開機程式移到這來 */
32 SETUPSEG  = DEF_SETUPSEG     /* 開始啟始程式 */
33 SYSSEG    = DEF_SYSSEG       /* 系統載入到 0x10000 (65536) */
34 SYSSIZE   = DEF_SYSSIZE      /* 系統大小為 16 位元組的數值 */

(左方的數字即為 bootsect.S 檔案的行數)。 (譯註: 原著的所有程式和您看到的行數若有不同,可能是不同版本的關係。為忠於原著,譯者不改變行數。) 而數值 DEF_INITSEGDEF_SETUPSEGDEF_SYSSEGDEF_SYSSIZE,由 include/asm/boot.h 所定義:


/* 不要亂改這些值,除非你真的知道你在幹嘛 */
#define DEF_INITSEG     0x9000
#define DEF_SYSSEG      0x1000
#define DEF_SETUPSEG    0x9020
#define DEF_SYSSIZE     0x7F00

現在,讓我們好好看看 bootsect.S 程式:


    54          movw    $BOOTSEG, %ax
    55          movw    %ax, %ds
    56          movw    $INITSEG, %ax
    57          movw    %ax, %es
    58          movw    $256, %cx
    59          subw    %si, %si
    60          subw    %di, %di
    61          cld
    62          rep
    63          movsw
    64          ljmp    $INITSEG, $go
       
    65  # bde - 改變 0xff00 到 0x4000 使用除錯器到 0x6400 上 (bde)。
    66  # 我們不用在意當檢查記憶體的最頂處會怎樣。
    67  # 同時我們的 BIOS 也可規劃將 wini 磁碟設備表放在高位址記憶體
    68  # 代替向量表。舊的堆疊可能弄掉磁碟設備表。
    69  # 
       
    70  go:     movw    $0x4000-12, %di         # 0x4000 是個隨意的值大於或等於
    71                                          # 開機磁區長度加上啟始程式長度
    72                                          # 加上堆疊的空間;
    73                                          # 12 是磁碟參數大小。
    74          movw    %ax, %ds                # ax 及 es 已包含 INITSEG。
    75          movw    %ax, %ss
    76          movw    %di, %sp                # 將堆疊放到 INITSEG:0x4000-12。

第 54 至 63 行將位址 0x7C00 的開機磁區碼移到 0x90000 的位址上。 做法為:

  1. 將 %ds:%si 設為 $BOOTSEG:0 (0x7C0:0 = 0x7C00)
  2. 將 %es:%di 設為 $INITSEG:0 (0x9000:0 = 0x90000)
  3. 設定數字為 16bit words 在 %cx (256 words = 512 bytes = 1 sector)
  4. 清除 DF (direction) 旗標在 EFLAGS 變成自動遞增的位址 (cld)
  5. 繼續並複製 512 位元組 (rep movsw)

不使用 rep movsd 的原因其實是故意的 (注意 - .code16 標籤)。

第 64 行跳到標籤 go: 為最新複製開機磁區,即節段 (segment) 0x9000。 之後跟著的三行指令 (第 64-76 行) 準備在 $INITSEG:0x4000-0xC 的堆疊,即 %ss = $INITSEG (0x9000) 和 %sp = 0x3FF4 (0x4000-0xC)。 至於何處要限制啟始程式的大小,我們先前有提過了。 (參照建置 Linux 核心執行檔的部份)

第 77 至 103 行為修正磁碟參數表,為了允許第一個磁碟讀取多磁區 (multi-sector reads):


    77  # 許多 BIOS 內定的磁碟參數表不會認出
    78  # 讀取多磁區之後在內定磁片參數表指定最大的磁區數。
    79  # 這意思可能是在一些案例中不能超過七個磁區。
    80  # 
    81  #
    82  # 自從讀取單一磁區很慢,且超過這個問題,
    83  # 我們必須小心地在唯讀記憶體上建立新的參數表
    84  # (第一個磁碟)。我們將設定最大的磁區數
    85  # 能達到 36,也就是能計算到 2.88 MB 的 ED 格式。
    86  #
    87  # 高的無妨,低的有害。
    88  # 
    89  # 下面的節段 ds = es = ss = cs - INITSEG, fs = 0,
    90  # 沒有用到 gs 暫存器。
       
    91          movw    %cx, %fs                # 將 fs 設成 0
    92          movw    $0x78, %bx              # fs:bx 為參數表的位址
    93          pushw   %ds
    94          ldsw    %fs:(%bx), %si          # ds:si 為來源
    95          movb    $6, %cl                 # 複製 12 位元組
    96          pushw   %di                     # di = 0x4000-12.
    97          rep                             # 不需 cld -> 弄好 66 行
    98          movsw
    99          popw    %di
   100          popw    %ds
   101          movb    $36, 0x4(%di)           # 修正磁區計數
   102          movw    %di, %fs:(%bx)
   103          movw    %es, %fs:2(%bx)

使用 BIOS 的中斷 0x13 服務,第 0 個函式 (設定 FDC) 可重置軟碟機控制器, 同時啟始磁區 (setup sectors) 緊跟著啟動磁區 (bootsector) 被載入, 換句話說,在實體位址 0x90200 ($INITSEG:0x200) 再使用 BIOS 的 0x13 中斷服務, 函式 2 (讀取磁區)。 發生在第 107 到 124 行中間:


   107  load_setup:
   108          xorb    %ah, %ah                # 重置軟碟控制器
   109          xorb    %dl, %dl
   110          int     $0x13   
   111          xorw    %dx, %dx                # 第 0 磁碟, 第 0 磁頭
   112          movb    $0x02, %cl              # 第 2 磁區, 第 0 軌
   113          movw    $0x0200, %bx            # 位址為 512, 在 INITSEG
   114          movb    $0x02, %ah              # 函式 2, "讀取磁區"
   115          movb    setup_sects, %al        # (假設為磁頭 0,磁軌 0)
   116          int     $0x13                   # 讀居
   117          jnc     ok_load_setup           # 成功則跳到 ok_load_setup
       
   118          pushw   %ax                     # 失敗,印出錯誤代碼
   119          call    print_nl
   120          movw    %sp, %bp
   121          call    print_hex
   122          popw    %ax     
   123          jmp     load_setup
       
   124  ok_load_setup:

若因為某些原因而載入失敗 (磁片壞掉或是有人在開機時抽走磁片), 我們可顯示錯誤代碼並重試。 要是怎麼試都無法成功,只有一條路,就是重新開機。 (若有壞事發生,只會導致更惡化)。

若啟始程式載入 setup_sects 磁區成功則跳到 ok_load_setup: 標籤。

之後我們載入壓縮核心檔到實體位址 0x10000。 這是在低記憶體位址中韌體的資料節段 (0-64K)。 在核心載入後,跳到 $SETUPSEG:0 (arch/i386/boot/setup.S)。 只用到一次的資料 (例如 BIOS 的資料) 會被核心程式 (壓縮過的) 從位址 0x10000 移到 0x1000 的過程中覆蓋掉 (當然還是指實體位址)。 setup.S 在設定成保護模式並跳到 0x1000,為壓縮過的核心程式檔頭, 換言之就是 arch/386/boot/compressed/{head.S,misc.c}。 這時設定堆疊後呼叫 decompress_kernel() 將核心解壓至 0x100000 位址, 並跳到該處。

值得一提的是舊的啟動載入程式 (如舊版的 LILO) 只能載入最開始四個磁區的啟始程式, 這就是為何啟始程式有時會有卡住的情形形。 同時,啟始程式可處理不同的組合,包括不同版本的 zImage 或 bzImage,甚至是更複雜的情形。

讓我們檢查雜牌電腦的啟動磁區程式,允許載入大的核心的狀態時,即 "bzImage"。 啟始磁區 (setup sectors) 通常被載入到 0x90200 的位址上, 但是核心被載入到 64K 時就需要特殊的輔助函式來呼叫 BIOS 將資料從記憶體的低處移到高處。 這段輔助函式參考 bootsect.Sbootsect_kludge, 同時定義在程式 setup.Sbootsect_helper 節段。 程式 setup.Sbootsect_kludge 包括啟始節段 (segment) 及 bootsect_helper 碼的偏移 (offset) 值,故啟動磁區可使用 lcall 達成跳躍的動作 (節段內跳躍)。 而為何在程式 setup.S 的主要原因為 bootsect.S 沒有空間再放程式。 (嚴格而論這不太正確,bootsect.S 還有四個位元組的額外空間和至少一個位元組的空間, 但很顯然地,這是不夠的。) 這段常式呼叫 BIOS 服務的 0x15 中斷 (ax=0x8700) 移到高記憶體區並重設 %es 暫存器讓它永遠指到 0x10000 位址。 可以保證 bootsect.S 程式碼不會因為複製磁碟的資料而產生低位址記憶體不足 (out of low memory) 的情形。

1.5 使用 LILO 開機程式

有好幾種理由讓 Linux 開機磁區使用特別的開機程式 (LILO):

  1. 它有能力處理多種 Linux 核心或是多種作業系統。
  2. 它有能力傳遞命令列參數。(在單用啟動磁區與啟始程式經過加上 BCP 修補亦可做到)
  3. 它有能力載入較大的 bzImage 核心,可達 2.5M,甚於一般程式的 1M 大小。
舊版的 LILO 程式 (v17 或更早的版本) 無法載入 bzImage 的核心。 較新版本的程式使用和啟動磁區加啟始程式 (bootsect+setup) 呼叫 BIOS 從低位址移資料到高位址的技術相同。 一些人 (尤其是 Peter Anvin) 力主該將 zImage 支援刪除。 另些人 (諸如 Alan Cox) 抑制這種想法,因有一些有問題的 BIOS 無法使用 bzImage,但是 zImage 可以跑得很順。

(譯註: 關於 H. Peter Anvin 與 Alan Cox 等人的爭論,可以參考這個網址 http://kt.linuxcare.com/kernel-traffic/kt20000807_79.epl#16。)

最後 LILO 跳到 setup.S 就開始正常的處理了。

1.6 高階初始化

藉由高階初始化,我們可以思考,不是任何事都和啟動 (bootstrap) 直接相關, 即使用組合語言的程式碼寫的程式,如 arch/i386/kernel/head.S 是用來解壓縮核心的。 跟著下面的步驟執行:

  1. 初始化節段值 (%ds = %es = %fs = %gs = __KERNEL_DS = 0x18) 。
  2. 初始化分頁表 (page tables)。
  3. 設定 %cr0 的 PG 位元讓分頁能動。
  4. 零清除 (Zero-clean) BSS (在多 CPU 下,即 SMP,由第一個 CPU 來做)。
  5. 複製開機參數的頭 2k (即核心的參數指令)。
  6. 用 EFLAGS 檢查 CPU 的型式,若可能的話,一併檢查 CPU ID...等等。
  7. 第一個 CPU 常式為 start_kernel(),其它執行的常式見 arch/i386/kernel/smpboot.c:initialize_secondary() ,若 ready=1 則重載入 esp/eip 而不重回原處執行。

程式 init/main.c:start_kernel() 是用 C 開發的,跟著下面的步驟:

  1. 處理全域的核心鎖 (只要一顆 CPU 處理初始動作)。
  2. 執行特別環境設定 (記憶體分析、重複製命令列參數....等等)。
  3. 顯示 Linux 核心字串 (banner),包括版本、編譯程式....等等,到核心的訊息緩衝區中。 相關的顯示定義在 init/version.c 的 linux_banner 變數中,並會用這個指令來顯示相同的字串 cat /proc/version
  4. Traps 初始化。
  5. 中斷向量 (IRQ) 初始化。
  6. 排程資料初始化。
  7. 初始化階段保存資料。
  8. 軟體中斷向量子系統初始化。
  9. 開機提命令列參數選項語法分析。
  10. 主控台初始化。
  11. 若編譯核心時加入模組 (module) 功能,則初始化動態載入模組能力。
  12. 若支援 "profile=" 命令列,則初始化特徵緩衝區 (profiling buffers)。
  13. kmem_cache_init(), 初始化需要厚分派 (slab allocator) 的記憶體。
  14. 允許中斷。
  15. 計算中央處理器的 BogoMips 值。
  16. 呼叫 mem_init() 計算 max_mapnrtotalram_pageshigh_memory 並輸出 "Memory: ..." 字串。
  17. kmem_cache_sizes_init() 結束厚分派記憶體的初始化。
  18. 程式用到的資料結構初始化。
  19. fork_init(),建立 uid_cache,初始化 max_threads, 為了 init_task 變成 max_threads/2 以所有可用計憶體為基準並規劃 RLIMIT_NPROC
  20. 建立不同的厚快取 (slab caches),諸如 VFS、VM、緩衝區快取....等等。
  21. 若 System V 行程通訊 (IPC) 支援亦編譯進來,則初始化行程通訊子系統。 順便提的 System V 信號處理機制,包括掛入 (mount) 核心內部的信號處理檔案系統 (shmfs filesystem)。
  22. 若編譯核心有加上使用者空間配額 (quota),則為它建立及初始化特別的厚快取 (slab cache)。
  23. 執行特別的檢測錯誤碼 (check for bugs),若可能的話,讓 processor/bus/etc 等錯誤排除。 對照不同的結構表現,包括 "ia64 沒有錯誤" 及 "ia32 有一點錯誤", 好的例子諸如 "f00f bug" 只能檢查在 686 以上等級中央處理器的問題。
  24. 設定旗標到排程器並喚起 "下一刻" (next opportunity) 來建立核心執行緒 init()。 當執行到 execute_command 若遇到 "init=" 的開機參數,則試著依序執行 /sbin/init/etc/init/bin/init/bin/sh。 若均失敗,則建議一定要設定 "init=" 參數。
  25. 跳到空閒迴圈,這是一個空置的執行緒,且行程代碼 pid=0。

重要的部份在這兒再提一次,就是 init() 核心執行緒呼叫 do_basic_setup(),回傳呼叫 do_initcalls() 這項目經過函式註冊,意思為 __initcallmodule_init() 以巨集喚起它們。 這些函式不依賴其它函式,也不需依賴手動調整 Makefile 來做。 也就是說,mailfiles 依靠樹狀目錄位置及結構,會該喚起及初始化的函式順序改變。 有時這是很重要的,因為當你想像二個子系統 A 和 B,而 B 要靠 A 初始化一些東西才能做。 此時,若 A 編譯成靜態的程式,而 B 編譯成模組,則 B 的進入點要等 A 準備好所有環境後才被喚出來。 但若 A 也編譯成模組,B 也一定要編譯成模組才不會出問題。 但若是 A 和 B 都是靜態的方式存在核心中呢? 順序在於核心 .initcall.init 的 ELF 區段中的進入點偏移值。 Rogier Wolff 提出一個優先權 (priority) 制度的方法, 讓模組可以連結到它應該連結處,但到目前為止沒有一個可以讓核心接受的漂亮方法被實作出來。 因此,確定你正確的連結順序。 若是照上面的例子,A 和 B 編譯成靜態的方式可以運作的很好, 假設都在相同 Makefile 清單上,那就永遠都可以運作。 若不能運作,改變在清單目的檔的順序。

另一件值得一書的事,Linux 有能力執行 "選擇性的初始程式" (alternative init program), 意即傳送開機時的參數列 "init="。 這在偶然覆蓋 /sbin/init 的意外狀況,或是在初始化劇本程式除錯及執行 /etc/inittab 開頭時, 都是很有用的方法。

1.7 x86 上的多處理器開機

在多中央處理器 (SMP) 的場合,開機處理器 (BP, Boot Processor) 處理啟動磁區的、啟始程式等連續動作, 一直跑到 start_kernel() 程式, 之後到 smp_init(),尤其是 src/i386/kernel/smpboot.c:smp_boot_cpus()。 這段 smp_boot_cpus() 各中央處理器跑到迴圈底處 (直到 NR_CPUS) ,此時並呼叫 do_boot_cpu()do_boot_cpu() 引發 (如 fork_by_hand) 目的處理器閒置的作業,並寫入眾所皆知的地方,也就是 Intel MP 規格書 (0x467/0x469) 中 EIP 的蹦跳碼 (trampoline code) 的 trampoline.S 組合語言程式。 之後目的處理器產生 STARTUP IPI,讓應用處理器 (AP, Application Processor) 執行 trampoline.S 程式。

用來啟動的處理器在低記憶體處為每一顆處理器建立一份蹦跳碼的複本。 應用處理器 (AP) 程式寫一個魔術數字 (magic number) 在自我的程式碼中, 用來讓開機處理器 (BP) 可驗證應用處理器執行蹦跳碼。 依 Intel MP 的規格,必需讓蹦跳碼放在低的記憶體。

蹦跳碼簡單地將 %bx 暫存器設成 1,進入 386 保護模式, 並跳到 arch/i386/kernel/head.S 的進入點 startup_32。

此時,應用處理器開始執行 head.S,並查覺它不是開機處理器, 跳過清除 BSS 的程式碼後進入 initialize_secondary() 子程式中, 讓該處理器進入閒置作業 (idle task),最後重呼叫 init_tasks[cpu] 開機處理器執行 do_boot_cpu(cpu) 初始化完成。

注意 init_task 可以共享,但每個閒置執行緒必須要有自己的 TSS。 這也就是為何 init_tss[NR_CPUS] 是陣列。

1.8 釋放初始階段的資料和程式碼

當作業系統自己初始化後,一些程式碼及資料結構就不會再用到了。 多數的作業系統 (BSD, FreeBSD....等等) 不會去除不要的資訊, 也因而浪費了珍貴的實體核心記憶體。 一種論點 (見 McKusick 的 4.4BSD 書籍) 說這些相關的程式碼遍及不同的子系統, 所以不可能釋放。 Linux,當然不適用此類的辯解,因為在 Linux 上, 只要原則上有些可能,則通常已經實作出來,或者有人正在開發。

所以,就像在前面敘述地,Linux 核心只能編譯成 ELF 二進位檔, 現在我們將找出理由 (或理由之一)。 理由就是 Linux 提供二個巨集將初始化的程式碼/資料拋棄。

  • __init - 處理初始化的程式碼
  • __initdata - 處理初始化的資料

有人歸功於 gcc 規格的特性 (或稱 gcc 魔法),定義在 include/linux/init.h:


#ifndef MODULE
#define __init        __attribute__ ((__section__ (".text.init")))
#define __initdata    __attribute__ ((__section__ (".data.init")))
#else
#define __init
#define __initdata
#endif

上述的意思,若這些程式靜態編譯進核心 (例如沒有定義模組) 則會在一個特別的 ELF 區段 .text.init, 會公開到連結表 arch/i386/vmlinux.lds 上。 若用別的方法 (例如是模組型態) 則巨集不做任何事。

在整個開機期間初始化核心執行緒 (函式 init/main.c:init()) 呼叫結構特殊的函式 free_initmem(), 用來釋放從 __init_begin__init_end 之間所有的記憶體頁。

在典型的系統 (我的工作站),傳回釋放的記憶體大約 260K 左右。

這個函式放在 .initcall.init ,經過 module_init() 註冊後, 也是一個靜態的釋放記憶體的例子。 目前 Linux 的走向,當設計一個子系統 (不一定是模組) 時, 從早期到未來都會提供一個初始/離開的進入點, 子系統若有需要就可以考慮用模組的方式。 例如管道檔案系統 (pipefs),參見 fs/pipe.c。 即便是弄的子系統永不會變成模組,如 bdflush (見 fs/buffer.c), 在較不重要的場合,使用 module_init() 仍是最漂亮的方法,而非使用巨集初始化。

有二個以上的巨集相仿,即 __exit__exitdata, 但有些和連接有關的模組支援,稍後會提到。

1.9 核心在交談模式列的處理

讓我們重新看看核心在開機過程,對傳入的參數列做哪些事:

  1. LILO (或是 BCP) 接受提示模式 (commandline) 使用 BIOS 鍵盤中斷, 並將它存放在實體記憶體中,同時顯示此時允許使用交談模式。
  2. arch/i386/kernel/head.S 試著複製最初的 2k 到零頁 (zeropage)。 要說明的是在目前 LILO 的版本 (21) 僅允許交談列的前 79 位元組。 這是 LILO 否認的錯誤 (bug) (當大的 EBDA 支援致能時), 當然 Werner 承諾會盡早修正。(譯按: Werner Almesberger 為 LILO 的作者。) 若實在需要比 79 個位元組還在的命令列時,您可以選擇使用 BCP 或是花工夫修改 arch/i386/kernel/setup.c:parse_mem_cmdline() 函式的程式碼。
  3. arch/i386/kernel/setup.c:parse_mem_cmdline() (由 setup_arch() 呼叫,而 setup_arch 是由 start_kernel() 呼叫) 從零頁複製 256 個位元組到 saved_command_line,經由 /proc/cmdline 顯示出來。 這跟行程常式的 "mem=" 選項一樣,可由 VM 參數對記憶體做適當地調整。
  4. 我們回到命令列的 parse_options() (由 start_kernel() 呼叫), 它處理一些 "核心內" 參數 (諸如 "init=" 及初始化的環境變數/參數), 以及由 checksetup() 所傳來的每個字。
  5. checksetup() 經過了 ELF 區段的 .setup.init 程式碼並喚起各個函式,若是相符的字就通過。 要注意使用註冊過的函式經由 __setup() 傳回的值 0, 它有可能也是經由 "variable=value" 的多個無效 "value" 之一,而有效的值是其它的。 Jeff Garzik 註解為:「被修理的專家。 :)」 為什麼?因為這很清楚地是載入順序的特性, 例如,核心連結到一種順序讓函式 A 在函式 B 前被喚起, 其它為相反的順序,傳回值依照這種順序。

所以說,我們該如何寫在開機提示列的處理程式? 我們可用 __setup(),巨集定義在 include/linux/init.h:



/*
 * 用在啟動時的核心命令列
 */
struct kernel_param {
        const char *str;
        int (*setup_func)(char *);
};

extern struct kernel_param __setup_start, __setup_end;

#ifndef MODULE
#define __setup(str, fn) \
   static char __setup_str_##fn[] __initdata = str; \
   static struct kernel_param __setup_##fn __initsetup = \
   { __setup_str_##fn, fn }

#else
#define __setup(str,func) /* 沒做什麼 */
endif

因此,你可以在你的程式中用這樣的方式 (從實際的周邊取出的程式,BusLogic HBA drivers/scsi/BusLogic.c):


static int __init
BusLogic_Setup(char *str)
{
        int ints[3];

        (void)get_options(str, ARRAY_SIZE(ints), ints);

        if (ints[0] != 0) {
                BusLogic_Error("BusLogic: Obsolete Command Line Entry "
                                "Format Ignored\n", NULL);
                return 0;
        }
        if (str == NULL || *str == '\0')
                return 0;
        return BusLogic_ParseDriverOptions(str);
}

__setup("BusLogic=", BusLogic_Setup);

注意 __setup() 在模組中什麼也沒做, 所以程式碼想處理開機命令列並能用模組或靜態連結時,要在模組初始程序中, 處理語法剖析的功能。 這也意謂著在編譯成模組卻不是靜態連結時,有可能寫個處理參數的程序碼,反之亦然。


Copyright (C) 2001-2026, Wilson Peng
All rights reserved.