經歷了跟體系結構密切相關的匯編代碼之后,就可以進入C語言編寫的結構無關的代碼了。
這個入口的函數是start_kernel函數,它主要更進一步地初始化系統相關的內容,以便系統進入一種服務狀態,提供一種虛擬機的服務,提供各種API調用的服務。
在start_kernel函數里,需要非常注意的是里面初始化函數的順序,這些初始化函數不能隨便調換初始化順序,否則就會導致系統運行出錯。
由于這個函數的內容非常多,涉及的內容也非常廣泛,每個函數都有一個比較大的概念,一種原理,一種想法。
因此,對于這個函數的學習需要很多時間,需要有漫長學習的心理準備。
由于本書基于ARM體系的一種結構學習,其它與此體系結構無關的代碼,就不再分析介紹。好了,現在就來開始學習第一節的內容,代碼如下:
asmlinkage void __init start_kernel(void){ char * command_line; extern const struct kernel_param __start___param[], __stop___param[];
看了這段代碼,首先發現asmlinkage和__init與一般開發C語言的應用程序有著明顯的差別,導致看不懂這兩個宏到底是用來做干什么用的。
其實這兩個宏是寫內核代碼的一種特定表示,一種盡可能快的思想表達,一種盡可能占用空間少的思路。
asmlinkage是一個宏定義,它的作用主要有兩個,一個是讓傳送給函數的參數全部使用棧式傳送,不用寄存器來傳送。
因為寄存器的個數有限,使用棧可以傳送更多的參數,比如在X86的CPU里只能使用6個寄存器傳送,只能傳送4個參數,而使用棧就沒有這種限制;另外一個用處是聲明這個函數是給匯編代碼調用的。
不過在ARM體系里,并沒有使用棧傳送參數的特性,原因何在?由于ARM體系的寄存器個數比較多,多達13個,這樣絕大多數的函數參數都可以通過寄存器來傳送,達到高效的目標。
因此,看到文件./include/linux/linkage.h里的asmlinkage宏定義如下:
#include #include #ifdef __cplusplus#define CPP_ASMLINKAGE extern "C"#else#define CPP_ASMLINKAGE#endif#ifndef asmlinkage#define asmlinkage CPP_ASMLINKAGE#endif#ifndef asmregparm# define asmregparm#endif
在這里可以看到asmlinkage,其實沒有定義,所以ARM體系里還是通過寄存器來傳送參數的。如果看一下X86下的代碼,就會定義如下:
#ifdef CONFIG_X86_32#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
這里定義asmlinkage為通過棧傳送參數。
接著來看另外一個宏定義__init,這個宏定義主要用來標志這個函數編譯出來的目標代碼放在那一段里。
對于應用程序的編譯和連接,不需要作這樣的考慮,但是對于內核代碼來說,就需要了,因為不同的段代碼有著不同的作用,比如初始化段的代碼,當系統運行正常以后,這段代碼就沒有什么用了,聰明的做法就是回收這段代碼占用的內存,讓內核的代碼占最少的內存。
還有另外一個作用,比如同一段的代碼都是編譯在一起,讓相關聯的代碼盡可能同在一片內存里,這樣當CPU加載代碼到緩存時,就可以一起命中,提高緩存的命中率,這樣就大大提高代碼的執行速度。
宏__init定義在文件./include/linux/init.h里,代碼如下:
/* These are for everybody (although not all archs will actually discard it in modules) */#define __init __section(.init.text) __cold notrace
使用這個宏聲明的函數,編譯時就會把目標代碼放到段.init.text里,這段都是放置初始化的代碼。
最后看到聲明一個字符的指針command_line,這個指針是指向命令行參數的指針,主要用來指向引導程序傳送給內核的命令行參數,在后面的函數setup_arch和函數setup_command_line就會對它進行處理。
smp_setup_processor_id();
緊跟參數后面的,就是調用函數smp_setup_processor_id()了,這個函數主要作用是獲取當前正在執行初始化的處理器ID。
如果仔細地閱讀完初始化函數start_kernel,就會發現里面還有調用smp_processor_id()函數,這兩個函數都是獲取多處理器的ID,為什么會需要兩個函數呢?
其實這里有一個差別的,smp_setup_processor_id()函數可以不調用setup_arch()初始化函數就可以使用,而smp_processor_id()函數是一定要調用setup_arch()初始化函數后,才能使用。
smp_setup_processor_id()函數是直接獲取對稱多處理器的ID,而smp_processor_id()函數是獲取變量保存的處理器ID,因此一定要調用初始化函數。
由于smp_setup_processor_id()函數不用調用初始化函數,可以放在內核初始化start_kernel函數的最前面使用,而函數smp_processor_id()只能放到setup_arch()函數調用的后面使用了。
smp_setup_processor_id()函數每次都要中斷CPU去獲取ID,這樣效率比較低。在這個函數里,還需要懂得另外一個概念,就是對稱多處理器(SymmetricalMulti-Processing)。
由于單處理器的頻率已經慢慢變得不能再高了,那么處理器的計算速度還要提高,還有別的辦法嗎?這樣自然就想到多個處理器的技術。
這就好比物流公司,有很多貨只讓一輛卡車在高速公路上來回運貨,這樣車的速度已經最快了,運的貨就一定了,不可能再多得去。
那么物流公司想提高運貨量,那只能多顧用幾臺卡車了,這樣運貨量就比以前提高了。處理器的制造廠家自然也想到這樣的辦法,就是幾個處理器放到一起,這樣就可以提高處理速度。
接著下來的問題又來,那么這幾個處理器怎么樣放在一起,成本最低,性能最高。考慮到這樣的一種情況,處理器只有共享主內存、總線、外圍設備,其它每個處理器是獨立的,這樣可以省掉很多外圍硬件成本。
當然所有這些處理器還共享一個操作系統,這樣的結構就叫做對稱多處理器(SymmetricalMulti-Processing)。在對稱多處理器系統里,所有處理器只有在初始化階段處理有主從之分,到系統初始化完成之后,大家是平等的關系,沒有主從處理器之分了。
在內核里所有以smp開頭的函數都是處理對稱多處理器相關內容的,對稱多處理器與單處理器在操作系統里,主要區別是引導處理器與應用處理器,每個處理器不同的緩存,中斷協作,鎖的同步。因此,在內核初始化階段需要區分,在與緩存同步數據需要處理,在中斷方面需要多個處理協作執行,在多個進程之間要做同步和通訊。如果內核只是有單處理器系統,smp_setup_processor_id()函數是是空的,不必要做任保的處理。
/* * Need to run as early as possible, to initialize the * lockdep hash: */ lockdep_init();
這個函數主要作用是初始化鎖的狀態跟蹤模塊。由于內核大量使用鎖來進行多進程、多處理器的同步操作,那么死鎖就會在代碼不合理時出現,這時要知道那個鎖造成的,真是比較困難的。
遇到這種情況,就需要想辦法知道那個鎖造成的,因此就需要跟蹤鎖的使用狀態,以便發現出錯時,把鎖的狀態打印出來。
造成死鎖的情況有很多,主要有以下幾種:
1. 同一個進程遞歸地加鎖同一把鎖。
2. 同一把鎖在兩次中斷里加鎖。
3. 幾把鎖形成一個閉環死鎖。
debug_objects_early_init();
這個函數主要作用是對調試對象進行早期的初始化,其實就是HASH鎖和靜態對象池進行初始化。
/* * Set up the the initial canary ASAP: */ boot_init_stack_canary(); cgroup_init_early();
cgroup_init_early()這個函數主要作用是控制組進行早期的初始化。
什么叫控制組(controlgroups)呢?簡單地說,控制組就是定義一組進程具有相同資源的占有程度。
比如可以指定一組進程使用CPU為30%,磁盤IO為40%,網絡帶寬為50%。因此通過控制組就可以把所有進程分配不同的資源。
可以參考這個文檔(http://docs.redhat.com/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/ch01.html)。
local_irq_disable();
這個函數主要作用是關閉當前CPU的所有中斷響應。在ARM體系里主要就是對CPSR寄存器進行操作。
early_boot_irqs_off();
這個函數主要作用是標記內核還在早期初始化代碼階段,并且中斷在關閉狀態,如果有任何中斷打開或請求中斷的事情出現,都是會提出警告,以便跟蹤代碼錯誤情況。
早期代碼初始化結束之后,就會調用函數early_boot_irqs_on來設置這個標志為真。
/* * Interrupts are still disabled. Do necessary setups, then * enable them */ tick_init();
這個函數主要作用是初始化時鐘事件管理器的回調函數,比如當時鐘設備添加時處理。
在內核里定義了時鐘事件管理器,主要用來管理所有需要周期性地執行任務的設備。
boot_cpu_init();
這個函數主要作用是設置當前引導系統的CPU在物理上存在,在邏輯上可以使用,并且初始化準備好。
在多CPU的系統里,內核需要管理多個CPU,那么就需要知道系統有多少個CPU,在內核里使用cpu_present_map位圖表達有多少個CPU,每一位表示一個CPU的存在。
如果是單個CPU,就是第0位設置為1。
雖然系統里有多個CPU存在,但是每個CPU不一定可以使用,或者沒有初始化,在內核使用cpu_online_map位圖來表示那些CPU可以運行內核代碼和接受中斷處理。
隨著移動系統的節能需求,需要對CPU進行節能處理,比如有多個CPU運行時可以提高性能,但花費太多電能,導致電池不耐用,需要減少運行的CPU個數,或者只需要一個CPU運行。
這樣內核又引入了一個cpu_possible_map位圖,表示最多可以使用多少個CPU。
在本函數里就是依次設置這三個位圖的標志,讓引導的CPU物理上存在,已經初始化好,最少需要運行的CPU。
page_address_init();
這個函數主要作用是初始化高端內存的映射表。
在這里引入了高端內存的概念,那么什么叫做高端內存呢?為什么要使用高端內存呢?其實高端內存是相對于低端內存而存在的,那么先要理解一下低端內存了。
在32位的系統里,最多能訪問的總內存是4G,其中3G空間給應用程序,而內核只占用1G的空間。
因此,內核能映射的內存空間,只有1G大小,但實際上比這個還要小一些,大概是896M,另外128M空間是用來映射高端內存使用的。
因此0到896M的內存空間,就叫做低端內存,而高于896M的內存,就叫高端內存了。
如果系統是64位系統,當然就沒未必要有高端內存存在了,因為64位有足夠多的地址空間給內核使用,訪問的內存可以達到10G都沒有問題。
在32位系統里,內核為了訪問超過1G的物理內存空間,需要使用高端內存映射表。
比如當內核需要讀取1G的緩存數據時,就需要分配高端內存來使用,這樣才可以管理起來。
使用高端內存之后,32位的系統也可以訪問達到64G內存。
在移動操作系統里,目前還沒有這個必要,最多才1G多內存。
printk(KERN_NOTICE "%s", linux_banner);
這行代碼主要作用是在輸出終端上顯示版本信息、編譯的電腦用戶名稱、編譯器版本、編譯時間。如下所示:
Linuxversion 2.6.37+ (tony@tony-desktop) (gcc version 4.3.3 (Sourcery G++Lite 2009q1-203) ) #40 Tue Mar 20 17:49:58 CST 2012
linux_banner是在文件kernel/init/version.c里定義,這個字符串是由編譯腳本自動生成。
setup_arch(&command_line);
這個函數主要作用是對內核架構進行初始化。
再次獲取CPU類型和系統架構,分析引導程序傳入的命令行參數,進行頁面內存初始化,處理器初始化,中斷早期初始化等等。
mm_init_owner(&init_mm, &init_task);
這個函數主要作用是設置最開始的初始化任務屬于init_mm內存。在ARM里,這個函數為空。
setup_command_line(command_line);
這個函數主要作用是保存命令行,以便后面可以使用。
setup_nr_cpu_ids();
這個函數主要作用是設置最多有多少個nr_cpu_ids結構。
setup_per_cpu_areas();
這個函數主要作用是設置SMP體系每個CPU使用的內存空間,同時拷貝初始化段里數據。
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
這個函數主要作用是為SMP系統里引導CPU進行準備工作。在ARM系統單核里是空函數。
build_all_zonelists(NULL);
這個函數主要作用是初始化所有內存管理節點列表,以便后面進行內存管理初始化。
page_alloc_init();
這個函數主要作用是設置內存頁分配通知器。
printk(KERN_NOTICE "Kernel command line: %s ", boot_command_line);
這行代碼主要作用是輸出命令參數到顯示終端。
parse_early_param();
這個函數主要作用是分析命令行最早使用的參數。
parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption);
這行代碼主要對傳入內核參數進行解釋,如果不能識別的命令就調用最后參數的函數。
/* * These use large bootmem allocations and must precede * kmem_cache_init() */ pidhash_init();
這個函數是進程ID的HASH表初始化,這樣可以提供通PID進行高效訪問進程結構的信息。
LINUX里共有四種類型的PID,因此就有四種HASH表相對應。
vfs_caches_init_early();
這個函數是虛擬文件系統的緩存初始化。
sort_main_extable();
這個函數是對內核內部的異常表進行堆排序,以便加速訪問。
trap_init();
這個函數是對異常進行初始化,在ARM系統里是空函數,沒有任何的初始化。
mm_init();
這個函數是標記那些內存可以使用,并且告訴系統有多少內存可以使用,當然是除了內核使用的內存以外。
/* * Set up the scheduler prior starting any interrupts (such as the * timer interrupt). Full topology setup happens at smp_init() * time - but meanwhile we still have a functioning scheduler. */ sched_init();
這個函數主要作用是對進程調度器進行初始化,比如分配調度器占用的內存,初始化任務隊列,設置當前任務的空線程,當前任務的調度策略為CFS調度器。
/* * Disable preemption - early bootup scheduling is extremely * fragile until we cpu_idle() for the first time. */ preempt_disable();
這個函數主要作用是關閉優先級調度。由于每個進程任務都有優先級,目前系統還沒有完全初始化,還不能打開優先級調度。
if (!irqs_disabled()) { printk(KERN_WARNING "start_kernel(): bug: interrupts were " "enabled *very* early, fixing it "); local_irq_disable(); }
這段代碼主要判斷是否過早打開中斷,如果是這樣,就會提示,并把中斷關閉。
rcu_init();
這個函數是初始化直接讀拷貝更新的鎖機制。
RCU主要提供在讀取數據機會比較多,但更新比較的少的場合,這樣減少讀取數據鎖的性能低下的問題。
radix_tree_init();
這個函數是初始化radix樹,radix樹基于二進制鍵值的查找樹。
/* init some links before init_ISA_irqs() */ early_irq_init();
init_IRQ();
這個函數是初始化中斷相關的工作,主要初始化中斷描述數組,然后調用每個CPU架構中斷初始化。
prio_tree_init();
這個函數是初始化優先搜索樹,主要用在內存反向搜索方面。
init_timers();
這個函數是主要初始化引導CPU的時鐘相關的數據結構,注冊時鐘的回調函數,當時鐘到達時可以回調時鐘處理函數,最后初始化時鐘軟件中斷處理。
hrtimers_init();
softirq_init();
這個函數是初始化軟件中斷,軟件中斷與硬件中斷區別就是中斷發生時,軟件中斷是使用線程來監視中斷信號,而硬件中斷是使用CPU硬件來監視中斷。
timekeeping_init();
這個函數是初始化系統時鐘計時,并且初始化內核里與時鐘計時相關的變量。
time_init();
這個函數是初始化系統時鐘。
profile_init();
這個函數是分配內核性能統計保存的內存,以便統計的性能變量可以保存到這里。
if (!irqs_disabled()) printk(KERN_CRIT "start_kernel(): bug: interrupts were " "enabled early ");
這兩行代碼是提示中斷是否過早地打開。
early_boot_irqs_on();
這個函數是設置內核還在早期初始化階段的標志,以便用來調試時輸出信息。
local_irq_enable();
這個函數是打開本CPU的中斷,也即允許本CPU處理中斷事件,在這里打開引CPU的中斷處理。如果有多核心,別的CPU還沒有打開中斷處理。
/* Interrupts are enabled now so all GFP allocations are safe. */ gfp_allowed_mask = __GFP_BITS_MASK; kmem_cache_init_late();
/* * HACK ALERT! This is early. We're enabling the console before * we've done PCI setups etc, and console_init() must be aware of * this. But we do want output early, in case something goes wrong. */ console_init();
這個函數是用來初始化控制臺,從這個函數之后就可以輸出內容到控制臺了。
在這個函數初化之前,都沒有辦法輸出內容,就是輸出,也是寫到輸出緩沖區里,緩存起來,等到這個函數調用之后,就立即輸出內容。
if (panic_later) panic(panic_later, panic_param);
這段代碼是判斷分析輸入的參數是否出錯,如果有出錯,就啟動控制臺輸出之后,立即打印出錯的參數,以便用戶立即看到出錯的地方。
lockdep_info();
這個函數是打印鎖的依賴信息,用來調試鎖。通過這個函數可以查看目前鎖的狀態,以便可以發現那些鎖產生死鎖,那些鎖使用有問題。
/* * Need to run this when irqs are enabled, because it wants * to self-test [hard/soft]-irqs on/off lock inversion bugs * too: */ locking_selftest();
這個函數是用來測試鎖的API是否使用正常,進行自我測試。比如測試自旋鎖、讀寫鎖、一般信號量和讀寫信號量。
#ifdef CONFIG_BLK_DEV_INITRD if (initrd_start && !initrd_below_start_ok && page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) { printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - " "disabling it. ", page_to_pfn(virt_to_page((void *)initrd_start)), min_low_pfn); initrd_start = 0; }#endif page_cgroup_init();
page_cgroup_init()這個函數是容器組的頁面內存分配。
enable_debug_pagealloc();
這個函數是設置內存分配是否需要輸出調試信息,如果調用這個函數,當分配內存時,不會輸出一些相關的信息。
kmemleak_init();
debug_objects_mem_init();
這個函數是創建調試對象內存緩存,所以緊跟內存緩存初始化后面
idr_init_cache();
這個函數是創建IDR機制的內存緩存對象。所謂的IDR就是整數標識管理機制(integerIDmanagement)。
引入的主要原因是管理整數的ID與對象的指針的關系,由于這個ID可以達到32位,也就是說,如果使用線性數組來管理,那么分配的內存太大了;如果使用線性表來管理,又效率太低了,所以就引用IDR管理機制來實現這個需求。
setup_per_cpu_pageset();
這個函數是創建每個CPU的高速緩存集合數組。因為每個CPU都不定時需要使用一些頁面內存和釋放頁面內存,為了提高效率,就預先創建一些內存頁面作為每個CPU的頁面集合。
numa_policy_init();
這個函數是初始化NUMA的內存訪問策略。所謂NUMA,它是NonUniform Memory AccessAchitecture的縮寫,主要用來提高多個CPU訪問內存的速度。因為多個CPU訪問同一個節點的內存速度遠遠比訪問多個節點的速度來得快。
if (late_time_init) late_time_init();
這段代碼是主要運行時鐘相關后期的初始化功能。
sched_clock_init();
calibrate_delay();
這個函數是主要計算CPU需要校準的時間,這里說的時間是CPU執行時間。如果是引導CPU,這個函數計算出來的校準時間是不需要使用的,主要使用在非引導CPU上,因為非引導CPU執行的頻率不一樣,導致時間計算不準確。
pidmap_init();
這個函數是進程位圖初始化,一般情況下使用一頁來表示所有進程占用情況。
anon_vma_init();
這個函數是初始化反向映射的匿名內存,提供反向查找內存的結構指針位置,快速地回收內存。
#ifdef CONFIG_X86 if (efi_enabled) efi_enter_virtual_mode();#endif
這段代碼是初始化EFI的接口,并進入虛擬模式。EFI是ExtensibleFirmware Interface的縮寫,就是INTEL公司新開發的BIOS接口。
thread_info_cache_init();
這個函數是線程信息的緩存初始化。
cred_init();
fork_init(totalram_pages);
這個函數是根據當前物理內存計算出來可以創建進程(線程)的數量,并進行進程環境初始化。
proc_caches_init();
這個函數是進程緩存初始化。
buffer_init();
這個函數是初始化文件系統的緩沖區,并計算最大可以使用的文件緩存。
key_init();
這個函數是初始化安全鍵管理列表和結構。
security_init();
這個函數是初始化安全管理框架,以便提供訪問文件/登錄等權限。
dbg_late_init();
vfs_caches_init(totalram_pages);
這個函數是虛擬文件系統進行緩存初始化,提高虛擬文件系統的訪問速度。
signals_init();
這個函數是初始化信號隊列緩存。
/* rootfs populating might need page-writeback */ page_writeback_init();
#ifdef CONFIG_PROC_FS proc_root_init();#endif
這個函數是初始化系統進程文件系統,主要提供內核與用戶進行交互的平臺,方便用戶實時查看進程的信息。
cgroup_init();
這個函數是初始化進程控制組,主要用來為進程和其子程提供性能控制。比如限定這組進程的CPU使用率為20%。
cpuset_init();
這個函數是初始化CPUSET,CPUSET主要為控制組提供CPU和內存節點的管理的結構。
taskstats_init_early();
這個函數是初始化任務狀態相關的緩存、隊列和信號量。任務狀態主要向用戶提供任務的狀態信息。
delayacct_init();
這個函數是初始化每個任務延時計數。當一個任務等CPU運行,或者等IO同步時,都需要計算等待時間。
check_bugs();
這個函數是用來檢查CPU配置、FPU等是否非法使用不具備的功能。
acpi_early_init(); /* before LAPIC and SMP init */
這個函數是初始化ACPI電源管理。
高級配置與能源接口(ACPI)ACPI規范介紹ACPI能使軟、硬件、操作系統(OS),主機板和外圍設備,依照一定的方式管理用電情況,系統硬件產生的Hot-Plug事件,讓操作系統從用戶的角度上直接支配即插即用設備,不同于以往直接通過基于BIOS 的方式的管理。
sfi_init_late();
ftrace_init();
這個函數是初始化內核跟蹤模塊,ftrace的作用是幫助開發人員了解Linux?內核的運行時行為,以便進行故障調試或性能分析。
/* Do the rest non-__init'ed, we're now alive */ rest_init();}
這個函數是后繼初始化,主要是創建內核線程init,并運行。
?
評論
查看更多