Chapter 18. 進階主題

En-Ran Zhou
18.1. 如何能學習更多有關 FreeBSD 內部的東西?
18.2. 如何能為 FreeBSD 出一份力?
18.3. SNAP 和 RELEASE 是什麼?
18.4. 要怎麼作出自己的 release?
18.5. 為什麼 make world 把原來裝的 binary 檔 都換掉了?
18.6. 在系統開機時,出現 "(bus speed defaulted)"
18.7. 在網路頻寬有限的情況下,我也可以跟上 current 的發展嗎?
18.8. 是怎麼把發行版本中的檔案切成一個個 240k 的小檔案的?
18.9. 我在 kernel 中加了新功能,我要把它寄給誰?
18.10. ISA 的隨插即用卡是如何偵測及初始化的?
18.11. 我為某設備寫了驅動程式,能不能給它一個 major number?
18.12. 關於更動目錄放置的原則?
18.13. 如何在 kernel panics 時得到最多的資訊?
18.14. 為什麼 dlsym() 不能操作 ELF 執行檔?
18.15. 我要如何增加或減少 kernel 能定址的空間?

18.1. 如何能學習更多有關 FreeBSD 內部的東西?

目前市面上還沒有探討作業系統內部的書是專為 FreeBSD 而寫 的。然而,許多一般的 UNIX 知識都可以直接應用在 FreeBSD 上。附 加一點,仍然有相關的書是專為 BSD 所寫的。

請參考 Handbook 的作業系統內部之參考書目

18.2. 如何能為 FreeBSD 出一份力?

請參考這篇文章 Contributing to FreeBSD 來提供您的建議。如果您能幫忙那就更歡迎了!

18.3. SNAP 和 RELEASE 是什麼?

目前有三個活躍/半活躍的分支在 FreeBSD 的 CVS Repository (RELENG_2 分支一年大概更動兩次,正是為何只有三個活躍的發展中分支):

  • RELENG_2_22.2-STABLE

  • RELENG_33.X-STABLE

  • RELENG_44-STABLE

  • HEAD-CURRENT5.0-CURRENT

如同其他兩個,HEAD 並不是真正 的 branch tag,它只是一個符號常數,指向 "current (尚未分支的發展中版本)" 簡記為 "-CURRENT"

以現在來說,"-CURRENT" 朝向 5.0 發展,而 4-STABLE 分支,也就是 RELENG_4,在 2000 年三月從 "-CURRENT" 分支出來。

2.2-STABLE 這個分支,也就是 RELENG_2_2,在 1996 年十一月從 -CURRENT 分支出 來,這個分支目前已經完全退休了。

18.4. 要怎麼作出自己的 release?

請參照 Release 工程 文章說明。

18.5. 為什麼 make world 把原來裝的 binary 檔 都換掉了?

沒錯,就是這樣子。如名字所示,make world 會重新編譯系統內建的每個 binary 檔,這樣在結束時就可確定有個一致 且乾淨的環境(所以要花上好一段時間)。

在執行 make worldmake install 時,如果有設 DESTDIR 這個環境變數,新產生的 binary 將會裝在 ${DESTDIR} 下同樣的目錄樹中。但在某些修改 shared library 和重建 binary 的無特定情況下,這樣做可能會使 make world 失敗。

18.6. 在系統開機時,出現 "(bus speed defaulted)"

Adaptec 1542 SCSI 卡允許使用者用軟體調整匯流排的存取速度。 早期的 1542 驅動程式試圖將它設成可用的最快速度,但後來發現在一 些機器上不能用,所以現在要在 kernel 設定中加 TUNE_1542 這個選項來啟動這個功能。在支援的機器 上用這個選項會使硬碟存取更快,但在不支援的機器上有可能會毀掉資料。

18.7. 在網路頻寬有限的情況下,我也可以跟上 current 的發展嗎?

是的,藉著 CTM 您就可以不用下載全部的程式碼。

