天天看點

核心指令行處理

核心指令行處理(1) 在啟動代碼main.c執行完早期的一些核心初始化任務之後,就會顯示核心的指令行資訊。為友善起見,這裡重新列出代碼清單5-3中的第10行内容: Kernel command line: console=ttyS0,115200 ip=bootp root=/dev/nfs 在這個簡單的例子中,引導中的核心在串行裝置ttyS0(通常是第一個序列槽)上打開一個控制台,通信波特率設定為115Kbit/s。此外,它還通過一個BOOTP伺服器獲得自身的初始化IP位址,并且通過NFS協定挂載根檔案系統。(我們将在第12章講到BOOTP、在第9章和第12章中講到NFS。現在我們隻是讨論Linux核心的指令行機制。) 引導裝入程式或第二階段引導裝入程式通過一系列被稱為核心指令行的參數實作對Linux的引導。盡管在實際中并不是通過shell指令提示來調用核心,但是許多版本的引導裝入程式常常采用将參數傳遞給Linux核心這種非常流行的模式。某些平台上的引導裝入程式不能很好地識别Linux,那麼就在核心編譯時定義核心指令行參數,并且将其作為Linux核心二進制映像固件代碼的一部分。而在另一些平台(例如運作Red Hat Linux的桌面PC)中,指令行參數可以由使用者修改而不用重新編譯核心。第二階段引導裝入程式(在PC中是Grub或Lilo)通過一個配置檔案建立核心指令行并且在核心引導過程中傳遞給核心。這些指令行參數是一種引導機制,用來在給定硬體平台上設定為正确引導所需的初始化配置。 Linux在整個核心中定義了大量的指令行參數。在Linux源碼中的.../Documentation子目錄中有一個名為kernel-parameters.txt檔案,該檔案包含了Linux核心指令行參數清單,它們按字母順序依次列出。前面提到關于核心文檔的警告資訊:核心的變化要快于核心文檔的變化,是以,可以以該檔案為向導,但它并不是一個最權威的參考。在kernel-parameters.txt檔案中有超過400多個核心指令行參數,但這并不是所有的核心指令行參數,是以必須直接查閱源代碼。 Linux核心指令行參數的基本文法比較簡單,大部分從代碼清單5-3第10行裡很容易看到。核心指令行參數的形式可以是單個單詞、key=value對或key= value1, value2, …等複合形式。通過使用這些資訊進行資料傳遞,所有的指令行都是可用的并且可以由許多的子產品來處理。前面提到的main.c中的setup_arch()函數就是通過核心指令行參數調用的。通過這種調用,可以向體系結構級或硬體平台級相關代碼中傳遞參數和配置指令。 裝置驅動程式編寫者和核心開發者都可以為他們特定的需要而增加相應的指令行參數。我們來看一下這種方式的實作機制。遺憾的是,在處理這些核心指令行參數的時候會涉及一些複雜的因素,首先就是原先的機制将受到抑制以便實作更為健壯的系統。第二個難點是我們需要掌握複雜的連結腳本以全面了解這種實作機制 。 __setup宏 可以考慮将控制台裝置作為使用核心指令行參數的一個例子。我們希望該裝置在核心引導的早期階段就初始化,這樣在引導過程中控制台資訊就可以通過該裝置輸出,該初始化過程建立在名為printk.o的核心目标檔案中,其C源代碼位于.../kernel/printk.c。執行控制台初始化的函數是console_setup(),該函數将核心指令行的參數作為其唯一的參數。 配置程式和裝置驅動程式與在核心指令行中所指定控制台參數進行通信的難點,在于要求該參數是标準通用的模式。該情形更複雜的情況是,指令行參數在那些子產品調用它們之前(或就在此時)就要用到。在檔案main.c中的啟動代碼裡,在核心指令行進行主要處理的位置,如果沒有每一個參數的使用資訊,就不可能知道這幾百個核心指令行參數中每一個參數的目标函數,是以需要用一種靈活通用的方法将核心指令行參數傳遞給其使用者。 對于Linux 2.4或更早的版本,開發者通過使用一個簡單的宏來解決上述問題。盡管沒有得到重視,但是__setup宏仍然在整個Linux核心中得到了廣泛使用。在後續内容中,我們會使用代碼清單5-3中的核心指令行來示範__setup是如何工作的。 從代碼清單5-3的第10行可知,下面的内容即是第一個傳遞給核心的完整的指令行參數: console=ttyS0,115200 引用該例子的真正目的并不在于指令行參數的實際含義,而在于說明其工作機制,是以如果你沒有了解該參數或參數值并不要緊。 代碼清單5-4的内容是.../kernel/printk.c中的一部分代碼,其中去掉了函數的主體部分,因為它與這裡讨論的内容無關,我們關心的隻是在代碼清單5-4中列出的内容,即對__setup的宏調用。__setup宏在這裡有兩個參數:一個字元串參數和一個函數指針。傳遞給__setup宏的字元串與第一個與核心指令行相關的8字元的參數console=一緻是絕非偶然的。 代碼清單5-4 控制台設定部分代碼 static int __init console_setup(char *str) { char name[sizeof(console_cmdline[0].name)]; char*s, *options; int idx; return 1; } __setup("console=", console_setup); 你可以将__setup宏看作是核心指令行控制台參數在核心中的注冊函數。當字元串資訊console=出現在核心指令行時,就通過__setup宏的第2個參數調用函數console_setup()。但是在并不知道控制台功能的情況下,這個子產品之外的配置代碼是如何擷取該資訊呢?事實上,其實作機制巧妙而複雜,并且依賴于目标連結器所建立的清單。 真正的細節隐藏于一系列的宏當中,這些宏通過在一部分目标代碼中增加段屬性(或其他屬性)用來隐藏。目标檔案會聯合函數指針(function pointer)依字母順序建立一個靜态清單,該清單會由最終vmlinux ELF映像中一個獨立ELF段的編譯器發出。了解上述技術細節非常重要,它在核心中許多進行特殊處理的地方都要用到。 我們來看看對于__setup宏這是如何實作的。代碼清單5-5是定義了__setup宏系列的頭檔案.../include/linux/init.h下的部分内容。 代碼清單5-5 init.h下的_setup 宏的定義 ... #define __setup_param(str, unique_id, fn, early) / static char __setup_str_##unique_id[] __initdata = str; / static struct obs_kernel_param __setup_##unique_id / __attribute_used__ / __attribute__((__section__(".init.setup"))) / __attribute__((aligned((sizeof(long))))) / = { __setup_str_##unique_id, fn, early } #define __setup_null_param(str, unique_id) / __setup_param(str, unique_id, NULL, 0) #define __setup(str, fn) / __setup_param(str, fn, fn, 0) ...

