天天看點

Linux核心建構系統之十

到目前為止,核心建構系統的大部分重要的地方都已讨論完畢,惟獨有一個很關鍵的方面還沒讨論完全,那就是依賴關系的處理。熟悉Linux内應用程式開發的人都知道,要想用 make 工具來自動化的管理他們的應用項目工程,就必須正确處理所要編譯的目标和生成這些目标所需檔案之間的依賴關系。舉個例子,比方你要編譯一個對象檔案 hello.o,那麼你就需要告訴 make 工具生成該對象檔案所需要的依賴有哪些後,make 才能幫你正确的管理它的編譯。這些所依賴的檔案通常有可能是:對應的C程式檔案 hello.c、你自己寫的頭檔案、函數庫内的頭檔案等等。

在實際應用項目開發的時候,我們開發者往往不會用手動的方式把這些所有依賴關系都列在 Makefile 中。更常見的方法是在 Makefile 中使用 gcc 的選項 -M(或者其他 -MD之類的),讓它幫我們生成包含如下形式依賴性規則的 *.d 檔案,然後在 Makefile 的後面用 -include 之類的來包含這個 *.d 檔案。這樣編譯的時候,如果其中某個依賴檔案被修改了,那對應的這個目标就一定會被make更新。

​​​​

那對于 Linux kernel,其實也需要處理一系列的依賴關系。隻不過,它将應用程所常用的那種依賴關系擴大化了。具體來說,核心建構系統在建構的時候,需要根據下列情況來決定一個目标是否需要被建構:

如果存在有該目标對應的依賴檔案被改動,或者目前還未被生成,那核心建構系統肯定要生成該目标;

如果生成該目标的指令行與之前儲存下來的指令行不一緻,比方改了一下C編譯器或彙編器的标志之類的,那該目标也要被重新生成;

如果某個 CONFIG_XXX 配置選項被改了。比方原先是 =y 的,現在改成 =m;原先沒設定過的,現在設定成 =m 之類的。那所有在源代碼中使用過這個選項的,比方在 hello.c(也有可能是另外某個頭檔案) 裡面用到了這個選項,那對應的目标 hello.o 就要被重新生成。

我們來看看這些是如何實作的,有些東西在之前的讨論中我們已經接觸到過,這裡隻是總結性的提一下。也有些沒讨論到的,這裡會列出一些代碼來分析。在核心建構系統的衆多makefile中都會使用到字元串 "-MD,$(depfile)",比方是在像檔案 .../scripts/Makefile.lib 中,将它賦給了 c_flags/a_flags/cpp_flags 等标志,這意味着建構系統在使用c編譯器/彙編器/c預處理器等過程中,就會産生依賴規則檔案。變量 depfile 定義在 .../scripts/Kbuild.include 檔案中:

###
# Name of target with a '.' as filename prefix. foo/bar.o => foo/.bar.o
dot-target = $(dir $@).$(notdir $@)
 
###
# The temporary file to save gcc -MD generated dependencies must not
# contain a comma
depfile = $(subst $(comma),_,$(dot-target).d)      

另外,也會經常使用到字元串 "$(LINUXINCLUDE)",比方在同樣的檔案中賦給這些标志變量。而下面變量 LINUXINCLUDE 的定義中(定義在頂層Makefile 檔案中)會使用 "-include include/linux/autoconf.h"。

# Use LINUXINCLUDE when you must reference the include/ directory.
# Needed to be compatible with the O= option
LINUXINCLUDE    := -Iinclude \
                   $(if $(KBUILD_SRC),-Iinclude2 -I$(srctree)/include) \
                   -I$(srctree)/arch/$(hdr-arch)/include               \
                   -include include/linux/autoconf.h      

是以這意味着,在産生的依賴規則檔案中,也會包含檔案 .../include/linux/autoconf.h,并且這種包含是全局迷漫性的。也就是這個檔案會被核心大多數目标所依賴。

這裡會存在一個問題。假如我前後兩次的核心編譯過程中,差別隻在于第一次配置選項 CONFIG_MY_DRIVER 被設定為 =m,而第二次被設定為 =y。那你想在這第二次編譯過程中是不是因為更新了 .../include/linux/autoconf.h 檔案而去全部編譯依賴這個檔案的所有目标呢?顯然是這樣的,因為 .../include/linux/autoconf.h 檔案會因為配置選項的改變而變得更新,是以,自然而然,依賴它的所有目标會被重新更新。

這是沒有必要的嚴重浪費。因為我隻是改變了我自己所寫内部子產品對應的那個那個配置選項 CONFIG_MY_DEVICE_DRIVER。換句話講,這個選項隻影響到我自己寫的那個内部子產品,而其他任何内部子產品或者基本核心代碼都沒有使用這個選項,是以在第二次編譯過程中,沒有必要去重新remake差不多全部目标,而隻需要重新編譯我自己寫的那個内部子產品即可。

