1. 程式人生 > >嵌入式linux基礎教程第二版 第五章 核心初始化

嵌入式linux基礎教程第二版 第五章 核心初始化

                                                                                            第五章      核心初始化

        5.1合成核心映象:Piggy 及其他

        make ARCH=arm CROSS_COMPILE=xcale_be- zImage

              LD                 vmlinux

              SYSMAP       System.map

              SYSMAP       .tmp_System.map

              OBJCOPY     arch/arm/boot/Image

              Kernel:           arch/arm/boot/Image is ready

              AS                  arch/arm/boot/compressed/head.o

              GZIP               arch/arm/boot/compressed/piggy.gz

              AS                   arch/arm/boot/compressed/piggy.o

              CC                  arch/arm/boot/compressed/misc.o

              AS                   arch/arm/boot/compressed/head-xscale.o

              AS                   arch/arm/boot/compressed/big-endian.o

              LD                   arch/arm/boot/compressed/vmlinux

              OBJCOPY      arch/arm/boot/zImage

              Kernel:            arch/arm/boot/zImage is ready

        構建系統產生了vmlinux映象。之後,構建系統處理了很多其他物件模組。其中,包括head.o、piggy.o以及與具體架構相關的head-xscale.o等。AS 表示構建系統掉用了彙編器,GZIP表明在進行壓縮。一般來說,這些物件模組都是和具體架構相關的,並且包含了一些底層函式。用於在特定架構上引導核心。