核心指令行處理(2)

代碼清單5-5是文法乏味的定義。回想代碼清單5-4,我們最初所調用的__setup宏的形式如下:

  1. __setup("console=", console_setup); 

經過稍稍簡化,編譯器在宏擴充後,其預處理器産生如下結果:

  1. static char __setup_str_console_setup[] __initdata = "console=";  
  2. static struct obs_kernel_param __setup_console_setup  /  
  3. __attribute__((__section__(".init.setup")))=  
  4.    {__setup_str_console_setup, console_setup, 0}; 

為了增加可讀性,将上述結果的第2行和第3行采用UNIX的行續符"/"分隔開來。

我們故意略去了兩個和本次讨論内容無關的編譯器屬性。簡要地說,__attribute_ used__(本身就是一個隐藏了很多文法細節的宏)會告訴編譯器發出一個函數或變量,即使在編譯過程中并沒有用到任何優化參數 。__attribute__(aligned)會告訴編譯器按照特定的邊界來對齊結構,在本例中是sizeof(long)。

簡化處理後剩下的就是這種機制的核心部分。首先,編譯器會産生名為__setup_str_ console_setup[]的初始化後字元數組,該數組包含console=字元串資訊;其次,編譯器會産生一個包含三個成員的結構:指向核心指令行字元串(在字元數組中聲明)的指針、指向配置函數本身的指針和一個簡單的辨別。這裡的關鍵在于依附于結構的段屬性,該屬性會通知編譯器将該結構送到ELF目标子產品内名為.init.setup的特殊段中。在這個連結階段,所有由__setup宏定義的結構一起被放置到這個.init.setup段中,實際結果就是建立了一個包含這些結構的數組。代碼清單5-6是.../init/main.c中的一部分内容,它們說明了這個資料是如何擷取和使用的。