18.8. 是怎麼把發行版本中的檔案切成一個個 240k 的小檔案的?

以 BSD 為基礎的較新系統有個 -b 選項 可以把檔案以任意數目 byte 切開。

這裡是 /usr/src/Makefile 中的一個 例子:

    bin-tarball:
    (cd ${DISTDIR}; \
    tar cf - . \
    gzip --no-name -9 -c | \
    split -b 240640 - \
    ${RELEASEDIR}/tarballs/bindist/bin_tgz.)

18.9. 我在 kernel 中加了新功能,我要把它寄給誰?

請參考 Contributing to FreeBSD 中的文章,以了解要如何提供您的程式碼。

同時也謝謝您的關心!

18.10. ISA 的隨插即用卡是如何偵測及初始化的?

由 Frank Durda IV 所寫:

簡單的說,當主機發出是否有 PnP 卡的詢問訊號時,所有的 PnP 卡 會在幾個固定的 I/O port 作回應。所以當偵測 PnP 的程式開始時,它 會先問有沒有 PnP 卡在,接著所有 PnP 卡會在它讀的 port 以自己的 型號 # 作回答,這樣偵測程式就會得到一個 wired-OR "yes" 的數字,其中至少會有一個 bit 是打開的。然後偵測程式會要求型號 (由 Microsoft/Intel指定)小於 X 的卡"離線"。再去看是 否還有卡回答同樣的詢問,如果得到 0,就表示沒 有型號大於 X 的卡。 現在程式會問是否有型號小於 X 的卡,如果有的話,程式再要型號大於 X-(limit/4) 的卡離線,然後重覆 上面的動作。用重複這種類似 semi-binary search 的方法,在某範圍內 找個幾次後,測程式最後會在機器中區分出所有的 PnP 卡,搜尋次數也 遠低於一個個找的 2^64 次。

一張卡的 ID 由兩個 32-bit(所以上面是 2ˆ64) + 8bit 偵錯碼 組成,第一個 32 bits 是用來區分各家廠商的。這些廠商從來沒有出來澄 清過,但看來應假設同一家出的不同種類的卡的廠商 ID 有可能不同。用 32 bits 只來表示不同廠商的想法實在有點過頭了。

第二個 32 bits 則是型號 #、乙太網路位址、或一些使這張卡獨特的 資料。除非第一個 32 bits 不同,否則廠商不可能作出第二個 32 bit 相 同的兩張卡。所以在一台機器中可以有同樣的好幾張卡,然而他們整個 64 bits 還是會都不一樣。

這兩個 32 bit 絕對不可以全為零,這才能使得最開始 binary search 中的 wired-OR 會得到一個非零數字。

一旦系統區分出所有卡的 ID,接著會經由同樣的 I/O port 一個個重 新啟動每張卡,接著找出已知介面卡所需的資源、有哪些中斷可以使用等 等。所有卡都會被掃描一次,來收集這些資料。

這些資訊接著和硬碟上的 ECU 檔案、或 MLB BIOS 裡的資料結合在一 起,通常是綜合 ECU 和 MLB 裡的 BIOS PnP 資料,這些週邊並不支援真正 的 PnP,然而偵測程式在檢查 BIOS 和 ECU 資料後,它可以避免 PnP 週邊 和那些偵測不到的相衝突。

接著再度拜訪這些 PnP 週邊,這次會把可用的 I/O、DMA、IRQ 和記憶 體映射的位址都指定給它們。這些週邊就會出現在所指定的地方,直到下一 次重新開機為止,不過也沒有人說不能把它們隨時移來移去。

上面有相當多的簡化,但你應該已經了解大致的過程。

