經曆了跟體系結構密切相關的彙編代碼之後,就可以進入C語言編寫的結構無關的代碼了。
這個入口的函數是start_kernel函數,它主要更進一步地初始化系統相關的内容,以便系統進入一種服務狀态,提供一種虛拟機的服務,提供各種API調用的服務。
在start_kernel函數裡,需要非常注意的是裡面初始化函數的順序,這些初始化函數不能随便調換初始化順序,否則就會導緻系統運作出錯。
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 <linux/compiler.h>
#include <asm/linkage.h>
#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%。是以通過控制組就可以把所有程序配置設定不同的資源。
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+ ([email protected]) (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\n", 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\n");
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\n");
這兩行代碼是提示中斷是否過早地打開。
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.\n",
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,并運作。