代碼清單5-6 核心指令行處理

  1. 1 extern struct obs_kernel_param __setup_start[], __setup_end[];  
  2. 2  
  3. 3 static int __init obsolete_checksetup(char *line)  
  4. 4 {  
  5. 5         struct obs_kernel_param *p;  
  6. 6  
  7. 7         p = __setup_start;  
  8. 8         do {  
  9. 9                 int n = strlen(p->str);  
  10. 10                 if (!strncmp(line, p->str, n)) {  
  11. 11                         if (p->early) {  
  12. 12                                  /* Already done in parse_early_param? (Needs  
  13. 13                                   * exact match on param part) */  
  14. 14                                  if (line[n] == '/0' || line[n] == '=')  
  15. 15                                           return 1;  
  16. 16                         } else if (!p->setup_func) {  
  17. 17                             printk(KERN_WARNING "Parameter %s is obsolete,"  
  18. 18                                     " ignored/n", p->str);  
  19. 19                                  return 1;  
  20. 20                         } else if (p->setup_func(line + n))  
  21. 21                                  return 1;  
  22. 22                }  
  23. 23                p++;  
  24. 24        } while (p < __setup_end);  
  25. 25        return 0;  
  26. 26 } 

對該段代碼解釋還算簡單。函數由一個在main.c檔案中其他地方解析的單指令行參數調用。在這個例子中,我們要讨論的指針line指向字元串console=ttyS0,115200,它是核心指令行的一個組成部分。兩個外部結構指針__setup_start和__setup_end是在一個連結腳本文本檔案中定義的,而不是定義在C檔案或頭檔案中。對于obs_kernel_param結構數組用來标記該數組起始和結束的标簽則存在于目标檔案的.init.setup段中。

在代碼清單5-6中,通過指針p對這個特殊的核心指令行參數尋找比對資訊的過程,對整個結構都進行了掃描。具體在本例中,代碼要為字元串資訊console=尋找比對資訊,在這個相關的結構中,函數傳回一個指向console_setup()函數的指針,它會以該參數(字元串ttyS0,115200)作為其唯一的函數參數,這一處理過程會在核心指令行處理完畢之前不停地重複。

采用所描述的這種機制将目标對象存放到ELF段的清單中,這種機制在核心中的許多地方都用到了。另一個采用這種機制的例子是,使用__init宏系列将初始化程式放到目标檔案中一個普通的段中。與其很相近的__initdata被__setup宏用來标記為隻在初始化過程中用到的資料。使用這些宏标記的初始化函數和資料被集中放到ELF段中,接下來,當使用了這些用來初始化的函數和資料之後,核心會釋放之前它們所占用的記憶體空間。你也許在引導過程的最後階段看到過類似的核心資訊:"Freeing init memory: 296K."。不同使用者對這些函數和資料的使用可能不盡相同,但是如果有三分之一兆,就值得使用__init宏系列,這也恰恰就是使用前面聲明的__setup_str_console_setup[]數組裡的__initdata宏的目的所在。

你也許會對代碼清單5-6中的obsolete_符号感到迷惑,這是因為核心開發者正在用一種更通用的機制來代替核心指令行處理機制,以實作對引導時間和可加載子產品參數的注冊。在目前情況下,__setup宏聲明了幾百個參數,然而在新的開發中希望使用核心頭檔案.../include/ linux/moduleparam.h中定義的一系列函數來實作,更值得注意的是使用module_param*宏系列。這些内容将在第8章中介紹裝置驅動程式的時候詳細介紹。

上面所說的這種新機制通過在解析程式中包含一個未知的函數指針參數進而保持了向後相容性,是以,對于module_param*結構來說,是未知的參數就會被視為未知參數,并且對指令行的處理過程就在開發者的控制下重新回到了原有的機制。在仔細研究../kernel/params.c中的代碼和.../init/main.c中的parse_args()調用後就可以對這一過程有很好的了解。