Microsoft 把表示印表機狀態的幾個主要 port 拿來作 PnP,他們的 邏輯是沒有一張卡會在這些地方解碼作相反的 I/O cycles。但是我找到 一款早期仍在評估 PnP 提案時的 IBM 原廠 printer board,它的確去解 對這些狀態 port 的寫入資料,但是 MS "說了就算"。所以 它們的確有對印表機狀態 port 寫入,還有讀取該位址 + 0x800、和另一個在 0x2000x3ff 之間的 port。

18.11. 我為某設備寫了驅動程式,能不能給它一個 major number?

這要看你是否打算將這個驅動程式公開使用,如果是的話,請把它的 原始碼送一份給我們,還有 files.i386 修改的 部份、kernel 設定檔樣本、以及用來產生設備檔的 MAKEDEV(8)。 如果你不打算公開、或因為版權問題而不能公開的話,我們有特地保留 character major number 32 和 block major number 8 給這方面的使用, 直接用這兩個就好了。不論如何,我們都會很感激你能在 發表驅動程式的消息。

18.12. 關於更動目錄放置的原則?

在回答關於更動目錄放置的原則方面,我在 1983 年寫好目前的作法 後就沒有再改變過,這種方式是針對原先的 FFS 檔案系統,後來也沒有 對它作任何更動。它在避免 cylinder group 被填滿這方面做得相當成功, 但是就像有些人已經注意到,它和 find 就配合得不大好。大部份的檔案 系統是由那些用 depth first search(aka ftw) 產生的 archive 製造出 來,解出來的目錄 inode 會橫跨好幾個 cylinder group,如果以後要做 depth first search 的話,這是最糟糕的情況之一。如果我們知道總共 會產生多少目錄的話,解法是在做任何存取/寫入動作之前,在每個 cylinder group 上先造出(所有目錄數/cylinder greoup 的數目)這麼多 的目錄。很明顯的,我們必須要有根據地去猜這 個數字,就算一個像 10 的很小固定數目也會使效率以級數成長。區分 restore (即解開上述的 archive) 和一般檔案操作的方法可以是(現在用的演算法可能要更敏感): 如果一些目錄(最多 10 個)都在 10 秒內產 生的話,那麼就把這些目錄 聚集在同一個 cylinder group。不管怎樣, 我的經驗指出這是一個已經 充份實驗過的部份。

Kirk McKusick, September 1998

18.13. 如何在 kernel panics 時得到最多的資訊?

[這節是從 Bill Paul 在 freebsd-current mailing list 上發表的信中節錄, Dag-Erling C. Smørgrav 修正了打字錯誤、再加上括弧裡的注解。]

    From: Bill Paul <wpaul@skynet.ctr.columbia.edu>
    Subject: Re: the fs fun never stops
    To: Ben Rosengart
    Date: Sun, 20 Sep 1998 15:22:50 -0400 (EDT)
    Cc: current@FreeBSD.org

[Ben 發表了下面的 panic 訊息]

    > Fatal trap 12: page fault while in kernel mode
    > fault virtual address   = 0x40
    > fault code              = supervisor read, page not present
    > instruction pointer     = 0x8:0xf014a7e5
                                    ^^^^^^^^^^
    > stack pointer           = 0x10:0xf4ed6f24
    > frame pointer           = 0x10:0xf4ed6f28
    > code segment            = base 0x0, limit 0xfffff, type 0x1b
    >                         = DPL 0, pres 1, def32 1, gran 1
    > processor eflags        = interrupt enabled, resume, IOPL = 0
    > current process         = 80 (mount)
    > interrupt mask          =
    > trap number             = 12
    > panic: page fault

當你看到像這樣的訊息時,只把它拷一份送上來是不夠的。我在上面 特地標明的 instruction pointer 值相當重要,不幸的是它會因設定而 不同。換句話說,這個值會跟你用的 kernel image 檔而變動。如果是用 某個 snapshot 版本的 GENERIC kernel,也許其他人可以追蹤到出問題 的函式,但如果你是用自訂的 kernel,那麼只有 才能告訴我們問題出在那裡。

