-----如下內容爲從網絡上整理所得------linux
嵌入式Linux啓動後,會先運行系統的bootloader,通常是用開源的U-boot;數組
Broadcom芯片的bootloader則是本身公司的CFE。緩存
bootloader前面文章有大致介紹了一下,如下主要分析Linux內核的啓動過程,以MIPS芯片爲例。安全
內核版本:2.6.31網絡
Bootloader將Linux內核映像拷貝到RAM中某個空閒地址處,而後通常有個內存移動操做,目的地址在數據結構
arch/mips/Makefile內指定:架構
load-$(CONFIG_MIPS_PB1550) += 0xFFFFFFFF80100000,
則最終bootloader定會將內核移到物理地址 0x00100000 處。框架
上面Makefile 裏指定的的 load 地址,最後會被編譯系統寫入到 arch/mips/kernel/vmlinux.lds 中:less
OUTPUT_ARCH(mips) ENTRY(kernel_entry) jiffies = jiffies_64; SECTIONS { . = 0xFFFFFFFF80100000; /* read-only */ _text = .; /* Text and read-only data */ .text : { *(.text) ...
這個文件最終會以參數 -Xlinker --script -Xlinker vmlinux.lds 的形式傳給 gcc,並最終傳給連接器 ld 來控制其行爲。
ld 會將 .text 節的地址連接到 0xFFFFFFFF80100000 處。ssh
關於內核 ELF 文件的入口地址(Entry point),即 bootloader 移動完內核後,直接跳轉到的地址,由ld 寫入 ELF的頭中,
其會依次用下面的方法嘗試設置入口點,當遇到成功時則中止:
a. 命令行選項 -e entry
b. 腳本中的 ENTRY(symbol)
c. 若是有定義 start 符號,則使用start符號(symbol)
d. 若是存在 .text 節,則使用第一個字節的地址。
e. 地址0
注意到上面的 ld script 中,用 ENTRY 宏設置了內核的 entry point 是 kernel_entry,
所以內核取得控制權後執行的第一條指令是在 kernel_entry 處。
linux 內核啓動的第一個階段是從 /arch/mips/kernel/head.s文件開始的。
而此處正是內核入口函數kernel_entry(),該函數定義在 /arch/mips/kernel/head.s文件裏。
kernel_entry()函數是體系結構相關的彙編語言,它首先初始化內核堆棧段,來爲建立系統中的第一個進程進行準備,
接着用一段循環將內核映像的未初始化數據段(bss段,在_edata和_end之間)清零,
最後跳轉到 /init/main.c 中的 start_kernel()初始化硬件平臺相關的代碼。
/*asmlinkage: This is a #define for some gcc magic that tells the *compiler that the function should not expect to find any of its arguments *in registers (a common optimization), but only on the CPU's stack.意思是如 *果函數定義前加宏asmlinkage ,表示這些函數經過堆棧而不是經過寄存器傳遞參數。 *init: 宏定義__init,用於告訴編譯器相關函數或變量的僅用於初始化。 *編譯器將標有—init的全部代碼存在初始化段中,初始化結束後就釋放這段內存 */ asmlinkage void __init start_kernel(void) { char * command_line; extern struct kernel_param __start___param[], __stop___param[]; //定義了核的參數數據結構 lockdep_init(); //初始化核依賴關係哈希表,classhash_table 和 chainhash_table smp_setup_processor_id(); //獲取當前正在執行初始化的處理器ID,若是kernel是單處理器,則此函數是空的,不做任何處理 debug_objects_early_init(); //這個函數主要做用是對調試對象進行早期的初始化,其實就是HASH鎖和靜態對象池進行初始化。 boot_init_stack_canary(); //stack_canary的是帶防止棧溢出攻擊保護的堆棧 cgroup_init_early(); //控制組進行早期的初始化 local_irq_disable(); //關閉當前CPU全部中斷響應 early_boot_irqs_off(); /*標記內核還在早期初始化代碼階段,而且中斷在關閉狀態,若是有任何中斷打開或請求中斷的事情出現,都是會提出警告,以便跟蹤代碼錯誤狀況。 *早期代碼初始化結束以後,就會調用函數early_boot_irqs_on來設置這個標誌爲真。 **/ early_init_irq_lock_class(); //每個中斷都有一個IRQ 描述符(struct irq_desc)來進行描述,這個函數的主要做用是設置全部的IRQ 描述符(struct irq_desc)的鎖是統一的鎖, //仍是每個IRQ 描述符(struct irq_desc)都有一個小鎖。 lock_kernel(); //這個函數主要做用是初始化大內核鎖。在對稱多處理器的系統裏,每個CPU均可以運行內核的代碼,但有時須要只能一個CPU運行內核代碼,那麼怎麼辦呢? //要解決這個問題,就須要給內核配備一把鎖 //只要擁有這把鎖的CPU才能夠運行內核的代碼,而且同一個CPU能夠遞歸地運行內核。 tick_init(); //這個函數主要做用是初始化時鐘事件管理器的回調函數,好比當時鍾設備添加時處理 boot_cpu_init(); //這個函數主要做用是設置當前引導系統的CPU在物理上存在,在邏輯上可使用,而且初始化準備好。 page_address_init(); //這個函數主要做用是初始化高端內存的映射表。在32位系統裏,內核爲了訪問超過1G的物理內存空間,須要使用高端內存映射表。 //好比當內核須要讀取1G的緩存數據時,就須要分配高端內存來使用,這樣才能夠管理起來。 //使用高端內存以後,32位的系統也能夠訪問達到64G內存。在移動操做系統裏,目前尚未這個必要,最多才1G多內存。 printk(KERN_NOTICE "%s", linux_banner); //輸出終端上顯示版本信息、編譯的電腦用戶名稱、編譯器版本、編譯時間 setup_arch(&command_line); //每種體系結構都有setup_arch函數,主要是再次獲取CPU類型和系統架構,分析引導程序傳入的命令行參數, //進行頁面內存初始化,處理器初始化,中斷早期初始化等等。 mm_init_owner(&init_mm, &init_task); //這個函數主要做用是設置最開始的初始化任務屬於init_mm內存 setup_command_line(command_line); //保存命令行 setup_nr_cpu_ids(); //設置最多有多少個nr_cpu_ids結構 setup_per_cpu_areas(); //設置SMP體系每一個CPU使用的內存空間,同時拷貝初始化段裏數據 smp_prepare_boot_cpu(); //爲SMP系統裏引導CPU進行準備工做 build_all_zonelists(); //初始化全部內存管理節點列表,以便後面進行內存管理初始化 page_alloc_init(); //設置內存頁分配通知器 printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line); //輸出命令參數到顯示終端 parse_early_param(); //分析命令行最先使用的參數 parse_args("Bootingkernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption); //對傳入內核參數進行解釋,若是不能識別的命令就調用最後參數的函數 pidhash_init(); //進程ID的HASH表初始化,這樣能夠提供通PID進行高效訪問進程結構的信息。LINUX裏共有四種類型的PID,所以就有四種HASH表相對應。 vfs_caches_init_early(); //虛擬文件系統的早期初始化 sort_main_extable(); //對內核內部的異常表進行堆排序,以便加速訪問 trap_init(); //對異常進行初始化 mm_init(); //設置內核內存分配器 sched_init(); //這個函數主要做用是對進程調度器進行初始化,好比分配調度器佔用的內存,初始化任務隊列,設置當前任務的空線程,當前任務的調度策略爲CFS調度器。 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主要提供在讀取數據機會比較多,但更新比較的少的場合,這樣減小讀取數據鎖的性能低下的問題。 early_irq_init(); //用於管理中斷的irq_desc[NR_IRQS]數組的每一個元素的部分字段設置爲肯定的狀態,它設置每個成員的中斷號 init_IRQ(); //是一個與特定體系結構相關的函數,用於管理中斷的irq_desc[NR_IRQS]結構數組各成員字段設置爲IRQ——NOREQUEST | IRQ——NOPROBE, //也就是未請求和未探測狀態,而後調用特定平臺的中斷初始化init_arch_irq()函數 prio_tree_init(); //優先搜索樹的初始化,主要用在內在反向搜索方面 init_timers(); //初始化引導CPU的時鐘相關的體系結構,註冊時鐘的回調函數,當時鍾到達時能夠回調時鐘處理函數,最後初始化時鐘軟件中斷處理。 hrtimers_init(); //初始化高精度的定時器,並設置回調函數 softirq_init(); //這個函數是初始化軟件中斷,軟件中斷與硬件中斷區別就是中斷髮生時,軟件中斷是使用線程來監視中斷信號,而硬件中斷是使用CPU硬件來監視中斷。 timekeeping_init(); //初始化系統時鐘計時,而且初始化內核裏與時鐘計時相關的變量。 time_init(); //初始化系統時鐘 sched_clock_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尚未打開中斷處理。 set_gfp_allowed_mask(__GFP_BITS_MASK); //Interrupts are enabled now so all GFP allocations are safe. kmem_cache_init_late(); //slab分配器的緩存機制 console_init(); //初始化控制檯,從這個函數以後就能夠輸出內容到控制檯了 //在這個函數初化以前,都沒有辦法輸出內容,就是輸出,也是寫到輸出緩衝區裏,緩存起來,等到這個函數調用以後,就當即輸出內容。 if (panic_later) panic(panic_later, panic_param); //判斷分析輸入的參數是否出錯,若是有出錯,就啓動控制檯輸出以後,當即打印出錯的參數,以便用戶當即看到出錯的地方。 lockdep_info(); //打印鎖的依賴信息,用來調試鎖。 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 //這段代碼是要支持初始RAM 磁盤,內核必需要使用CONFIG_BLK_DEV_RAM 和CONFIG_BLK_DEV_INITRD 選項進行編譯。 page_cgroup_init(); //容器組的頁面內存分配。 enable_debug_pagealloc(); //設置內存分配是否須要輸出調試信息,若是調用這個函數,當分配內存時,不會輸出一些相關的信息。 kmemtrace_init(); // kmemleak_init(); //memory lead偵測初始化 debug_objects_mem_init(); //在kmem_caches以後表示創建一個高速緩衝池,創建起SLAB_DEBUG_OBJECTS標誌。 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(); //主要運行時鐘相關後期的初始化功能。 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(num_physpages); //根據當前物理內存計算出來能夠建立進程(線程)的數量,並進行進程環境初始化 proc_caches_init(); //進程緩存初始化。 buffer_init(); //初始化文件系統的緩衝區,並計算最大可使用的文件緩存。 key_init(); //初始化安全鍵管理列表和結構。 security_init(); //初始化安全管理框架,以便提供訪問文件/登陸等權限。 vfs_caches_init(num_physpages); //虛擬文件系統進行緩存初始化,提升虛擬文件系統的訪問速度。 radix_treee_init(); //初始化radix樹,radix樹基於二進制鍵值的查找樹。 signals_init(); //初始化信號隊列緩存。 page_writeback_init(); //計算當前系統vm-radio等,設置是否須要回寫操做 #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(); //這個函數是初始化ACPI電源管理。高級配置與能源接口(ACPI)ACPI規範介紹ACPI能使軟、硬件、操做系統(OS), //主機板和外圍設備,依照必定的方式管理用電狀況,系統硬件產生的Hot-Plug事件,讓操做系統從用戶的角度上直接支配即插即用設備, //不一樣於以往直接經過基於BIOS 的方式的管理。 ftrace_init(); //初始化內核跟蹤模塊,ftrace的做用是幫助開發人員瞭解Linux 內核的運行時行爲,以便進行故障調試或性能分析。 /* Do the rest non-__init'ed, we're now alive */ rest_init(); //這個函數是後繼初始化,主要是建立內核線程init,並運行。 }
在上面已經對基本的硬件、系統的結構初始化完成,接着下來系統要作的工做,就是建立進 程,對進程進行管理,纔可讓系統生生不息,處理各類各樣的任務。雖然大部份的初始化工做已經完成,但還須要更進一步初始化,所以建立一個內核初始化線程 來繼續初始化。爲了有一個乾淨,又能夠拷貝,又方便建立線程的方法,就是建立一個特別的內核線程kthreadd,這樣全部之後須要建立的線程都是由這個線程建立出來的,能夠說這個線程爲其他內核線程之母。建立完這兩個線程以後,初始化進程還須要對線程調度器的狀態進行運行一次,以便初始化到合適的值。最後,這個初始化進程就退化爲一個IDLE空閒進程了,完成了引導系統全部的工做,進入系統正常運行的狀態。
staticvoid noinline __init_refok rest_init(void) __releases(kernel_lock) { int pid; kernel_thread(kernel_init,NULL, CLONE_FS | CLONE_SIGHAND); //這行代碼是調用建立內核線程函數kernel_thread來建立內核第二階段的初始化線程。 numa_default_policy(); //這行代碼是設置當前進程使用缺省的內存管理策略。 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); //這行代碼是建立一個乾淨內核線程,以便之後其它全部內核線程所有拷貝它,並由它來建立,這樣達到更方便建立線程,不用設置太多參數。 kthreadd_task= find_task_by_pid_ns(pid, &init_pid_ns); //這行代碼是經過進程ID查找內核線程的任務地址,並保存起來,方便訪問。 unlock_kernel(); //這行代碼是釋放大內核鎖,以便多CPU能夠訪問內核代碼。 /* * The boot idle thread must execute schedule() * at least once to get things moving: */ init_idle_bootup_task(current); //這行代碼是初始化空閒進程的調度器,以便讓空閒進程知道怎麼樣調度任務列表裏的進程。current是指向當前IDLE任務的結構。 rcu_scheduler_starting(); //內核RCU鎖機制調度啓動 preempt_enable_no_resched(); //這行代碼是減小內核搶先計數,而且不進行調度操做。這樣運行後,以便IDLE進程可讓別的高優先級的進程運行。 schedule(); //這行代碼是調用進程調度函數schedule,主要初始化調度器能夠切換回到空閒任務。 preempt_disable(); //這行代碼是增長內核搶先計數。 /*Call into cpu_idle with preempt disabled */ cpu_idle(); //這行代碼是調用CPU空閒任務進程運行,再也不返回來。 }
系統已經完成了整個初始化過程,那麼這個初始化進程最好的歸宿是那裏呢?顯然它就是進化爲一個空閒進程,當系統沒有其它任務處理時,就會經過進程管理器選擇這個優先級最低,沒有什麼事情作的任務,以便整個CPU還有事情可作。也許你也會問,爲何必定要一個空閒進程,不要這個進程不行嗎?確定回答是不行的,整個系統裏的CPU資源總須要使用的,若是不使用CPU資源,那麼這個CPU就意味着再也不執行指令了,CPU就已經停機了,當有新的任務到來時就沒有辦法切換過去。若是選擇CPU停機的方式,那麼這個CPU須要再運行任務時,就須要喚醒。而喚醒的方法,須要外界施加條件才能夠,通常都是物理條件,就是觸發CPU的中斷信號。
空閒進程主要作的工做,就是不斷查找是否有新的任務能夠運行,若是沒有新的任務運行,就繼續執行空閒任務處理的事情。下面就來仔細地分析這個函數的具體工做,代碼以下:
void cpu_idle(void) { local_fiq_enable(); //這行代碼是打開ARM系統的快速中斷,所謂的FIQ是相對於普通的IRQ來講的,FIQ是能夠打斷普通的IRQ中斷,反之不行。 //下面就進入無限循環的空閒進程處理: /* endless idle loopwith no priority at all */ while (1) { void (*idle)(void)= pm_idle; //這行代碼是調用定製的空閒處理函數。 #ifdefCONFIG_HOTPLUG_CPU if(cpu_is_offline(smp_processor_id())) { leds_event(led_idle_start); cpu_die(); } #endif //這段代碼是處理熱插拔的CPU機制,當容許當前這個CPU進入睡眠狀態,就能夠進入。 if (!idle) idle =default_idle; //這段代碼是當沒有用戶定義的空閒處理函數時,就調用缺省的空閒處理函數。缺省的空閒處理函數,就會調用系統架構的空閒處理函數。 leds_event(led_idle_start); //這行代碼是打開LED顯示空閒運行狀態。 tick_nohz_stop_sched_tick(1); //這行代碼是中止進程調度計數。 while(!need_resched()) idle(); //這段代碼是當須要調度標誌爲空時,就不斷調用空閒處理函數進行運行。 leds_event(led_idle_end); //這行代碼是當須要調度其它進行運行了,LED結束顯示空閒運行狀態。 tick_nohz_restart_sched_tick(); //這行代碼是從新開始計算調度運行計數。 preempt_enable_no_resched(); //這行代碼是減小搶佔計數,不須要從新調度,下面立刻就開始調度。 schedule(); //這行代碼是對進程任務進行調度,以便當即運行正在等待的任務。 preempt_disable(); //這行代碼是增長搶佔計數,禁止調度發生。 } }