對于由__setup宏所建立的結構obs_kernel_param,其中标志(flag)成員的用途是最後要注意的内容。仔細研究代碼清單5-6就會明白。該結構中稱為early的标志用來訓示這個特定的核心指令行參數是否會在引導過程中預先使用,一些指令行參數就是特意要在引導過程中提前用到,那麼在這種情況下的标志就會為提前解析指令行參數提供一種實作機制。你會在main.c代碼中看到一個名為do_early_param()的函數,該函數會周遊數組,該數組是__setup宏結構由目标連結器産生的,同時該函數會處理每一個被标記為預先使用的核心指令行參數,在引導過程執行這一處理操作時給開發者一些控制權。

子系統初始化

許多Linux子系統的初始化代碼都可在main.c中找到。一些子系統的初始化代碼在main.c中顯而易見,如對init_timers()和console_init()的調用,它們在初始化過程之初就要調用。另外一些子系統所采用的初始化機制與前面所提到的__setup宏非常類似,簡單地講,目标代碼連結器會為不同的初始化程式建立函數指針清單,同時采用簡單的循環機制依次執行。代碼清單5-7顯示了這一過程。

代碼清單5-7 初始化程式示例

  1. static int __init customize_machine(void)  
  2. {  
  3.     /* customizes platform devices, or adds new ones */  
  4.     if (init_machine)  
  5.         init_machine();  
  6.     return 0;  
  7. }  
  8. arch_initcall(customize_machine); 

這部分代碼來源于.../arch/arm/kernel/setup.c,它是為一個特殊開發闆提供使用者定制的簡單程式。

*_initcall宏

對于代碼清單5-7中的初始化程式,有兩個要點需要注意。首先,程式中的函數是由__init宏定義的,就像在前面看到的。__init宏将該函數放到了vmlinux ELF檔案中一個稱為.init.text的段中,我們可以想到将一個函數放到目标檔案中一個特殊段中的目的,這是為了當函數不再使用後可以将函數所占用的記憶體空間釋放。

第二個需要注意的事情是在函數定義之後的宏,即arch_initcall(customize_machine),該宏是在.../include/linux/init.h中所定義的一系列宏中的一個。這些宏如代碼清單5-8所示。

代碼清單5-8 initcall宏系列

  1. #define __define_initcall(level,fn) /  
  2.     static initcall_t __initcall_##fn __attribute_used__ /  
  3.     __attribute__((__section__(".initcall" level ".init"))) = fn  
  4. #define core_initcall(fn)         __define_initcall("1",fn)  
  5. #define postcore_initcall(fn)     __define_initcall("2",fn)  
  6. #define arch_initcall(fn)         __define_initcall("3",fn)  
  7. #define subsys_initcall(fn)       __define_initcall("4",fn)  
  8. #define fs_initcall(fn)           __define_initcall("5",fn)  
  9. #define device_initcall(fn)       __define_initcall("6",fn)  
  10. #define late_initcall(fn)         __define_initcall("7",fn) 

__initcall宏與前面介紹的__setup宏在形式上非常相似,這些宏基于函數名聲明了一個資料清單,并且使用段屬性将這些資料内容放到vmlinux ELF檔案中被唯一命名的段中。這樣做的好處是,main.c可以任意調用其并不知道的子系統初始化程式,如果不這樣做,那麼唯一的方法就是采用前面描述的方法,即隻能改寫main.c中的相關内容,讓核心了解每一個子系統。

如代碼清單5-8所示,這些段的名稱為.initcallN.init,這裡的N表示的是數量1~7,資料被配置設定到由宏命名的函數位址處。在代碼清單5-7和代碼清單5-8所示的例子中,資料的配置設定形式如下(為了簡化起見,省去了段屬性):

  1. static initcall_t __initcall_customize_machine = customize_machine; 

該資料被放到核心目标檔案中的一個名為.initcall1.init的段中。

這裡的N用來提供初始化調用的順序關系,比如使用core_initcall()宏聲明的函數在其他所有函數之前被調用,使用postcore_initcall()宏聲明的函數在其後被調用,依次類推,使用late_initcall()宏聲明的初始化函數在最後被調用。