要做的事包括這些:

  1. 把 instruction pointer 的值記下來。注意在前面的 0x8: 在這個情況中並不重要,我們要的是 0xf0xxxxxx

  2. 當系統重新開機後,執行這道命令:

        % nm -n /(造成 panic 的 kernel 檔案) | grep f0xxxxxx
    其中 f0xxxxxx 就是記下來的 instruction pointer 值。有可能不會剛好找到完整的這個字串, 這是因為 kernel symbol table 裡的各個 symbol 只是函式的進 入點,但 instruction pointer 所指的位址有可能是在函式內的 某一處,而不一定在開頭。所以如果找不到整個字串,那麼把 instruction pointer 值的最後一個數字拿掉,再試一次:
        % nm -n /(造成 panic 的 kernel 檔案) | grep f0xxxxx
    如果這樣也找不到,那就把另一個數字去掉再找,一直重複到找到 為止, 結果是一串可能造成 panic 的函式列表。這樣比直接找到 出問題的函式來得差,但至少好過什麼都沒有。

我常常看到人們顯示一大片 panic 訊息,但很少看到有人花一點時間 把 instruction pointer 和 kernel symbol table 中的函式比較一下。

要追蹤出造成 panic 原因的最好方法是先做出 crash dump,然後用 gdb(1) 在上面做 stack trace。

不管是那一種,我通常是用這個方法:

  1. 寫好 kernel 設定檔。如果你需要用 kernel debugger,在設 定檔中加上 options DDB 這個選項。 (當我懷疑有出現無窮迴圈時,通常會用這個來設定中斷點。)

  2. config -g KERNELCONFIG 做出用來編譯的目錄。

  3. cd /sys/compile/ KERNELCONFIG; make

  4. 等待 kernel 編譯結束。

  5. make install

  6. 重新開機

make(1) 將會製造出兩個 kernel。kernel 還有 kernel.debugkernel 將會被安裝到 /kernel,而 kernel.debug 可用來給 gdb(1) 當作 debugging symbols 的來源。

要確定能抓到 crash dump,先編輯 /etc/rc.confdumpdev 指 到 swap 分割區。這樣 rc(8) 會用 dumpon(8) 來啟動 crash dump,你也可以手動執行 dumpon(8) 在 panic 之後, crash dump 可以用 savecore(8) 存起來;如果 /etc/rc.conf 裡有設 dumpdev 那麼重新開機後 rc(8) 會自動執行 savecore(8) 把 crash dump 存在 /var/crash

Note: FreeBSD 的 crash dump 通常和機器裡的實際記憶體一樣大,就 像如果有 64MB 記憶體,crash dump 大小就是 64MB。所以要確定 /var/crash 下有足夠的空間,或是可以手 動執行 savecore(8) 把 crash dump 放到另一個空間較夠的 目錄下。另一種也許可以限制 crash dump 的方法,是在 kernel 設定檔中用 options MAXMEM=(foo),將 kernel 可用的記憶體限制在合理的大小。舉例來說,如果你有 128MB 的記憶 體,但是可以限制 kernel 只能用 16MB 的記憶體,這樣 crash dump 就是 16MB 而不是 128MB 了。

一旦發現有了 crash dump,就可以用 gdb(1) 來做 stack trace ,如下所示:

    % gdb -k /sys/compile/KERNELCONFIG/kernel.debug /var/crash/vmcore.0
    (gdb) where

要注意可能會出現好幾個螢幕的可用資訊,你可以用 script(1) 把所有輸出都存起來。用包括所有 debug symbol 的 kernel 來除錯,這 樣應該可以直接顯示 panic 是發生在那一行。通常是由下往上讀 stack strace,這樣才能一個個追蹤出有哪些動作引到 crash。也可以用 gdb(1) 把各種變數或結構的內容印出來,以檢查系統 crash 時的 實際狀態。