為了避免這種不必要的額外負擔,而達到隻編譯那些因為配置選項變更而确實受到影響的目标。核心建構系統使用 .../scripts/basic/fixdep 做了一個小動作。該動作修改依賴規則檔案,從中删除對 .../include/linux/autoconf.h 檔案的直接依賴,而代之以對 .../include/config/ 目錄下的空的頭檔案的依賴。下面,我們慢慢來解釋這是怎麼做到的。

在前面我們讨論 kconfig 的時候說過,配置工具 .../scripts/kconfig/conf 在産生 auto.conf、auto.conf.cmd 和 autoconf.h 等三個檔案。與此同時,它會在函數 conf_write_autoconf 中調用conf_split_config 函數。當時并沒有說這個函數的确切工作過程,而隻是留下一個伏筆說它會負責在 .../include/config 産生一系列的頭檔案。那這裡我們詳細來看看它内部是如何産生這些檔案。先列出該函數的架構:

​​​​

該函數先使用 conf_get_autoconfig_name 取得檔案 ".../include/config/auto.conf" 的名稱,然後用 for 循環處理每一個 CONFIG_XXX 的定義。比方針對我們之前的那個 CONFIG_MY_DEVICE_DRIVER 配置選項,處理的時候,它在字元數組 path 中存儲這樣的字元串:my/device/driver.h。接下來,它會在後面用 open 系統調用在 .../include/config 目錄下去建立名為此字元串的空頭檔案。從這裡可以看出,針對每個配置過的配置選項,都會有這樣的頭檔案産生。

前面說過建構系統會将對 .../include/linux/autoconf.h 檔案的依賴轉換為對這裡的這些頭檔案的依賴。這是怎樣的一個機制?比方說,在我自己的内部子產品中,因為使用到了配置選項 CONFIG_MY_DEVICE_DRIVER,是以 fixdep 會将我對 autoconf.h 檔案的依賴轉換成對 .../include/config/my/device/driver.h 頭檔案的依賴。而對其他内部子產品或者基本核心的目标來說,在代碼中沒有使用到這個配置選項,是以 fixdep 對它們的處理,隻是簡單的從它們對應的依賴規則中删除對 autoconf.h 檔案的依賴。

這個過程現在不明白不要緊,我們先來看看 fixdep 在建構系統代碼中是如何被使用的。在這之後,我們來舉例說明這個過程。在核心建構系統中,你在變量 if_changed_dep 以及宏 rule_cc_o_c 的定義中都能發現對 scripts/basic/fixdep 的調用:

# Execute the command and also postprocess generated .d dependencies file.
if_changed_dep = $(if $(strip $(any-prereq) $(arg-check) ),                  \
        @set -e;                                                             \
        $(echo-cmd) $(cmd_$(1));                                             \
        scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).tmp;\
        rm -f $(depfile);                                                    \
        mv -f $(dot-target).tmp $(dot-target).cmd)      
define rule_cc_o_c
        $(call echo-cmd,checksrc) $(cmd_checksrc)                         \
        $(call echo-cmd,cc_o_c) $(cmd_cc_o_c);                            \
        $(cmd_modversions)                                                \
        $(cmd_record_mcount)                                              \
        scripts/basic/fixdep $(depfile) $@ '$(call make-cmd,cc_o_c)' >    \
                                                      $(dot-target).tmp;  \
        rm -f $(depfile);                                                 \
        mv -f $(dot-target).tmp $(dot-target).cmd
endef      

概括一下,其中對 fixdep 的調用文法是:fixdep 。fixdep 會讀入依賴規則檔案和指令行,并輸出一些内容送向标準輸出。那麼前面這兩段代碼中對 fixdep 的調用就會将這些标準輸出中輸出的内容重重定向到檔案 $(dot-target).tmp。最後此檔案被重命名為 $(dot-target).cmd。

還是讓我們舉例來說明吧,現在假設我們要編譯的目标是 .../drivers/mydriver.o,它是構成我們前面内部子產品 .ko 的對應對象檔案。那麼根據前面我們讨論的記過,建構系統會調用 rule_cc_o_c 來生成 mydriver.o。假設前面針對 mydriver.o 生成出來的依賴規則檔案 .mydriver.o.d 是這樣的:

​​​​

并且,生成 mydriver.o 的指令行是:

arm-linux-gcc -Wp,-MD,drivers/.mydriver.o.d  -nostdinc ....      

那麼fixdep 在處理的時候,會首先向标準輸出輸出内容:

cmd_drivers/mydriver.o := arm-linux-gcc -Wp,-MD,drivers/.mydriver.o.d -nostdinc ...      

然後,它周遊處理依賴規則中的所有依賴檔案,用GREP檢查這些檔案中是否包含有對配置選項諸如 CONFIG_MY_DEVICE_DRIVER 之類的使用。如果有的話,它将 .../include/config/my/device/driver.h 檔案也加到依賴檔案清單中。注意在周遊處理的過程中,fixdep 會把 autoconf.h 從依賴清單中過濾掉。周遊完之後,它又會向标準輸出輸出下面這樣的内容:

​​​​

繼續閱讀