和__setup宏系列非常類似,*_initcall宏系列可以看作是核心子系統初始化程式的注冊函數,而且這些初始化程式也是在核心啟動後就要執行,且執行後不再使用。這些宏提供了一種機制,以實作在系統啟動過程中可以執行初始化程式,并且在程式執行之後将程式丢棄同時回收記憶體。在執行初始化程式的時候也為開發者提供了7種不同的級别,是以,如果一個子系統依賴于另一個子系統可用,那麼就可以使用這些級别來提高它的執行順序。如果使用grep指令查找核心中的[a-z]*_initcall字元串資訊,就會發現這些系列的宏在核心中使用非常廣泛。

對于*_initcall系列的宏,最後要注意的是:多級别的用法在Linux 2.6核心的開發過程中引入,早期版本的核心是用__initcall()宏來實作的,目前__initcall()宏仍然在廣泛使用中,尤其是在裝置驅動程式中。為了保持向後相容性,已經将__initcall()宏定義為device_initcall(),這是一個級别為6的initcall。

5.5 init線程

.../init/main.c中的内容主要用來實作核心的運轉。在start_kernel()函數通過調用一些初始化函數執行一些基本的核心初始化任務之後,就産生了第一個核心線程。該線程最終成為核心的init()線程,其線程ID号(PID)為1。可以知道,init()就成為使用者空間中所有Linux程序的父程序。在引導過程中運作着兩個截然不同的線程:一個是前面提到的start_kernel();另一個就是現在的init()。前者在完成自身的任務之後最終成為idle程序,而後者稱為init程序,如代碼清單5-9所示。

代碼清單5-9 核心init線程的建立

  1. static void noinline rest_init(void)  
  2.         __releases(kernel_lock)  
  3. {  
  4.         kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);  
  5.         numa_default_policy();  
  6.         unlock_kernel();  
  7.         preempt_enable_no_resched();  
  8.         /*  
  9.          * The boot idle thread must execute schedule()  
  10.          * at least one to get things moving:  
  11.          */  
  12.         schedule();  
  13.         cpu_idle();  

從代碼清單5-9可以看出,start_kernel()函數調用了rest_init(),通過調用kernel_thread().init來産生核心的init程序,以繼續完成核心其餘的初始化任務,而由start_kernel()開始的線程在調用cpu_idle()的過程中不停地重複執行。

這樣的結果非常有趣。你或許也注意到這個相當龐大的start_kernel()函數被__init宏所标記,這意味着它所占用的記憶體空間将在核心初始化的最後階段被釋放。在釋放記憶體之前需要退出該函數和它所占用的位址空間,這是通過start_kernel()調用rest_init()來實作的,如代碼清單5-9所示,一段非常小的記憶體空間處在了空閑狀态。

5.5.1 通過initcall初始化

當建立init()之後,它會調用do_initcalls()函數,而do_initcalls()函數是用來調用所有被*_initcall宏系列所注冊的初始化函數的,其實作代碼如代碼清單5-10所示。