好啦,如果你有第二台電腦而且有夠瘋狂,可以將 gdb(1) 設定 成遠端除錯。這樣你可以在一台機器中用 gdb(1) 去除錯另一台裡的 kernel,可以執行的包括設定中斷點、在 kernel 原始碼中一步步執行等 等,就像在一般使用者程式上除錯一樣。由於沒有什麼機會為除錯而設置 兩台並鄰電腦,所以我還沒有這樣玩過。

[Bill 補充:"我忘了提到一點:如果你有啟動 DDB 而 kernel 也已經進入除錯器,可以在 DDB 命令列下打 'panic',強迫產生 panic (還有 crash dump)。也有可能在 panic 階段時再進入除錯器, 如果這樣的話,輸入 'continue',接著它就會完成 crash dump。" -ed]

18.14. 為什麼 dlsym() 不能操作 ELF 執行檔?

在 ELF 一系列的工具中,內定是不會讓 dynamic linker 看到執行 檔裡定義了哪些 symbol。所以 dlsym() 沒有辦 法用藉由呼叫 dlopen(NULL, flags) 取得的 handle,用它去搜尋有那些 symbol 一定會失敗。

如果你想要用 dlsym() 找出某個 process 的主執行檔中有哪些 symbol,則要在 link 時對 ELF linker (ld(1)) 加上 -export-dynamic 這個參數。

18.15. 我要如何增加或減少 kernel 能定址的空間?

預設值是,FreeBSD 3.x 的 kernel 可以定址的空間是 256 MB 而 FreeBSD 4.x 可以到 1 GB。如果是網路負荷相當重的伺服器 (例如大型 FTP 或 HTTP 伺服器),你也許會發現 256 MB 可能不大夠。

所以,要如何增加定址空間呢?要從兩方面著手。首先首先告訴 kernel 本身要保留較大空間給自己。其次,既然是在定址空間的最上 面載入 kernel,所以還要調低載入的位址,才不會和前面定址的範圍 重疊。

增加 src/sys/i386/include/pmap.h 裡的 NKPDE 就可以達成第一個目標。1 GB 的定址空間會 像這樣:

    #ifndef NKPDE
    #ifdef SMP
    #define NKPDE                   254     /* addressable number of page tables/pde's */
    #else
    #define NKPDE                   255     /* addressable number of page tables/pde's */
    #endif  /* SMP */
    #endif

要算出 NKPDE 的正確值,將想要的空間大小 (以 megabyte 為單位)除以 4,接著單 CPU 機器減 1, 雙 CPU 則是減 2。

要解決第二個問題,必須自行算出 kernel 被載入的位址:求出 0x100100000 減掉定址空間大小的值(以 byte 為單位),如 1 GB 大小就是 0xc0100000。把src/sys/i386/conf/Makefile.i386 裡的 LOAD_ADDRESS 設成這個值﹔接著在 src/sys/i386/conf/kernel.script 中,將 section 列表最前面的 location counter 設成相同的值,如下:

    OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
    OUTPUT_ARCH(i386)
    ENTRY(btext)
    SEARCH_DIR(/usr/lib); SEARCH_DIR(/usr/obj/elf/home/src/tmp/usr/i386-unknown-freebsdelf/lib);
    SECTIONS
    {
      /* Read-only sections, merged into text segment: */
      . = 0xc0100000 + SIZEOF_HEADERS;
      .interp     : { *(.interp)    }

然後重新編譯您的 kernel。您可能會在執行 ps(1)top(1) 這類的程式時碰到問題﹔make world 應該就可以解決 (或把改過的 pmap.h 複製到 /usr/include/vm/ 下,再手動編譯 libkvmps(1) 還有 top(1))。

注意:kernel 所能定址的空間大小必須是 4 megabytes 的倍數。

[David Greenman 補充:我認為 kernel 定址空間大小應該要是 2 的乘冪,但不大確定這一點。舊的啟動程式會動到 high order address bits,記得它假設至少有 256 MB。]