上圖為合成核心映象的結構

        Image物件:當核心ELF檔案構建成功之後,核心構建系統繼續處理其他的目標。Image物件是由vmlinux物件生成的。去掉ELF檔案中的冗餘段,並去掉所有可能存在的除錯符號,就是Image了。下面這條命令用於該用途

        xscale_be-objcopy -O binary -R .note -R .note.gnu.build-id -R .comment -S vmlinux arch/arm/boot/Image

        -O選項指示objcopy生成一個二進位制檔案,-R刪除ELF檔案中的.note、.note.gnu.build.id和.comment這三個段。-S選項用於去除除錯符號。objcopy以ELF的映象vmlinux為輸入,生成名為Image的目標二進位制檔案。Image將vmlinux從ELF轉換成二進位制形式,並去除了除錯資訊和前面的.note*和,comment段

       與具體架構相關的物件:由幾個彙編原始檔編譯的物件(head.o和head-xscale.o),他們完成與底層具體架構及處理器相關的一些任務。建立piggy.o物件,用gzip命令對Image檔案進行壓縮生成piggy.o。

           cat  Image   |    gzip  -f  -9 > piggy.gz

       彙編器彙編名為piggy.S的組合語言檔案,而這個檔案包含了一個對壓縮檔案piggy.gz的引用。從本質上說,二進位制核心映象以負載的形式依附在了一個底層的啟動載入程式之上,採用組合語言編寫。啟動載入程式先初始化處理器和必須的記憶體區域,然後解壓二進位制核心映象(piggy.gz)並將解壓後的核心映象(Image)載入到系統記憶體的合適位置,最後將控制權轉交給它。

       piggy.s

       .section    .piggydata,  #alloc

       .global      input_data

   input_data

       .incbin   "arch/arm/boot/compressed/piggy.gz"

       .global      end_data

   input_data_end:

       彙編器彙編這個檔案並生成一個ELF格式的映象piggy.o,該映象包含一個名為.piggydata的段,這個檔案的作用是將壓縮後的二進位制核心映象(piggy.gz)放到這個段中,成為其內容。該檔案通過彙編器的預處理指令.incbin將piggy.gz包含進來,incbin類似於include只是它包含的是二進位制資料。總之,該彙編檔案的作用是將壓縮的二進位制核心映象(piggy.gz)放在另一個映象(piggy.o)中。啟動載入程式利用input_data和input_data_end來確定包含的核心映象的邊界

       啟動載入程式:啟動載入程式將linux核心映象載入到記憶體中。有些啟動載入程式會對核心映象進行校驗和檢查,而大多數啟動載入程式會解壓並重新部署核心映象。當硬體單板加電時,引導載入程式獲得其控制權,根本不依賴於核心。引導載入程式將核心映象從工作站載入到開發板的實體記憶體上而啟動載入程式將核心映象從開發板的物理儲存載入到記憶體中。啟動載入程式負責提供合適的上下文讓核心運行於其中,並且執行必要的步驟以解壓和重新部署核心二進位制映象。

       啟動載入程式和核心映象拼接在一起用於載入

       

       針對ARM XScale的合成核心映象

       在我們研究的這個例子中,啟動載入程式包含了上圖顯示的二進位制映象,這個啟動載入程式完成以下功能

       (1)底層的用匯編語言編實現的處理器初始化,這包括支援處理器內部指令和資料快取、禁止中斷並建立C語言執行環境。這部分功能由head.o和head-xscale.o完成

       (2)解壓並重新部署映象,這部分功能由misc.o完成

       (3)其他與處理器相關的初始化,比如big-endian.o,將特定處理器的位元組設定為大端位元組序

        引導訊息:

        在PC上引導某種linux發行版,在PC自身的BIOS訊息之後,你會看到很多由linux輸出控制檯訊息,表明它正在初始化各個核心子系統。在嵌入式系統中啟動linux時的情況與PC工作站類似。

         Using  base  address  0x01000000  and  length  0x001ce114

         Uncompressing  Linux.....done,  booting  the  kernel

         Linux  version  2.6.32-07500-g8bea867 ([email protected])  (gcc  version  4.2.0  20070126(prerelease)(MontaVista  4.2.0-3.0.0.0702771  2007-03-10))  #12  Wed  Dec  16  23:07:01  EST  2009

          .

          .

          .

          .

       第一行是由板卡上的引導載入程式Redboot產生的,第二行是啟動載入程式產生的。這行訊息由.../arch/arm/boot/compressed/misc.c中的函式decompress_kernel()產生的。第三行是核心版本字串這是核心本身輸出的第一行訊息,核心進入函式start_kernel()(這個函式在原始檔.../init/main.c中)之後,首先執行的幾行程式碼中包括下面這行

         printk(KERN_NOTICE:"%s",linux_banner);

       這些核心版本字串包括了核心版本、核心編譯時侯所使用的使用者名稱/機器名、工具連資訊、構建號、編譯核心映象時的日期和時間

       產品開發人員一般使用構建號來自動跟蹤構建軌跡。構建號儲存在.version的隱藏檔案中,這個檔案位於核心原始碼的頂層目錄,構建號會由構建指令碼.../scripts/mkversion自動構建。它是一個數字的字串標籤,當核心程式碼有重大變化並重新編譯時自動遞增。

        5.2初始化時的控制流

       我們來研究一個完整的啟動週期(從引導載入程式到核心)中的控制流。引導載入程式是一種底層軟體,儲存在系統的非易失性記憶體(快閃記憶體或ROM)中,系統加電時他立即獲得控制權。它通常體積很小,包含一些簡單函式,主要用於底層初始化、作業系統映象的載入和系統診斷。它可能會包含讀寫記憶體的函式,以檢查和修改記憶體的內容。它包含底層的板卡自檢程式,包括檢測記憶體和I/O裝置。引導載入程式還包含了一些處理邏輯,用於將控制權轉交給另一個程式,一般是作業系統,比如linux。

        基於ARM  XScale平臺;名為Redboot的引導載入程式,當系統第一次加電時這個引導載入程式開始執行,然後會載入作業系統。當引導載入程式部署並載入了作業系統映象(這個映象可能儲存在本地的快閃記憶體,硬碟驅動器中,或通過區域網或其他裝置)之後,就將控制權轉交給那個映象。

        對於特定的ARM  XScale平臺,引導載入程式將控制權轉交給啟動載入程式(第二階段引導裝入程式)的head.o模組,

      

       處於核心映象之前的啟動載入程式有個很重要的任務:建立合適的環境,解壓並重新部署核心映象,並將控制權轉交給它。啟動載入程式將控制權轉交給核心主體中的一個模組,通常這個模組的名字都是head.o。

       當啟動載入程式完成它的工作後,將控制權轉交給核心主體的head.o,之後再轉到檔案main.c中的函式start_kernel()

       核心入口:head.o

       核心開發人員的目的是讓head.o這個與架構相關的模組通用化,不依賴於任何機器型別。這個模組由head.S生成,它的具體路徑是.../arch/<ARCH>/kernel/head.s,<ARCH>為具體的架構。

       head.o模組完成與架構和CPU相關的初始化,為核心主體的執行做好準備。與CPU相關的初始化工作儘可能的做到了在同系列處理器中通用。與機器相關的初始化工作是在別處完成的。head.o還要執行下列底層任務。

      (1)檢查處理器和架構的有效性

      (2)建立初始的頁表表項

      (3)啟用處理器的記憶體管理單元(MMU)

      (4)進行錯誤檢測並報告

      (5)調轉到核心主體的執行位置,也就是檔案main.c中的函式start_kernel()

       嵌入式新手企圖單步除錯這些程式碼,但最終發現偵錯程式並不能派上用場。當啟動載入程式第一次將控制權交給核心的head.o時,處理器運行於我們過去常說的實地址模式。處理器的程式計數器或其他類似暫存器中所包含的值成為邏輯地址,而處理器記憶體地址引腳上的電訊號地址成為實體地址。在實地址模式下,這兩者相等。為了啟用記憶體地址轉換,需要先初始化相關的暫存器和核心資料結構,當這些初始化完成後,就會開啟處理器的MMU。在MMU開啟的一瞬間,處理器看到的地址空間被替換成了一個虛擬地址空間,而這個空間的結構和形式由核心開發者決定。當MMU功能開啟的一瞬間實體地址被替換成了邏輯地址,這就是為什麼不能像除錯普通程式碼那樣除錯這段程式碼。

       在核心引導過程的早期階段,地址的對映範圍有限。很多開發者試圖修改head.o以適應特定平臺,但因為這個限制而犯錯。假設你有一個硬體裝置,你需要在系統引導的早期階段載入一個韌體。一種方法是將必須的韌體靜態編譯到核心映象中,然後使用一個指標引用它,並將他下載到你的裝置中。然而由於在核心引導的早期階段,其地址對映存在限制,很有可能韌體映象所處的位置超出了這個範圍,當代嗎執行時,它產生一個頁面錯誤,因為這時你想訪問一個記憶體區域,但處理器內部還沒有建立起對這塊區域的有效對映。在早期階段頁面錯誤處理程式還沒有安裝到位,所以最終結果會莫名其妙的系統崩潰。在系統引導早期階段有一點非常確定,不會有任何錯誤訊息能夠幫你找到問題所在。

       明智的做法是儘可能推遲所有硬體的初始化工作,直到核心完成引導之後。用這種方式,你可以使用眾所周知的裝置驅動程式模型來訪問定製硬體,而不是去修改複雜很多的組合語言程式碼。這一層及的程式碼使用了很多技巧,而他們都沒有說明文件可供參考。這方面最常見的一個例子就是解決硬體上的一些錯誤,而這些錯誤可能有說明文件也可能沒有。如果你必須修改語言早期啟動程式碼,你需要開發時間、費用和複雜度等方面付出更高的代價。硬體和軟體開發工程師需要在硬體開發早期階段討論這些問題。此時往往硬體設計的一個小小改變可以大大減少軟體開發時間。

       嵌入式開發人員必須對虛擬記憶體環境熟悉。

       核心啟動:main.c

       核心自身的head.o模組完成的最後一個任務就是將控制權交給一個由C語言編寫的,負責核心啟動的原始檔。我們後續主要介紹這個檔案,

       控制權從核心的第一個物件模組(head.o)轉交至C語言函式start_kernel( ),這個函式位於檔案.../init/main.c中,核心從這裡開始了它新的生命旅程。任何想要深入學習linux核心的人都要仔細研究main.c檔案,研究它由哪些部分組成的以及這些成員是如何初始化和是例項化的。組合語言之後的大部分linux核心啟動工作是由main.c來完成的,從初始化第一個核心執行緒開始,直到掛載根檔案系統並執行最初的使用者空間linux應用程式。

       函式start_kernel()目前是main.c中最大的一個函式。大多數linux核心初始化工作都是在這個函式中完成的。此處的目的是要突出那些在嵌入式系統開發環境中由用的部分,再說一遍如果你想更系統的理解linux核心,花時間研究以下main.c是個很好的方法。

      架構設定

       .../init/main.c中的函式start_kernel()在其執行的開始階段會呼叫setup_arch(),而這個函式是在檔案.../arch/arm/kernel/setup.c中定義的。該函式接受一個引數——一個指向核心命令列的指標
               setup_arch(&command_line);

       該語句呼叫一個與具體架構相關的設定函式,linux支援的每種架構都提供了這個函式,它負責完成那些對某種具體架構通用的初始化工作。函式setup_arch()會呼叫其他具體識別CPU的函式,並提供了一種機制,用於呼叫高層特定CPU的初始化函式。例如setup_arch()直接呼叫setup_processor(),它位於.../arch/arm/kernel/setup.c中。這個函式會驗證CPU的ID和版本,並呼叫特定CPU的初始化函式,同時會在系統引導時向控制檯列印幾行相關資訊。

       4 CPU:XScale-IXP42x  Family  [690541c1]  version  1  (ARMv5TE),  cr = 000039ff

       5 CPU:  VIVT  data  cache,  VIVT instruction  cache

       6 MACHINE: ADI  Engineering  Coyote

       在這裡你可以看到CPU型別、ID字串和版本,這些資訊都是從處理器核心直接讀取的。接著是處理器快取和機器型別的詳細資訊。

       架構設定的最後的工作中有一項是完成那些依賴於機器型別的初始化。不同架構採用的機制有所不同。對於ARM架構,可以在.../arch/arm/mach-*系列目錄中找到與具體機器相關的初始化程式碼檔案,具體的檔案取決於具體的機器型別。

       核心命令列的處理

       在設定了架構之後,main.c開始執行一些通用的早期的初始化工作,並顯示核心命令列。

       linux一般由一個引導載入程式(或啟動載入程式)啟動的,它會向核心傳遞一系列引數,這些引數稱為核心命令列。雖然你不會真正的使用shell命令來啟動核心,但很多引導載入程式都可以使用這種大家熟悉的方式向核心傳遞引數。在有些平臺上,引導載入程式不能識別Linux,這時可以在編譯的時候定義核心命令列,並硬編碼到核心的二進位制映象中。在其他平臺上,使用者可以修改命令列的內容,而不需要重新編譯核心。這時,bootsrap loader從一個配置檔案生成核心命令列,並在系統引導時將它傳遞給核心。這些核心命令列引數相當於一種引導機制用於設定一些必須的初始設定,以正確引導特定的機器。

       linux核心中定義了大量的命令列引數。核心原始碼的.../Documentation子目錄中有一個名為kernel-parameters.txt的檔案,其中按字典順序列出了所有的核心命令列引數。核心的變化遠遠快於它的文件,使用這個文件作為指南而不是權威參考。雖然這個檔案中記錄了上百個不同的核心命令列引數,但這個列表不一定完整,因此,你必須參考原始碼。

       核心命令列的引數使用語法很簡單,

               Kernel  command  line:  console=ttyS0, 115200, root = /dev/nfs ip = dhcp

       可以是單個單詞,一個建/值對,或是key = value1, value2...這種一個鍵和多個值的形式。命令列是全域性可訪問的,並且可以由很多模組處理。我麼前面說的,main.c中的start_kernel()函式在掉用函式setup_arch()時會傳入核心命令列作為引數,這也是唯一的引數。通過這個呼叫,與架構相關的引數和配置就被傳遞給了那些與架構和機器程式碼相關的程式碼。

       裝置驅動程式的編寫者和核心開發人員都可以新增額外的核心命令列引數,以滿足自身的具體需求。遺憾的是,在使用和處理命令列的過程中會涉及一些複雜的內容。首先,原來的機制已棄用,取而代之的是一個更加健壯的實現機制。另外,為了完全理解這個機制,你需要理解複雜的連線指令碼檔案。

       __setup巨集

       考慮以下如何指定控制檯裝置,這可以作為一個使用核心命令列引數的例子。

       我們希望在系統引導的早期階段初始化控制檯,以便我們可以在引導過程中將訊息輸出到這個目的的裝置上。初始化工作由一個名為printk.o的核心物件完成的。這個模組的C語言原始檔位於.../kernel/printk.c中。控制檯裝置的初始化函式名為console_setup().並且這個函式只有一個引數就是核心命令列。

       現在面臨的問題是如何以一種模組化和通用的方式傳遞配置引數,我們在核心命令列中指定了控制檯相關的引數,但要將他們傳遞給需要此資料的相關設定函式和驅動程式。問題更復雜一些,因為一般情況下命令列引數在較早的時候就會用到,在模組使用他們之前或者就在此時。核心命令列主要時在main.c中處理的,但其中的啟動程式碼不可能知道對應於每個核心命令列引數的目標處理函式,因為這樣的引數有上百個。我們需要一種靈活和通用的機制,用於將核心命令列引數傳遞給它的使用者。

       檔案.../include/linux/init.h中定義了一個特殊的巨集,用於將核心命令列的字串的一部分同某個函式關聯起來,而這個函式會處理字串的那個部分。下面舉一個例子說明__setup巨集是如何使用的。

       下面這個字串是傳給核心的一個完整的命令列引數,

                  console=ttyS0, 115200

        下列程式碼清單是一個.../kernel/printk.c中的一個程式碼片段,我們省略了函式體的內容,因為這個和我們的討論無關。程式碼清單中最後以行,呼叫__setup巨集,這個巨集需兩個引數,在這裡我們傳入了一個字串字面量和一個函式指標。傳遞給__setup巨集的字串字面量是console=,這正好是核心命令列中有關控制檯引數的前八個位元組,而這並非巧合。

        設定控制檯列表,有init/main.c呼叫

        static  int  __init  console_setup(char *str)

        {

              char buf[sizeof(console_cmdline[0].name) + 4];

              char *s, *options,*brl_options = NULL;

              ...

              ....

              return 1;

        }

        __setup("console=", console_setup);

        可以將__setup巨集看作一個註冊函式,即為核心命令列中的控制檯相關引數註冊的處理函式。實際上指,當在核心命令中碰到console=字串時,就呼叫__setup巨集的第二個引數所指定的函式,這裡就是呼叫函式console_setup()。但這個資訊是如何傳遞給早期的設定程式碼的?這些程式碼在模組之外也不瞭解控制檯相關的函式。這個機制非常巧妙,並且依賴於編譯連線時生成的列表

        具體的細節隱藏在一組巨集之中,這些巨集設計用於省去繁瑣的語法規則,方便將段屬性(或其他屬性)新增到一部分目的碼中。這裡的目標是建立一個靜態列表,其中的每一項包含一個字串字面量及關聯的函式指標。這個列表由編譯器生成,存放在一個單獨命名的ELF段中,而該段是最終的ELF映象vmlinux的一部分。理解這個技術很重要,核心中很多地方都使用這個技術來完成一些特殊的處理。

        init.h中定義了__setup系列巨集(.../include/linux/init.h)

        ...

        #define  __setup_param(str, unique_id, fn, early)                      \

                static  const char __setup_str_##unique_id[ ] __initconst           \

                        __aligned(1) = str;      \

                static  struct  obs_kernel_param  __setup_##unique_id        \

                       __used  __section(.init.setup)             \

                       __attribute__((aligned((sizeof(long)))))         \

                       = {__setup_str_##unique_id, fn, early}

       #define  __setup(str, fn)

              __setup_param(str, fn, fn, 0)

       ...

       巨集呼叫時這樣使用的

               __setup("console=", console_setup);

       編譯器預處理後是下面的樣子

       static const  cahr  __setup_str_console_setup[ ]  __initconst \

       __aligned(1) = "console = ";

       static  struct  obs_kernel_param  __setup_console_setup __used  \

       __section(.init.setup)  __attribute__ ((aligned((sizeof(long)))))      \

       = { __setup_str_console_setup, console_setup, early};

       __used巨集指示編譯器生成函式或變數。__attribute__((aligned))巨集指示編譯器將結構體對齊到一個特定的記憶體邊界上--在這裡是指按sizeof(long)的長度進行對齊。將這兩個巨集去掉以簡化後:

       static  struct  obs_kernel_param  __setup_console_setup  \

       __section(.init.setup) = {__setup_str_console_setup, console_setup, early};

       首先編譯器生成一個字元陣列,__setup_str_console_setup[ ], 並將其內容初始化為console=。接著編譯器生成一個結構體其中包含三個成員:一個指向核心命令列字串(就是剛剛宣告的字元陣列)的指標,一個指向設定函式本身的指標,和一個簡單的標誌。這裡最關鍵的一點就是為結構體指定了一個段屬性(__section)。段屬性指示編譯器將結構體放在ELF物件模組的一個特殊的段中,名字為.init.setup。在連線階段所有使用__setup巨集定義的結構體都會彙集在一起,並放到.init.setup段中,實際的結果是生成了一個結構體陣列。下面程式碼是../init/main.c中的一個程式碼片段,其中顯示瞭如何訪問和使用這塊資料。

       extern  struct  obs_kernel_param  __setup_start[], __setup_end[ ];

       static  int  __init  obsolete_checksetup(char *line)

       {

              struct  obs_kernel_param *p;

              int hard_early_param = 0;

              p = __setup_start;

              do{

                    int n = strlen(p->str)

                    if(!strcmp(line,p->str,n)){

                         if(p->early) {

                               if(line[n] == '\0' || line[n] == '=')

                                     had_early_param = 1;

                               }else if(!p->setup_func) {

                                     printk(KERN_WARNING "Parameter %s is obsolete,"

                                                         "ignored\n", p->str);

                                     return 1;

                              }else if(p->setup_func(line+n))

                                     return 1;

                    }

                    p++;

              }while(p < __setup_end);

              return had_early_param;

        }

       這個函式只有一個引數就是核心命令列字串的相關部分。核心命令列字串是由檔案.../kernel/params.c中的函式解析的。在我們上述例子中,line指向字串"console=ttyS0, 115200",它是核心命令列的一部分,兩個外部結構體指標,__setup_start和__setup_end是在聯結器指令碼中定義的,這兩個標籤標記一個結構體陣列的起始和結尾,這個陣列就是放在目標檔案的.init.setup段中的型別為obs_kernel_param的結構體陣列。

       上述程式碼通過指標p掃描所有結構體,並找到一個與核心命令列引數匹配的結構體。

       這段程式碼尋找與字串console=匹配的結構體,會呼叫結構體中的函式指標成員,及呼叫函式指標成員也就是函式console_setup(),並將命令列字串的剩餘部分(即字串ttyS0,115200)傳給他,作為這個函式的唯一引數。對於核心命令列中的每個部分這個過程都要重複一次,知道處理完核心命令列的所有內容。

       剛才講述的這個技術的另一個使用是__init巨集,這些巨集用於將所有一次性的初始化函式放到物件檔案的同一個段中。還有一個類似的巨集__initconst,用於標記一次性使用的資料,在__setup巨集中就使用了它。使用這些巨集標記的初始化函式和資料會被彙集到ELF的特殊段中。當這些一次性初始化函式和資料使用完成以後核心就會釋放他們所佔據的記憶體。在系統引導接近尾聲的時候核心會輸出一條常見的訊息:Free init memory:296k;

       __setup巨集展開後生成一個obs_kernel_param的結構體,而這個結構體中包含了一個用作標誌的成員,我們要說的就是這個標誌成員的作用,early用於表示特定命令列引數是否早已在早期的引導過程中被處理了。這些命令列引數特意用於引導早期,而這個標誌提供了一種實現早期解析演算法的機制。在main.c中找到一個名為do_early_param()的函式,該函式遍歷一個由聯結器生成的陣列並進行處理,而這個陣列的每個成員都是由__setup巨集展開的,並且標記為早期處理。這允許開發人員對引導過程中引數的處理時機進行一些控制。

       子系統初始化

       很多核心子系統的初始化工作都是由main.c中的程式碼完成的。有些是通過顯式函式呼叫完成的,比如呼叫函式init_timers()和console_init(),他們需要很早就被呼叫。其他子系統需要通過一種非常類似__setup巨集的技術來初始化的,我們前面介紹過這中技術。簡單來說,就是聯結器首先構造一個函式指標的列表,其中每個指標指向一個初始化函式,然後在程式碼中使用簡單的迴圈,依次執行這些函式。

        下面是一個簡單的初始化函式,位於.../arch/arm/kernel/setup.c,該函式用某個特定的硬體板卡進行一些定製的初始化。

        static int __init  customize_machine(void)

        {

               if(init_machine)

                    init_machine();

                    return 0;

         }

         arch_initcall(customize_machine);

       *__initcall系列巨集

       上述程式碼有兩點值的我們注意,首先,函式定義使用了__init巨集,這個巨集為函式指定了section屬性,因此編譯器會將函式放在vmlinux的一個名為.init.text的段中。回想以下這樣做的目的—將這個函式放在目標檔案的一個特殊的段中,不需要這個函式的時候,核心就可以回收它所佔用的記憶體空間。

       需要注意的另外一點是緊跟在函式後面的巨集,arch_initcall(customize_machine)。檔案.../include/linux/init.h中定義了一系列的巨集。arch_initcall就是其中之一。類似於__setup巨集,這些巨集聲明瞭一個基於函式名的資料項,他們同樣也使用了section屬性,從而將這些資料項放置到vmlinux(ELF檔案格式)中的一個特別命名的段中。這種方法的好處是main.c可以呼叫任意子系統初始化函式,而不需要對此子系統有任何的瞭解。除此以外的唯一方法就是在main.c中新增核心中所有子系統的相關資訊,而這會使main.c中內容非常混亂。

       initcall系列巨集

       #define  __define_initcall(level, fn, id)   \

            static  initcall_t  __initcall_##fn##id  __used    \

            __attribute__((__section__(".initcall" "level" ".init"))) = fn

       #define  early_initcall(fn)  __define_initcall("early",fn,early)

       #define pure_initcall(fn)               __define_initcall("0", fn, 0)

       #define core_initcall(fn)               __define _initcall("1", fn, 1)

       #define core_initcall_sync(fn)         __define_initcall("1s",fn, 1s)

       #define pistcore_initcall(fn)         __define_initcall("2",fn,2)

       ;;;

       從上述程式碼可以推斷出段名是.initcallN.init。其中N是定義的級別,從1-7.其中以s結尾的是針對同步的initcall。每個巨集都定義了一個數據項(變數),巨集的引數是一個函式地址,並且這個函式地址的值被賦給了該資料項。例如

       arch_initcall(customize_machine)

       展開後會像下面

       static  initcall_t  __initcall_customize_machine = customize_machine;

       該資料項被放置在核心目標檔案的名為.initcall3.init的段中

       級別的值(N)用於對所有的初始化呼叫進行排序。使用core_initcall()巨集宣告的函式會在所有其他函式之前呼叫。根據N值來先後呼叫。

       類似於__setup巨集,可以將*_initcall系列巨集看作一組註冊函式,用於註冊核心子系統的初始化函式,他們只需要在核心啟動時執行一次,之後就沒有用了。這些巨集提供了一種機制,讓這些初始化函式在系統啟動時得以執行,並在這些函式執行完後丟棄其程式碼並回收記憶體。開發人員可以為初始化函式指定執行級別,有多達7個級別可選。因此,如果你有一個子系統依賴於另一個子系統,可以使用級別來保證子系統的初始化順序。使用grep命令在核心程式碼中搜索字串【a-z】*_initcall,你會發現這一系列的巨集使用的非常廣泛。

       關於*_initcall系列巨集,多級別是在開發2.6系列核心的過程中引入的,早期核心版本使用__initcall()巨集來達到這個目的。在裝置驅動中這個巨集使用的也特別廣泛。

       init執行緒

       檔案.../init/main.c中程式碼賦予核心“生命”。函式start_kernel()執行基本的核心初始化,並顯示呼叫一些早期初始化函式之後,就會生成第一個核心執行緒,這個執行緒就是稱為init的核心執行緒,它的程序ID是1。init是所有使用者空間linux程序的父程序。系統引導到這個時間點,有兩個顯著不同的執行緒正在執行;一個是函式start_kernel()代表的執行緒,另一個就是init執行緒。前者在完成它的工作後變成系統的空閒程序,而後者會變成init程序。

       核心init執行緒的建立

       static  noinline  void  __init_refok  rest_init(void)

              __release(kernel_lock)

       {

               int pid;

               rcu_scheduler_starting();

               kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);

               numa_default_policy();

               pid = kernel_thread(kthread, NULL,CLONE_FS|CLONE_FILES);

               kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);

               unlock_kernel();

               啟動時空閒執行緒必須至少呼叫schedule()

               一次,以便系統運作

               init_idle_bootup_task(current);

               preempt_enable_no_resched();

               schedule();

               preempt_disable();

               當搶佔禁用後呼叫cpu_idle

               cpu_idle();

      }

       函式start_kernel()呼叫函式rest_init()函式。核心的init程序是通過呼叫kernel_process()生成的,以函式kernel_init()作為其第一個引數。init會繼續完成剩餘的系統初始化,而執行函式start_kernel()的執行緒會在呼叫cpu_idle()之後進入無限迴圈。

       為什麼要採用這樣的結構呢?也許你已經發現了start_kernel()是一個相當大的函式,而且是由__init巨集標記的,這意味著在核心初始化的最後階段,它所佔用的記憶體會被回收。而在回收記憶體之前,必須要退出這個函式和它所佔的地址空間。解決的辦法是讓start_kernel()呼叫rest_init()。rest_init()這個函式相對來說小很多,它會最終變成系統的空閒程序。

       通過initcalls進行初始化

       當執行函式kernel_init的核心執行緒誕生後,它會最終呼叫do_initcalls()。我們在前邊講過,*_initcalls系列巨集可以用於註冊初始化的函式,而函式do_initcalls()則負責呼叫其中的大部分函式。

       通過initcalls進行初始化

       extern  initcall_t  __initcall_start[], __initcall_end[], __early_initcall_end[];

       static  void  __init  do_initcalls(void)

       {

               initcall_t  *fn;

               for(fn = __early_initcall_end;  fn < __initcall_end;  fn++)

                        do_one_initcall(*fn);

               flush_scheduled_work();確保initcall序列中沒有懸而未決的事情。

       }

       初始化過程由兩處程式碼比較類似,do_pre_smp_initcalls()處理從__initcall_start到__early_initcall_end的部分,而do_initcalls處理剩餘部分。這些標籤是在vmlinux的連線階段由聯結器的指令碼檔案定義的。這些標籤標記程式碼清單的開始和結尾,程式碼清單中的成員都是使用*_initcall系列巨集註冊的初始化函式,頂層核心原始碼目錄中包含一個名為system.map的檔案,從中可以看到這些標籤。

        initcall_debug

        它允許你觀察啟動過程中的函式呼叫,只需在啟動核心時設定initcall_debug,就可以看到系統輸出的相關診斷訊息。這是個檢視核心初始化細節的好辦法,特別是可以瞭解核心呼叫各個子系統和模組的順序。更有趣的是函式呼叫的持續時間,如果你關心繫統啟動的時間,通過這種方法可以確定啟動時間是在哪些地方被消耗的。

        最後的引導步驟

        在生成kernel_init()執行緒,並呼叫各個初始化函式之後,核心開始執行引導過程的最後一些步驟。這包括釋放初始化函式和資料所佔用的記憶體開啟系統控制檯裝置,並啟動第一個使用者空間程序。下面程式碼顯示了核心的init程序執行的最後一些步驟,程式碼來自main.c

        static  noinline  int  init_post(void)

               __release(kernel_lock)

        {

                  if(execute_command)

                  {

                         run_init_process(execute_command);

                         printk(KERN_WARNING“Failed  to  execute  %s.Attempting”

                                    "defaults...\n",  execute_command);

                   }

                   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_post()的最後會產生一個核心錯誤(kernel  panic),它通常是控制檯的最後一條輸出訊息。無論如何,run_init_process()命令中至少有一個必須正確無誤的執行,run_init_process()函式在成功呼叫後不會返回,他用一個新的程序覆蓋呼叫程序,實際上是用一個新的程序替換了當前程序(execve)。最常見的系統配置一般會生成/sbin/init作為使用者空間區的初始化程序。下一張詳細研究

       嵌入式開發人員可以選擇定製的使用者空間區初始化程式,。這就是前面程式碼片段中的條件語句的意圖。如果execute_comand非空,它會指向一個執行在使用者空間的字串,而這個字串中包含了一個定製的、由使用者提供的命令。開發人員在核心命令列中指定這個命令,並且它會由我們前面所研究的__setup巨集進行設定。 

/