代碼清單5-10 使用initcalls初始化

  1. static void __init do_initcalls(void)  
  2. {  
  3.     initcall_t *call;  
  4.     for( call = &__initcall_start; call < &__initcall_end; call++) {  
  5.         if (initcall_debug) {  
  6.             printk(KERN_DEBUG "Calling initcall 0x%p", *call);  
  7.             print_symbol(":%s()", (unsigned long) *call);  
  8.             printk("/n");  
  9.         }  
  10.         (*call)();  

除了兩個用于訓示循環範圍的标簽__initcall_start和__initcall_end之外,該段代碼很好了解。在C源代碼和頭檔案中不會看到這樣的标簽,它們是在vmlinux連結階段所用的連結腳本檔案中定義的,用來表示使用*_initcall宏系列所生成的初始化函數清單的起始和結束位置。你可以在Linux核心頂層目錄下的System.map檔案中看到每一個這樣的标簽,這些标簽以字元串__initcall開始,就像代碼清單5-8中所表示的那樣。

你如果對do_initcalls()函數中的調試列印資訊感到疑惑的話,可以看一下由在引導過程中設定的核心指令行參數initcall_debug所執行的系統調用,該指令行參數允許列印如代碼清單5-10所示的調試資訊。核心隻需簡單地以核心指令行參數initcall_debug開始就可以實作這些調試資訊的輸出 。

下面是一個啟用了這些調試語句時的輸出的例子:

  1. ...  
  2. Calling initcall 0xc00168f4: tty_class_init+0x0/0x3c()  
  3. Calling initcall 0xc000c32c: customize_machine+0x0/0x2c()  
  4. Calling initcall 0xc000c4f0: topology_init+0x0/0x24()  
  5. Calling initcall 0xc000e8f4: coyote_pci_init+0x0/0x20()  
  6. PCI: IXP4xx is host  
  7. PCI: IXP4xx Using direct access for memory space  
  8. ... 

注意在代碼清單5-7中對customize_machine()的調用,調試資訊的輸出包括了函數的虛拟核心位址(在該例中是0xc000c32c)和函數大小(在這裡是0x2c)。這是了解核心初始化的一個有效方法,特别是對不同子系統和子產品的調用次序的了解。即使是在一個具有相當配置的嵌入式系統之上,也有幾十個這樣的初始化函數通過這種方式調用。在這個以嵌入式ARM XScale為平台的例子中,共有92個這樣不同的核心初始化程式。

5.5.1 通過initcall初始化

當建立init()之後,它會調用do_initcalls()函數,而do_initcalls()函數是用來調用所有被*_initcall宏系列所注冊的初始化函數的,其實作代碼如代碼清單5-10所示。

代碼清單5-10 使用initcalls初始化

  1. static void __init do_initcalls(void)  
  2. {  
  3.     initcall_t *call;  
  4.     for( call = &__initcall_start; call < &__initcall_end; call++) {  
  5.         if (initcall_debug) {  
  6.             printk(KERN_DEBUG "Calling initcall 0x%p", *call);  
  7.             print_symbol(":%s()", (unsigned long) *call);  
  8.             printk("/n");  
  9.         }  
  10.         (*call)();  

除了兩個用于訓示循環範圍的标簽__initcall_start和__initcall_end之外,該段代碼很好了解。在C源代碼和頭檔案中不會看到這樣的标簽,它們是在vmlinux連結階段所用的連結腳本檔案中定義的,用來表示使用*_initcall宏系列所生成的初始化函數清單的起始和結束位置。你可以在Linux核心頂層目錄下的System.map檔案中看到每一個這樣的标簽,這些标簽以字元串__initcall開始,就像代碼清單5-8中所表示的那樣。

你如果對do_initcalls()函數中的調試列印資訊感到疑惑的話,可以看一下由在引導過程中設定的核心指令行參數initcall_debug所執行的系統調用,該指令行參數允許列印如代碼清單5-10所示的調試資訊。核心隻需簡單地以核心指令行參數initcall_debug開始就可以實作這些調試資訊的輸出 。

下面是一個啟用了這些調試語句時的輸出的例子:

  1. ...  
  2. Calling initcall 0xc00168f4: tty_class_init+0x0/0x3c()  
  3. Calling initcall 0xc000c32c: customize_machine+0x0/0x2c()  
  4. Calling initcall 0xc000c4f0: topology_init+0x0/0x24()  
  5. Calling initcall 0xc000e8f4: coyote_pci_init+0x0/0x20()  
  6. PCI: IXP4xx is host  
  7. PCI: IXP4xx Using direct access for memory space  
  8. ... 

注意在代碼清單5-7中對customize_machine()的調用,調試資訊的輸出包括了函數的虛拟核心位址(在該例中是0xc000c32c)和函數大小(在這裡是0x2c)。這是了解核心初始化的一個有效方法,特别是對不同子系統和子產品的調用次序的了解。即使是在一個具有相當配置的嵌入式系統之上,也有幾十個這樣的初始化函數通過這種方式調用。在這個以嵌入式ARM XScale為平台的例子中,共有92個這樣不同的核心初始化程式。

繼續閱讀