文章目錄
Linux 核心啟動分析
1. 核心啟動地址1.1. 名詞解釋
ZTEXTADDR
解壓代碼啟動並執行開始地址。沒有物理地址和虛擬位址之分,因為此時MMU處於關閉狀態。這個地址不一定時RAM的地址,可以是支援讀寫定址的flash等儲存中介。
Start address of decompressor. here's no point in talking about virtual or physical addresses here, since the MMU will be off at the time when you call the decompressor code. You normally call the kernel at this address to start it booting. This doesn't have to be located in RAM, it can be in flash or other read-only or read-write addressable medium.
ZRELADDR
核心啟動在RAM中的地址。壓縮的核心映像被解壓到這個地址,然後執行。
This is the address where the decompressed kernel will be written, and eventually executed. The following constraint must be valid:
__virt_to_phys(TEXTADDR) == ZRELADDR
The initial part of the kernel is carefully coded to be position independent.
TEXTADDR
核心啟動的虛擬位址,與ZRELADDR相對應。一般核心啟動的虛擬位址為RAM的第一個bank地址加上0x8000。
TEXTADDR = PAGE_OFFSET + TEXTOFFST
Virtual start address of kernel, normally PAGE_OFFSET + 0x8000.This is where the kernel image ends up. With the latest kernels, it must be located at 32768 bytes into a 128MB region. Previous kernels placed a restriction of 256MB here.
TEXTOFFSET
核心位移地址。在arch/arm/makefile中設定。
PHYS_OFFSET
RAM第一個bank的物理起始地址。
Physical start address of the first bank of RAM.
PAGE_OFFSET
RAM第一個bank的虛擬起始地址。
Virtual start address of the first bank of RAM. During the kernel
boot phase, virtual address PAGE_OFFSET will be mapped to physical
address PHYS_OFFSET, along with any other mappings you supply.
This should be the same value as TASK_SIZE.
1.2. 核心啟動地址確定
核心啟動引導地址由bootp.lds決定。 Bootp.lds : arch/arm/bootp
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0;
.text : {
_stext = .;
*(.start)
*(.text)
initrd_size = initrd_end - initrd_start;
_etext = .;
}
}
由上 .= 0可以確定解壓代碼啟動並執行開始地址在0x0的位置。ZTEXTADDR的值決定了這個值得選取。
Makefile : arch/arm/boot/compressed
如果設定核心從ROM中啟動的話,可以在make menuconfig 的配置介面中設定解壓代碼的起始地址,否則解壓代碼的起始地址為0x0。實際上,預設從ROM啟動時,解壓代碼的起始地址也是0x0。
feq ($(CONFIG_ZBOOT_ROM),y)
ZTEXTADDR := $(CONFIG_ZBOOT_ROM_TEXT)
ZBSSADDR := $(CONFIG_ZBOOT_ROM_BSS)
else
ZTEXTADDR :=0 ZBSSADDR := ALIGN(4)
endif
SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/BSS_START/$(ZBSSADDR)/
……
$(obj)/vmlinux.lds: $(obj)/vmlinux.lds.in arch/arm/mach-s3c2410/Makefile .config
@sed "$(SEDFLAGS)" < $< > $@
@sed "$(SEDFLAGS)" < $< > $@ 規則將TEXT_START設定為ZTEXTADDR。TEXT_START在arch/arm/boot/compressed/vmlinux.lds.in 中被用來設定解壓代碼的起始地址。
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = TEXT_START;
_text = .;
.text : {
_start = .;
*(.start)
*(.text)
*(.text.*)
……
}
}
核心的編譯依靠vmlinux.lds,vmlinux.lds由vmlinux.lds.s 產生。從下面代碼可以看出核心啟動的虛擬位址被設定為PAGE_OFFSET + TEXT_OFFSET,而核心啟動的物理地址ZRELADDR在arch/arm/boot/Makefile中設定。
OUTPUT_ARCH(arm)
ENTRY(stext)
SECTIONS
{
#ifdef CONFIG_XIP_KERNEL
. = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);
#else
. = PAGE_OFFSET + TEXT_OFFSET;
#endif
.init : { /* Init code and data */
_stext = .;
_sinittext = .;
*(.init.text)
_einittext = .;
……
}
}
# arch/arm/boot/Makefile
# Note: the following conditions must always be true:
# ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)
# PARAMS_PHYS must be within 4MB of ZRELADDR
# INITRD_PHYS must be in RAM
ZRELADDR := $(zreladdr-y)
#---> zrealaddr-y is specified with 0x30008000 in arch/arm/boot/makefile.boot
PARAMS_PHYS := $(params_phys-y)
INITRD_PHYS := $(initrd_phys-y)
export ZRELADDR INITRD_PHYS PARAMS_PHYS
通過下面的命令編譯核心映像,由參數-a, -e設定其入口地址為ZRELADDR,此值在上面ZRELADDR := $(zreladdr-y)指定。
quiet_cmd_uimage= UIMAGE $@
cmd_uimage = $(CONFIG_SHELL) $(MKIMAGE) -A arm -O linux -T kernel /
-C none -a $(ZRELADDR) -e $(ZRELADDR) /
-n 'Linux-$(KERNELRELEASE)' -d $< $@
1.3. 小結
從上面分析可知道,linux核心被bootloader拷貝到RAM後,解壓代碼從ZTEXTADDR開始運行(這段代碼是與位置無關的PIC)。核心被解壓縮到ZREALADDR處,也就是核心啟動的物理地址處。相應地,核心啟動的虛擬位址被設定為TEXTADDR,滿足如下條件:
TEXTADDR = PAGE_OFFSET + TEXT_OFFSET
核心啟動的物理地址和虛擬位址滿足入下條件:
ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)= virt_to_phys(TEXTADDR)
假定開發板為smdk2410,則有:
核心啟動的虛擬位址
TEXTADDR = 0xC0008000
核心啟動的物理地址
ZRELADDR = 0x30008000
如果直接從flash中啟動還需要設定ZTEXTADDR地址。
2. 核心啟動過程分析
核心啟動過程經過大體可以分為兩個階段:核心映像的自引導;linux核心子模組的初始化。
2.1. 核心映像的自引導
這階段的主要工作是實現壓縮核心的解壓和進入核心代碼的入口。
Bootloader完成系統引導後,核心映像被調入記憶體指定的物理地址ZTEXTADDR。典型的核心映像由自引導程式和壓縮的VMlinux組成。因此在啟動核心之前需要先把核心解壓縮。核心映像的入口的第一條代碼就是自引導程式。它在arch/arm/boot/compressed/head.S檔案中。
Head.S檔案主要功能是實現壓縮核心的解壓和跳轉到核心vmlinux核心的入口。Decompress_kernel(): arch/arm/boot/compressed/misc.c 和call_kernel這兩個函數實現了上述功能。在調用decompress_kernel()解壓核心之前,需要確保解壓後的核心代碼不會覆蓋掉原來的核心映像。以及設定核心代碼的入口地址ZREALADDR。
.text
adr r0, LC0
ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}
.type LC0, #object
LC0: .word LC0 @ r1
.word __bss_start @ r2
.word _end @ r3
.word zreladdr @ r4
.word _start @ r5
.word _got_start @ r6
.word _got_end @ ip
.word user_stack+4096 @ sp
上面這段代碼得到核心代碼的入口地址,儲存在r4中。
/*
* Check to see if we will overwrite ourselves.
* r4 = final kernel address
* r5 = start of this image
* r2 = end of malloc space (and therefore this image)
* We basically want:
* r4 >= r2 -> OK
* r4 + image length <= r5 -> OK
*/
cmp r4, r2
bhs wont_overwrite
add r0, r4, #4096*1024 @ 4MB largest kernel size
cmp r0, r5
bls wont_overwrite
mov r5, r2 @ decompress after malloc space
mov r0, r5
mov r3, r7
bl decompress_kernel
b call_kernel
上面代碼判斷解壓後的核心代碼會不會覆蓋原來的核心映像,然後調用核心解壓縮函數decompress_kernel()。
ulg
decompress_kernel(ulg output_start, ulg free_mem_ptr_p, ulg free_mem_ptr_end_p,
int arch_id)
{
output_data = (uch *)output_start; /* 指定核心執行地址,儲存在r4中*/
free_mem_ptr = free_mem_ptr_p;
free_mem_ptr_end = free_mem_ptr_end_p;
__machine_arch_type = arch_id;
arch_decomp_setup(); /*解壓縮前的初始化和設定,包括串口傳輸速率設定等*/
makecrc(); /*CRC校正*/
putstr("Uncompressing Linux...");
gunzip(); /*調用解壓縮函數*/
putstr(" done, booting the kernel./n");
return output_ptr;
}
把核心映像解壓到ZERALADDR地址後,調用call_kernel函數進入核心代碼的入口地址。
call_kernel: bl cache_clean_flush
bl cache_off
mov r0, #0 @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
mov pc, r4 @ call kernel
我們知道r4寄存器內儲存的是核心的執行地址,mov pc, r4使得程式指標指向了核心的執行地址,所以下面將進入核心代碼執行階段。
2.2. linux核心子模組的初始化2.2.1. 預備工作
進入真正的核心代碼,首先執行的也是一個叫做head.S(arch/arm/kernel/)的檔案。同時head.S也包含了同目錄下head-common.S(arch/arm/kernel/)。這兩個檔案聯合起來主要負責下面幾項工作:
判斷CPU類型,尋找啟動並執行CPU ID值與Linux編譯支援的ID值是否支援
判斷體系類型,查看R1寄存器的Architecture Type值是否支援
建立頁表
開啟MMU
跳轉到start_kernel()(核心子模組初始化程式)
注: 暫時不對各個子程式實現作細節性的分析。
2.2.2. 核心各子模組初始化
Start_kernel函數是Linux核心通用的初始化函數。無論對於什麼體繫結構的Linux,都要執行這個函數。Start_kernel()函數是核心初始化的基本過程。下面按照函數對核心模組初始化的先後順序進行分析。
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
smp_setup_processor_id(); /*指定當前的cpu的邏輯號,這個函數對應於對稱式多處理器的設定,當系統中只有一個cpu的情況,此函數為空白,什麼也不做*/
lockdep_init(); /* 初始化lockdep hash 表 */
/* 初始化irq */
local_irq_disable();
early_boot_irqs_off();
early_init_irq_lock_class();
/* 鎖定核心、設定cpu的狀態為’present’,’online’等狀態、初始化頁表、列印核心版本號碼等資訊、設定體繫結構、為cpu分配啟動記憶體空間 ,在此期間中斷仍然處於關閉狀態*/
lock_kernel();
boot_cpu_init();
page_address_init();
printk(KERN_NOTICE);
printk(linux_banner);
setup_arch(&command_line);
setup_per_cpu_areas();
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
/*在開啟任何中斷之前開啟調度器 */
sched_init();
/*關閉任務搶佔功能,因為早期的調度器功能比較脆弱,直到第一次調用cpu_idle()*/
preempt_disable();
/* 建立記憶體地區鏈表節點,對於單cpu節點數為1 */
build_all_zonelists();
/* 發通知給每個CPU,處理每個CPU的記憶體狀態*/
page_alloc_init();
/* 分析早期沒命令參數*/
printk(KERN_NOTICE "Kernel command line: %s/n", saved_command_line);
parse_early_param();
/* 分析命令參數 */
parse_args("Booting kernel", command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
/* 排序核心建立的異常表 */
sort_main_extable();
unwind_init();
/*設定陷阱門和中斷門 */
trap_init();
/*初始化核心中的讀-拷貝-更新(Read-Copy-Update RCU)子系統 */
rcu_init();
/*初始化IRQ */
init_IRQ();
/* 按照開發辦上的實體記憶體初始化pid hash表 */
pidhash_init();
/*初始化計時器 */
init_timers();
/* 高解析度&高精度的計時器 (high resolution)初始化 */
hrtimers_init();
/*初始化非強制中斷 */
softirq_init();
/* 初始化時鐘資源和普通計時器的值 */
timekeeping_init();
/* 初始化系統時間*/
time_init();
/*為核心分配記憶體以儲存收集的資料*/
profile_init();
/* 開中斷 */
if (!irqs_disabled())
printk("start_kernel(): bug: interrupts were enabled early/n");
early_boot_irqs_on();
local_irq_enable();
/*
* 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);
/*如果定義了CONFIG_LOCKDEP宏,則列印鎖依賴資訊,否則什麼也不做 */
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:
*/
/* 如果定義CONFIG_DEBUG_LOCKING_API_SELFTESTS宏,則locking_selftest()是一個空函數,否則執行鎖自測*/
locking_selftest();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok &&
initrd_start < min_low_pfn << PAGE_SHIFT) {
printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
"disabling it./n",initrd_start,min_low_pfn << PAGE_SHIFT);
initrd_start = 0;
}
#endif
/*
(1)dcache_init()建立SLAB緩衝,該緩衝儲存目錄項描述符。傳存本身被稱作dentry_cache。當進程訪問檔案或目錄時所涉及的目錄名有多個目錄分量組成,目錄項描述符就是針對每個分量而建立的。目錄項各結構把檔案或目錄分量與其索引結點結合起來,因而可以通過該目錄項可以更快地找到與其對應的索引結點
(2)inode_init()初始化雜湊表索引結點和等待隊列對頭,該隊頭存放核心要鎖存的雜湊索引結點。
(3)file_init()確定給每個進程呢個zhogn的檔案所分配的最大記憶體量
(4)mnt_init()建立了儲存vfsmount對象且名為mnt_cache的緩衝,VFS利用這協對吸納給來掛載檔案系統。該常式也建立mount_hashtable隊列,該隊列存放mnt_cache中引用的快速存取對象。然後該常式發出調用來初始化sysfs檔案系統並掛載root檔案系統。
*/
vfs_caches_init_early();
cpuset_init_early();
/* Mem_init()函數為mem_map中的自由區作標記並且列印出自由記憶體的大小。這個函數在系統的各個部分申請過記憶體後執行 */
mem_init();
/* 初始化cache相關的鏈表,函數在初始化頁表分配器後smp_init()之前執行 */
kmem_cache_init();
/* 為邏輯號為0的cpu初始化頁面。如果是smp情況下,只要cpu表現為online態,此函數就會執行 */
setup_per_cpu_pageset();
/* numa記憶體策略器初始化 */
numa_policy_init();
/* 記憶體初始化後調用 */
if (late_time_init)
late_time_init();
/*計算並列印許多著名的"BogoMips"的值,該值度量處理器在一個時鐘節拍內可以反覆執行多少個delay().對不同速度的處理器,cali_brate_delay()允許的延遲大約相同*/
calibrate_delay();
/* 初始化pidmap_array,分配pid=0給當前進程 */
pidmap_init();
/* 初始化頁表快取 */
pgtable_cache_init();
/*初始化優先順序樹index_bits_to_maxindex數組*/
prio_tree_init();
/*建立anon_vma結構對象slab緩衝*/
anon_vma_init();
#ifdef CONFIG_X86
if (efi_enabled)
efi_enter_virtual_mode();
#endif
/*根據可用記憶體大小來建立使用者緩衝區uid_cache,初始化最大線程數max_threads,為init_task配置RLIMIT_NPROC的值為max_threads/2 */
fork_init(num_physpages);
/*建立各種塊緩衝區,比如VFS, VM等*/
proc_caches_init();
buffer_init();
unnamed_dev_init();
key_init();
security_init();
vfs_caches_init(num_physpages);
radix_tree_init();
signals_init();
/* rootfs populating might need page-writeback */
page_writeback_init();
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif
cpuset_init();
taskstats_init_early();
delayacct_init();
/*檢查錯誤,其實是調用check_writebuffer_bugs()函數檢查是否有物理地址混淆的現象 */
check_bugs();
/* advanced configuration and power management interface */
acpi_early_init(); /* before LAPIC and SMP init */
/* Do the rest non-__init'ed, we're now alive */
/* 建立init進程,刪除核心鎖,啟動idle線程 */
rest_init();
}
進入init進程後,將執行init()函數負責完成掛接根檔案系統、初始化裝置驅動和啟動使用者空間的init進程。(sunny 負責研究這部分)