Linux核心是 Linux 作業系統(OS)的主要元件,也是計算機硬體與其程序之間的核心介面。它負責兩者之間的通訊,還要儘可能高效地管理資源。之所以稱為核心,是因為它在作業系統中就像果實硬殼中的種子一樣,並且控制著硬體(無論是電話、筆記型電腦、伺服器,還是任何其他型別的計算機)的所有主要功能。
核心到底是什麼呢?其實核心就是系統上面的一個檔案而已,這個檔案包含了驅動主機各項硬體的檢測程式與驅動模組。這個核心檔案通常被放置在/boot/vmlinux-xxx,不過也不一定,因為一部主機上面可以擁有多個核心檔案,只是開機的時候僅僅能選擇一個來載入而已。而核心主要負責記憶體管理、程序管理、裝置驅動程式、系統呼叫和安全防護這四項工作。
往期推薦:
史上最全的Linux常用命令彙總(超全面!超詳細!)收藏這一篇就夠了!
史上最全的Uboot常用命令彙總(超全面!超詳細!)收藏這一篇就夠了!
連Linux的開機流程都不瞭解,怎麼好意思說自己是程式設計師?
要分析Linux啟動流程,要先編譯一下Linux原始碼,因為很多檔案是需要編譯才會生成的。通過分析以下Linux核心的連線指令碼檔案arch/arm/kernel/vmlinux.lds
,通過連線指令碼可以找到Linux核心的入口為stext,stext定義在arch/arm/kernel/head.S
中 , 因 此 要 分 析 Linux 內 核 的 啟 動 流 程 , 就 得 先 從 文 件
arch/arm/kernel/head.S
的 stext 處開始分析。
/*
* Kernel startup entry point.
* ---------------------------
*
* This is normally called from the decompressor code. The requirements
* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
* r1 = machine nr, r2 = atags or dtb pointer.
* /
根據程式碼的註釋,Linux核心啟動之前要求如下:
ENTRY(stext)
ARM_BE8(setend be ) @ ensure we are in BE8 mode
THUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM.
THUMB( bx r9 ) @ If this is a Thumb-2 kernel,
THUMB( .thumb ) @ switch to Thumb now.
THUMB(1: )
#ifdef CONFIG_ARM_VIRT_EXT
bl __hyp_stub_install
#endif
@ ensure svc mode and all interrupts masked
safe_svcmode_maskall r9
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p @ yes, error 'p'
#ifdef CONFIG_ARM_LPAE
mrc p15, 0, r3, c0, c1, 4 @ read ID_MMFR0
and r3, r3, #0xf @ extract VMSA support
cmp r3, #5 @ long-descriptor translation table format?
THUMB( it lo ) @ force fixup-able long branch encoding
blo __error_lpae @ only classic page table format
#endif
#ifndef CONFIG_XIP_KERNEL
adr r3, 2f
ldmia r3, {r4, r8}
sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET)
add r8, r8, r4 @ PHYS_OFFSET
#else
ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case
#endif
/*
* r1 = machine no, r2 = atags or dtb,
* r8 = phys_offset, r9 = cpuid, r10 = procinfo
*/
bl __vet_atags
#ifdef CONFIG_SMP_ON_UP
bl __fixup_smp
#endif
#ifdef CONFIG_ARM_PATCH_PHYS_VIRT
bl __fixup_pv_table
#endif
bl __create_page_tables
/*
* The following calls CPU specific code in a position independent
* manner. See arch/arm/mm/proc-*.S for details. r10 = base of
* xxx_proc_info structure selected by __lookup_processor_type
* above. On return, the CPU will be ready for the MMU to be
* turned on, and r0 will hold the CPU control register value.
*/
ldr r13, =__mmap_switched @ address to jump to after
@ mmu has been enabled
adr lr, BSYM(1f) @ return (PIC) address
mov r8, r4 @ set TTBR1 to swapper_pg_dir
ldr r12, [r10, #PROCINFO_INITFUNC]
add r12, r12, r10
ret r12
1: b __enable_mmu
ENDPROC(stext)
通過分析上述程式碼,第12行,呼叫safe_svcmode_maskall 確保 CPU 處於 SVC 模式,並且關閉了所有的中斷。 關閉後讀取處理器的ID,ID值儲存在r9暫存器中。然後呼叫__lookup_processor_type
檢查當前系統是否支援此 CPU,如果支援的就獲 取 procinfo 信 息 。 procinfo 是 proc_info_list 類 型 的 結 構 體 ,proc_info_list 在 文 件arch/arm/include/asm/procinfo.h 中的定義如下:
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags; /* used by head.S */
unsigned long __cpu_io_mmu_flags; /* used by head.S */
unsigned long __cpu_flush; /* used by head.S */
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};
Linux核心將每種處理器都抽象為一個proc_info_list的結構體,每種處理器對應一個procinfo。因此可以通過處理器ID來找到對應的procinfo結構, __lookup_processor_type 函數找到對應處理器的 procinfo 以後會將其儲存到 r5 暫存器中。
第41行程式碼中,呼叫函數__vet_atags
驗證 atags 或裝置樹(dtb)的合法性。第48行,呼叫函數__create_page_tables
建立頁表。第57行,將函數__mmap_switched
的地址儲存到 r13 暫存器中。 __mmap_switched
定義在檔案 arch/arm/kernel/head-common.S
, __mmap_switched
最終會呼叫 start_kernel 函數。第 64 行 , 調 用 __enable_mmu
函 數 使 能 MMU , __enable_mmu
定 義 在 文 件arch/arm/kernel/head.S 中。 __enable_mmu
最終會通過呼叫__turn_mmu_on
來開啟 MMU,__turn_mmu_on
最後會執行 r13 裡面儲存的__mmap_switched
函數。
__mmap_switched 函數定義在檔案 arch/arm/kernel/head-common.S 中,函數程式碼如下:
__mmap_switched:
adr r3, __mmap_switched_data
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, [r3, #16] )
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
cmp r7, #0
strne r0, [r7] @ Save control register values
b start_kernel
ENDPROC(__mmap_switched)
該函數最終通過呼叫start_kernel來啟動Linux核心
start_kernel通過呼叫眾多的子函數來完成Linux啟動之前的一些初始化工作,由於start_kernel函數裡面呼叫的函數太多,而且這些子函數又很複雜,我們只是簡單的瞭解一下Linux核心的啟動流程,只需要簡單瞭解一些比較重要的函數就可以啦!
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
lockdep_init(); /* lockdep 是死鎖檢測模組,此函數會初始化
* 兩個 hash 表。此函數要求儘可能早的執行!
*/
set_task_stack_end_magic(&init_task);/* 設定任務棧結束魔術數,
*用於棧溢位檢測
*/
smp_setup_processor_id(); /* 跟 SMP 有關(多核處理器),設定處理器 ID。
* 有很多資料說 ARM 架構下此函數為空函數,那是因
* 為他們用的老版本 Linux,而那時候 ARM 還沒有多
* 核處理器。
*/
debug_objects_early_init(); /* 做一些和 debug 有關的初始化 */
boot_init_stack_canary(); /* 棧溢位檢測初始化 */
cgroup_init_early(); /* cgroup 初始化, cgroup 用於控制 Linux 系統資源*/
local_irq_disable(); /* 關閉當前 CPU 中斷 */
early_boot_irqs_disabled = true;
/*
* 中斷關閉期間做一些重要的操作,然後開啟中斷
*/
boot_cpu_init(); /* 跟 CPU 有關的初始化 */
page_address_init(); /* 頁地址相關的初始化 */
pr_notice("%s", linux_banner);/* 列印 Linux 版本號、編譯時間等資訊 */
setup_arch(&command_line); /* 架構相關的初始化,此函數會解析傳遞進來的
* ATAGS 或者裝置樹(DTB)檔案。會根據裝置樹裡面
* 的 model 和 compatible 這兩個屬性值來查詢
* Linux 是否支援這個單板。此函數也會獲取裝置樹
* 中 chosen 節點下的 bootargs 屬性值來得到命令
* 行引數,也就是 uboot 中的 bootargs 環境變數的
* 值,獲取到的命令列引數會儲存到
*command_line 中。
*/
mm_init_cpumask(&init_mm); /* 看名字,應該是和記憶體有關的初始化 */
setup_command_line(command_line); /* 好像是儲存命令列引數 */
setup_nr_cpu_ids(); /* 如果只是 SMP(多核 CPU)的話,此函數用於獲取
* CPU 核心數量, CPU 數量儲存在變數
* nr_cpu_ids 中。
*/
setup_per_cpu_areas(); /* 在 SMP 系統中有用,設定每個 CPU 的 per-cpu 資料 */
smp_prepare_boot_cpu();
build_all_zonelists(NULL, NULL); /* 建立系統記憶體頁區(zone)連結串列 */
page_alloc_init(); /* 處理用於熱插拔 CPU 的頁 */
/* 列印命令列資訊 */
pr_notice("Kernel command line: %s\n", boot_command_line);
parse_early_param(); /* 解析命令列中的 console 引數 */
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,set_init_arg);
jump_label_init();
setup_log_buf(0); /* 設定 log 使用的緩衝區*/
pidhash_init(); /* 構建 PID 雜湊表, Linux 中每個程序都有一個 ID,
* 這個 ID 叫做 PID。通過構建雜湊表可以快速搜尋程序
* 資訊結構體。
*/
vfs_caches_init_early(); /* 預先初始化 vfs(虛擬檔案系統)的目錄項和
* 索引節點快取
*/
sort_main_extable(); /* 定義核心異常列表 */
trap_init(); /* 完成對系統保留中斷向量的初始化 */
mm_init(); /* 記憶體管理初始化 */
sched_init(); /* 初始化排程器,主要是初始化一些結構體 */
preempt_disable(); /* 關閉優先順序搶佔 */
if (WARN(!irqs_disabled(), /* 檢查中斷是否關閉,如果沒有的話就關閉中斷 */
"Interrupts were enabled *very* early, fixing it\n"))
local_irq_disable();
idr_init_cache(); /* IDR 初始化, IDR 是 Linux 核心的整數管理機
* 制,也就是將一個整數 ID 與一個指標關聯起來。
*/
rcu_init(); /* 初始化 RCU, RCU 全稱為 Read Copy Update(讀-拷貝修改) */
trace_init(); /* 跟蹤偵錯相關初始化 */
context_tracking_init();
radix_tree_init(); /* 基數樹相關資料結構初始化 */
early_irq_init(); /* 初始中斷相關初始化,主要是註冊 irq_desc 結構體變
* 量,因為 Linux 核心使用 irq_desc 來描述一箇中斷。
*/
init_IRQ(); /* 中斷初始化 */
tick_init(); /* tick 初始化 */
rcu_init_nohz();
init_timers(); /* 初始化定時器 */
hrtimers_init(); /* 初始化高精度定時器 */
softirq_init(); /* 軟中斷初始化 */
timekeeping_init();
time_init(); /* 初始化系統時間 */
sched_clock_postinit();
perf_event_init();
profile_init();
call_function_init();
WARN(!irqs_disabled(), "Interrupts were enabled early\n");
early_boot_irqs_disabled = false;
local_irq_enable(); /* 使能中斷 */
kmem_cache_init_late(); /* slab 初始化, slab 是 Linux 記憶體分配器 */
console_init(); /* 初始化控制檯,之前 printk 列印的資訊都存放
* 緩衝區中,並沒有列印出來。只有呼叫此函數
* 初始化控制檯以後才能在控制檯上列印資訊。
*/
if (panic_later)
panic("Too many boot %s vars at `%s'", panic_later,panic_param);
lockdep_info();/* 如果定義了宏 CONFIG_LOCKDEP,那麼此函數列印一些資訊。 */
locking_selftest() /* 鎖自測 */
......
page_ext_init();
debug_objects_mem_init();
kmemleak_init(); /* kmemleak 初始化, kmemleak 用於檢查記憶體漏失 */
setup_per_cpu_pageset();
numa_policy_init();
if (late_time_init)
late_time_init();
sched_clock_init();
calibrate_delay(); /* 測定 BogoMIPS 值,可以通過 BogoMIPS 來判斷 CPU 的效能
* BogoMIPS 設定越大,說明 CPU 效能越好。
*/
pidmap_init(); /* PID 點陣圖初始化 */
anon_vma_init(); /* 生成 anon_vma slab 快取 */
acpi_early_init();
......
thread_info_cache_init();
cred_init(); /* 為物件的每個用於賦予資格(憑證) */
fork_init(); /* 初始化一些結構體以使用 fork 函數 */
proc_caches_init(); /* 給各種資源管理結構分配快取 */
buffer_init(); /* 初始化緩衝快取 */
key_init(); /* 初始化金鑰 */
security_init(); /* 安全相關初始化 */
dbg_late_init();
vfs_caches_init(totalram_pages); /* 為 VFS 建立快取 */
signals_init(); /* 初始化訊號 */
page_writeback_init(); /* 頁回寫初始化 */
proc_root_init(); /* 註冊並掛載 proc 檔案系統 */
nsfs_init();
cpuset_init(); /* 初始化 cpuset, cpuset 是將 CPU 和記憶體資源以邏輯性
* 和層次性整合的一種機制,是 cgroup 使用的子系統之一
*/
cgroup_init(); /* 初始化 cgroup */
taskstats_init_early(); /* 程序狀態初始化 */
delayacct_init();
check_bugs(); /* 檢查寫緩衝一致性 */
acpi_subsystem_init();
sfi_init_late();
if (efi_enabled(EFI_RUNTIME_SERVICES)) {
efi_late_init();
efi_free_boot_services();
}
ftrace_init();
rest_init(); /* rest_init 函數 */
}
start_kernel 裡面呼叫了大量的函數,每一個函數都是一個龐大的知識點,如果想要學習Linux 核心,那麼這些函數就需要去詳細的研究。本篇文章只是簡單介紹 Linux核心啟動流程,因此不會去講太多關於 Linux 核心的知識。 start_kernel 函數最後呼叫了 rest_init
rest_init 函數定義在檔案 init/main.c 中,函數內容如下:
static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
smpboot_thread_init();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which,
* if we schedule it before we create kthreadd, will OOPS.
*/
kernel_thread(kernel_init, NULL, CLONE_FS);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
complete(&kthreadd_done);
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
schedule_preempt_disabled();
/* Call into cpu_idle with preempt disabled */
cpu_startup_entry(CPUHP_ONLINE);
}
在第三行,通過呼叫函數rcu_scheduler_starting
,來啟動 RCU 鎖排程器。
第十行,呼叫函數 kernel_thread
建立 kernel_init 執行緒,也就是大名鼎鼎的 init 核心程序。init 程序的 PID 為 1。 init 程序一開始是核心程序(也就是執行在核心態),後面 init 程序會在根檔案系統中查詢名為「init」這個程式,這個「init」程式處於使用者態,通過執行這個「init」程式, init 程序就會實現從核心態到使用者態的轉變。
第十二行,呼叫函數 kernel_thread 建立 kthreadd 核心程序,此核心程序的 PID 為 2。kthreadd程序負責所有核心程序的排程和管理。
第二十五行,呼叫函數cpu_startup_entry
來進入 idle 程序, cpu_startup_entry
會呼叫cpu_idle_loop, cpu_idle_loop 是個 while 迴圈,也就是 idle 程序程式碼。 idle 程序的 PID 為 0, idle
程序叫做空閒程序,如果學過 FreeRTOS 或者 UCOS 的話應該聽說過空閒任務。 idle 空閒程序就和空閒任務一樣,當 CPU 沒有事情做的時候就在 idle 空閒程序裡面「瞎逛遊」,反正就是給CPU 找點事做。當其他程序要工作的時候就會搶佔 idle 程序,從而奪取 CPU 使用權。其實大家應該可以看到 idle 程序並沒有使用 kernel_thread 或者 fork 函數來建立,因為它是有主程序演變而來的。
在 Linux 終端中輸入ps -A
就可以列印出當前系統中的所有程序,其中就能看到 init 進
程和 kthreadd 程序:
從圖中可以看出, init 程序的 PID 為 1, kthreadd 程序的 PID 為 2。之所以圖中沒有顯示 PID 為 0 的 idle 程序,那是因為 idle 程序是核心程序。
kernel_init 函數就是 init 程序具體做的工作,定義在檔案 init/main.c 中,函數內容如下:
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable(); /* init 程序的一些其他初始化工作 */
/* need to finish all async __init code before freeing the
memory */
async_synchronize_full(); /* 等待所有的非同步呼叫執行完成 */
free_initmem(); /* 釋放 init 段記憶體 */
mark_rodata_ro();
system_state = SYSTEM_RUNNING; /* 標記系統正在執行 */
numa_default_policy();
flush_delayed_fput();
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
第五行,kernel_init_freeable 函數用於完成 init 程序的一些其他初始化工作。
第十三行,ramdisk_execute_command 是一個全域性的 char 指標變數,此變數值為「/init」,
也就是根目錄下的 init 程式。 ramdisk_execute_command 也可以通過 uboot 傳遞,在 bootargs 中使用「rdinit=xxx」即可, xxx 為具體的 init 程式名字。
第十六行,如果存在「/init」程式的話就通過函數 run_init_process 來執行此程式。
第三十九行,如果 ramdisk_execute_command 為空的話就看 execute_command 是否為空,反正不管如何一定要在根檔案系統中找到一個可執行的 init 程式。 execute_command 的值是通過uboot 傳遞,在 bootargs 中使用「init=xxxx」就可以了,比如「init=/linuxrc」表示根檔案系統中的 linuxrc 就是要執行的使用者空間 init 程式。
第四十六~四十九行,如果 ramdisk_execute_command 和 execute_command 都為空,那麼就依次查詢「/sbin/init」、「/etc/init」、「/bin/init」和「/bin/sh」,這四個相當於備用 init 程式,如果這四個也不存在,那麼 Linux 啟動失敗!
第五十二行,如果以上步驟都沒有找到使用者空間的 init 程式,那麼就提示錯誤發生!
Linux 核心最終是需要和根檔案系統打交道的,需要掛載根檔案系統,並且執行根檔案系統中的 init 程式,以此來進去使用者態。這裡就正式引出了根檔案系統,根檔案系統也是我們系統移植的最後一片拼圖。 Linux 移植三巨頭: uboot、 Linux kernel、 rootfs(根檔案系統)。
不積小流無以成江河,不積跬步無以至千里。而我想要成為萬里羊,就必須堅持學習來獲取更多知識,用知識來改變命運,用部落格見證成長,用行動證明我在努力。
如果我的部落格對你有幫助、如果你喜歡我的部落格內容,記得「點贊」 「評論」 「收藏」一鍵三連哦!聽說點讚的人運氣不會太差,每一天都會元氣滿滿呦!如果實在要白嫖的話,那祝你開心每一天,歡迎常來我部落格看看。