@
大體流程分析
涉及Linux的原始碼版本為linux-4.9.282。
- 系統上電,CPU首先去執行固化在ROM中的BIOS
- BIOS主要做硬體自檢,並去啟動盤的第一個扇區(MBR)載入執行BootLoader
- Linux系統的BootLoader這裡是GRUB,可以用Grub2工具生成BootLoader程式碼
- MBR中的boot.img會引導載入core.img中的lzma_decompress.img
- lzma_decompress.img中會將CPU切換至保護模式,並解壓執行GRUB的核心映象kernel.img
- kernel.img中跑的就是GURB(BootLoader),會根據配置資訊讓使用者選擇kernel,載入指定的kernel並傳遞核心啟動引數
- 將真正的作業系統的kernel映象載入執行,Linux Kernel的啟動入口是 start_kernel()
- start_kernel()中會進行一部分初始化工作,最後呼叫rest_init()來完成其他的初始化工作
- rest_init()中會建立系統1號程式kernel_init,kernel_init會執行ramdisk中的init程式,並切換至使用者態,載入驅動後執行真正的根檔案系統中的init程式
- rest_init()中會建立系統2號程式kthread,負責所有核心態執行緒的排程和管理,是核心態所有執行執行緒的祖先
一.BIOS
1.1 BIOS簡介
計算機系統上電之後,CPU要執行指令,CPU是什麼模式?指令放在哪?執行的指令是什麼?
上電後CPU處於真實模式,執行ROM中固化的指令,就是BIOS(Basic Input and Output System)
上電後CPU處於真實模式,只有1M的定址範圍,所以對映的記憶體地址也只有1M的範圍,在X86體系中,對於CPU上電真實模式的地址空間對映如下:
可以看出,CPU將地址0xF0000~0xFFFFF這64K的地址對映給ROM使用,BIOS的程式碼就存放在ROM中,上電之後,進行復位操作,將 CS 設定為 0xFFFF,將 IP 設定為 0x0000,所以第一條指令就會指向 0xFFFF0,正是在 ROM 的範圍內。在這裡,有一個 JMP 命令會跳到 ROM 中做初始化工作的程式碼,於是,BIOS 開始進行初始化的工作。
1.2 POST
BIOS中主要做兩件事:
- 最主要的一件事就是硬體自檢POST(Power On Self Test)
- 提供中斷服務
其中最主要的就是POST,POST主要是判斷一些硬體介面讀寫是否正常,檢查系統硬體是否存在並載入一個BootLoader,POST的主要任務如下:
- 檢查CPU暫存器
- 檢查BIOS程式碼的完整性
- 檢查基本元件如DMA,計時器,中斷控制器
- 搜尋,確定系統主存大小
- 初始化BIOS
- 識別,組織,選擇出哪些裝置是可以啟動的
BIOS工作在CPU和IO裝置之間,因此他總是能知道計算機的所有硬體資訊。如果任何的硬碟或IO裝置發生變化,只需更新BIOS即可。BIOS被儲存在RRPROM/FLASH記憶體中,BIOS不能儲存在硬碟或者其他裝置中,因為BIOS是管理這些裝置的。BIOS使用匯編語言編寫。
二.BootLoader (GRUB)
2.1 What's MBR?
BIOS確認硬體沒有問題之後,就要載入執行BootLoader了,BootLoader一般放在外部的儲存介質中比如磁碟,也就是我們俗稱的啟動盤(OS也裝在其中),BootLoader並不是一次就可以全部載入的,首先會去尋找載入MBR中的程式碼(Master Boot Record),MBR是啟動盤上的第一個扇區,大小512Bytes。
因為我們在給磁碟分割槽的時候,第一個扇區一般會保留一些初始化啟動程式碼,這裡的MBR就是磁碟分割槽的第一個扇區,最後以Magic Number 0XAA55結束(表示這是一個啟動盤的MBR扇區),MBR中的分佈如下:
當BIOS識別到合法的MBR之後,就會將MBR中的程式碼載入到記憶體中執行,這部分程式碼是如何產生的?執行這部分程式碼有什麼用?下面就來探討一下MBR中的啟動程式碼,不過首先得了解一下GRUB。
2.2 What's GRUB?
GRUB是一個BootLoader,可以在系統中選擇性的引導不同的OS,實際上就是載入引導不同的Kernel映象,當Kernel掛載成功之後就將控制權交給Kernel。
如何將啟動程式安裝到磁碟中?Linux中有一個工具,叫 Grub2,全稱 Grand Unified Bootloader Version 2。顧名思義,就是搞系統啟動的。使用 grub2-install /dev/sda,可以將啟動程式安裝到相應的位置。
如果使用的是傳統的grub,則安裝的boot loader為stage1、stage1_5和stage2,如果使用的是grub2,則安裝的是boot.img和core.img,這裡介紹grub2
2.3 boot.img
Grub2會先安裝MBR中的程式碼,也就是boot.img,由boot.S編譯而來,所以知道了MBR中的程式碼就是boot.S,而且可以由Grub2載入到MBR中!
當BIOS完成自己的任務之後,就會把boot.img從MBR中載入到記憶體中(0X7C00)執行,這裡就解釋了上面的問題:MBR中的程式碼是如何產生的?
還有一個問題:執行MBR中的程式碼有什麼作用? 也可以理解為boot.img有什麼作用?
由於boot.img大小為MBR的大小,即512Bytes,做不了太多的事情,可以把boot.img理解為UBoot中的SPL,UBoot中的SPL是一個很小的loader程式碼,可以執行於SOC的內部SRAM中,它的主要功能就是載入執行真正的UBoot。
所以boot.img的使命就是載入GRUB的另一個映象core.img
2.4 core.img
core.img 由 lzma_decompress.img、diskboot.img、kernel.img 和一系列的模組組成,功能比較豐富,能做很多事情,core.img的組成如示:
boot.img 先載入的是 core.img 的第一個扇區。如果從硬碟啟動的話,這個扇區裡面是 diskboot.img,對應的程式碼是 diskboot.S。
boot.img 將控制權交給 diskboot.img 後,diskboot.img 的任務就是將 core.img 的其他部分載入進來,先是解壓縮程式 lzma_decompress.img(這裡的GURB Kernel映象是壓縮過的,所以要先載入解壓縮程式),再往下是 kernel.img,最後是各個模組 module 對應的映像。這裡需要注意,它不是 Linux 的核心,而是 GRUB 的核心。
lzma_decompress.img 切換CPU到保護模式
lzma_decompress.img 對應的程式碼是 startup_raw.S,lzma_decompress.img中乾的事很重要!!!在此之前,CPU還是真實模式,只有1M的定址範圍,後期的程式是不可能跑在這1M的空間中,所以在lzma_decompress.img中會首先呼叫real_to_prot,將CPU從真實模式切換到保護模式,以獲得更大的定址空間方便載入後續的程式!!!
關於CPU從真實模式到保護模式的切換,要幹很多事情,不僅僅是定址範圍的擴大,還涉及到很多許可權相關的問題,這裡簡單羅列一下切換到保護模式做的事情:
- 啟動分段:在記憶體中建立段描述符,將段暫存器變成段選擇子,段選擇子指向段描述符,可以方便實現程式切換
- 啟動分頁:便於管理記憶體與實現虛擬記憶體
- 開啟Gate A20:切換保護模式的函式 DATA32 call real_to_prot 會開啟 Gate A20,也就是第 21 根地址線的控制線。
這樣一來,CPU就切換到了保護模式,有了足夠的定址範圍來執行接下來的程式, startup_raw.S會對kernel.img進行解壓,然後去執行kernel.img中的程式碼,注意這裡的kernel.img指的是GURB的kernel,並不是作業系統的Kernel,因為我們需要執行GURB來引導載入作業系統的Kernel。
kernel.img 選擇載入 Linux Kernel Image
kernel.img 對應的程式碼是 startup.S 以及一堆 c 檔案,在 startup.S 中會呼叫 grub_main,這是 GRUB kernel 的主函式,GURB中會解析grub.conf配置檔案,瞭解到系統中所存在的作業系統,然後通過視覺化介面,通過使用者反饋選中需要載入的作業系統,裝載指定的核心檔案,並傳遞核心啟動引數。
從grub_main函式開始分析,grub_load_config()會解析grub.conf配置檔案,在這裡獲取到可載入的Kernel資訊。後面呼叫 grub_command_execute (“normal”, 0, 0),最終會呼叫 grub_normal_execute() 函式。在這個函式裡面,grub_show_menu() 會顯示出讓你選擇的那個作業系統的列表,使用者選中之後,就會呼叫grub_menu_execute_entry() ,開始解析並載入使用者選擇的那一項作業系統。
比如GRUB中的linux16命令,就是裝載指定的Kernel並傳遞啟動引數的,於是 grub_cmd_linux() 函式會被呼叫,它會首先讀取 Linux 核心映象頭部的一些資料結構,放到記憶體中的資料結構來,進行檢查。如果檢查通過,則會讀取整個 Linux 核心映象到記憶體。如果配置檔案裡面還有 initrd 命令,用於為即將啟動的核心傳遞 init ramdisk 路徑。於是 grub_cmd_initrd() 函式會被呼叫,將 initramfs 載入到記憶體中來。當這些事情做完之後,grub_command_execute (“boot”, 0, 0) 才開始真正地啟動核心。
關於GRUB中的linux16命令,如下:
Grub2的學習可以參考:grub2詳解(翻譯和整理官方手冊) - 駿馬金龍 - 部落格園 (cnblogs.com)
三.Kernel Init
3.1 Unpack the kernel
到目前為止,核心已經被載入到記憶體並且掌握了控制權,且收到了boot loader最後傳遞的核心啟動引數,包括init ramdisk映象的路徑,但是所有的核心映象都是以bzImage方式壓縮過的,所以需要對核心映象進行解壓!
核心引導協議要求bootloader最後將核心映象讀取到記憶體中,核心映象是以bzImage格式被壓縮。bootloader讀取核心映象到記憶體後,會呼叫核心映象中的startup_32()函式對核心解壓,也就是說,核心是自解壓的。解壓之後,核心被釋放,開始呼叫另一個startup_32()函式(同名),startup32函式初始化核心啟動環境,然後跳轉到start_kernel()函式,核心就開始真正啟動了,PID=0的0號程式也開始了……
解壓釋放Kernel之後,將建立pid為0的idle程式,該程式非常重要,後續核心所有的程式都是通過fork它建立的,且很多cpu降溫工具就是強制執行idle程式來實現的。然後建立pid=1和pid=2的核心程式。pid=1的程式也就是init程式,pid=2的程式是kthread核心執行緒,它的作用是在真正呼叫init程式之前完成核心環境初始化和設定工作,例如根據grub傳遞的核心啟動引數找到init ramdisk並載入。
已經建立的pid=1的init程式和pid=2的kthread程式,但注意,它們都是核心執行緒,全稱是kernel_init和kernel_kthread,而真正能被ps捕獲到的pid=1的init程式是由kernel_init呼叫init程式後形成的。
3.2 start_kernel()
核心的啟動從入口函式 start_kernel() 開始,位於核心原始碼的 init/main.c 檔案中,start_kernel 相當於核心的 main 函式!
我簡單畫了一個框架,便於理解:
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
set_task_stack_end_magic(&init_task); //初始化0號程式
smp_setup_processor_id();
debug_objects_early_init();
/*
* Set up the the initial canary ASAP:
*/
boot_init_stack_canary();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_disabled = true;
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
boot_cpu_init();
page_address_init();
pr_notice("%s", linux_banner);
setup_arch(&command_line); //架構相關的初始化
mm_init_cpumask(&init_mm);
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas();
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
boot_cpu_hotplug_init();
/* ...... */
/*
* These use large bootmem allocations and must precede
* kmem_cache_init()
*/
setup_log_buf(0);
pidhash_init();
vfs_caches_init_early();
sort_main_extable();
trap_init(); //設定中斷門 處理各種中斷 具體的實現和架構相關
mm_init(); //初始化記憶體管理模組,初始化buddy allocator、slab
/*
* 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(); //初始化排程模組
/* ...... */
kmem_cache_init_late(); //完成slab初始化的最後一步工作
/* ...... */
thread_stack_cache_init();
cred_init();
fork_init(); //設定程式管理器,為task_struct建立slab快取
proc_caches_init();
buffer_init(); //設定buffer快取,為buffer_head建立slab快取
key_init();
security_init();
dbg_late_init();
vfs_caches_init(); //設定VFS子系統,為VFS data structs建立slab快取
signals_init(); //POSIX訊號機制初始化
/* rootfs populating might need page-writeback */
page_writeback_init();
proc_root_init();
nsfs_init();
cpuset_init();
cgroup_init();
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();
/* Do the rest non-__init'ed, we're now alive */
rest_init();
prevent_tail_call_optimization();
}
start_kernel()的一些重點工作如下:
- set_task_stack_end_magic(&init_task):為系統建立的第一個程式設定stack,0號程式
- setup_arcg():進行一些架構相關的設定,包括設定kernel的data、code空間;設定頁表
- trap_init():初始化中斷門,包括了系統呼叫的中斷
- mm_init():初始化記憶體管理系統,包括buddy allocator初始化;開始slab分配器初始化(由kmem_cache_init_late()完成初始化收尾工作)
- sched_init():初始化排程系統,建立相關資料結構
- fork_init():初始化程式控制,為task_struct建立slab快取
- vfs_caches_init():初始化VFS系統,VFS data structs建立slab快取
- 呼叫rest_init():完成其他初始化工作
靜態建立0號程式init_task
set_task_stack_end_magic(&init_task);
中的init_task是系統建立的第一個程式,稱為0號程式,是唯一一個沒有通過fork()或者kernel_thread產生的程式,其初始化如下:
/* init_task.c@init */
struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);
/* init_task.h@include/linux */
/*
* INIT_TASK is used to set up the first task table, touch at
* your own risk!. Base=0, limit=0x1fffff (=2MB)
*/
#define INIT_TASK(tsk) \
{ \
INIT_TASK_TI(tsk) \
.state = 0, \
.stack = init_stack, \
.usage = ATOMIC_INIT(2), \
.flags = PF_KTHREAD, \
/* ...... */
}
setup_arch(&command_line)
setup_arch(&command_line);
中實現了體系相關的初始化。這裡展示一下arm64架構下的程式碼:
void __init setup_arch(char **cmdline_p)
{
pr_info("Boot CPU: AArch64 Processor [%08x]\n", read_cpuid_id());
sprintf(init_utsname()->machine, UTS_MACHINE);
init_mm.start_code = (unsigned long) _text;
init_mm.end_code = (unsigned long) _etext;
init_mm.end_data = (unsigned long) _edata;
init_mm.brk = (unsigned long) _end;
*cmdline_p = boot_command_line;
early_fixmap_init();
early_ioremap_init();
setup_machine_fdt(__fdt_pointer);
parse_early_param();
/*
* Unmask asynchronous aborts after bringing up possible earlycon.
* (Report possible System Errors once we can report this occurred)
*/
local_async_enable();
/*
* TTBR0 is only used for the identity mapping at this stage. Make it
* point to zero page to avoid speculatively fetching new entries.
*/
cpu_uninstall_idmap();
xen_early_init();
efi_init();
arm64_memblock_init(); //暫時使用memblock allocator作為記憶體分配器,buddy allocator準備完畢後捨棄
/*
* paging_init() sets up the page tables, initialises the zone memory
* maps and sets up the zero page.
*/
paging_init(); //設定頁表
acpi_table_upgrade();
/* Parse the ACPI tables for possible boot-time configuration */
acpi_boot_table_init();
if (acpi_disabled)
unflatten_device_tree();
bootmem_init();
kasan_init();
request_standard_resources();
early_ioremap_reset();
if (acpi_disabled)
psci_dt_init();
else
psci_acpi_init();
cpu_read_bootcpu_ops();
smp_init_cpus();
smp_build_mpidr_hash();
#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
conswitchp = &dummy_con;
#endif
#endif
if (boot_args[1] || boot_args[2] || boot_args[3]) {
pr_err("WARNING: x1-x3 nonzero in violation of boot protocol:\n"
"\tx1: %016llx\n\tx2: %016llx\n\tx3: %016llx\n"
"This indicates a broken bootloader or old kernel\n",
boot_args[1], boot_args[2], boot_args[3]);
}
}
在setup_arch()中主要做的事有:
- 解析早期的命令列引數,根據使用者的定義,構建記憶體對映框架
- arm64_memblock_init():暫時使用memblock allocator作為記憶體分配器,buddy allocator準備完畢後捨棄
- paging_init():sets up the page tables, initialises the zone memory maps and sets up the zero page.
- request_standard_resources():構建核心空間的code、data段空間
trap_init()
trap_init()
裡面設定了很多中斷門,用來處理各種中斷服務,這個函式的實現是體系相關的,下面是X86架構的trap_init()實現:
其中系統呼叫的中斷門是set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_compat);
mm_init()
mm_init()
初始化記憶體管理模組,包括了:
- mem_init():buddy allocator初始化
- kmem_cache_init():slab快取機制初始化開始,由kmem_cache_init_late()完成初始化收尾工作
/*
* Set up kernel memory allocators
*/
static void __init mm_init(void)
{
/*
* page_ext requires contiguous pages,
* bigger than MAX_ORDER unless SPARSEMEM.
*/
page_ext_init_flatmem();
mem_init();
kmem_cache_init();
percpu_init_late();
pgtable_init();
vmalloc_init();
ioremap_huge_init();
kaiser_init();
}
sched_init()
sched_init()
用來初始化排程模組,主要是初始化排程相關的資料結構。
fork_init()
fork_init()設定程式管理器,為task_struct建立slab快取
vfs_caches_init()
vfs_caches_init()設定VFS子系統,為VFS data structs建立slab快取。
vfs_caches_init() 會用來初始化基於記憶體的檔案系統 rootfs。在這個函式裡面,會呼叫 mnt_init()->init_rootfs()。這裡面有一行程式碼:register_filesystem(&rootfs_fs_type)
在 VFS 虛擬檔案系統裡面註冊了一種型別,我們定義為 struct file_system_type rootfs_fs_type。檔案系統是我們的專案資料庫,為了相容各種各樣的檔案系統,我們需要將檔案的相關資料結構和操作抽象出來,形成一個抽象層對上提供統一的介面,這個抽象層就是 VFS(Virtual File System),虛擬檔案系統。
3.3 rest_init()
在rest_init()中,主要的工作有以下兩點:
- kernel_thread(kernel_init, NULL, CLONE_FS):建立kernel_init(Linux系統的1號程式),由kernel_init演變出使用者態的1號init程式
- kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES):建立kthreadd(Linux系統的2號程式),由kthreadd建立、管理核心的後續執行緒
static noinline void __ref rest_init(void)
{
int pid;
rcu_scheduler_starting();
/*
* 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); //建立系統1號程式
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); //建立系統2號程式
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);
}
這裡用到kernel_thread()
,kernel_thread()
就是建立一個核心執行緒並返回pid,看一下kernel_thread()的原始碼:
/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL, 0);
}
kernel_init到init程式的演變
這一塊要明確兩個問題:
- kernel_init是1號程式,如何才可以讓kernel_init具有init程式的功能?
- kernel_init處於核心態中,init是使用者程式,在使用者態中執行,如何實現核心態到使用者態的轉變?
首先關注一下kernel_init()的原始碼:
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable();
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
free_initmem();
mark_readonly();
system_state = SYSTEM_RUNNING;
numa_default_policy();
rcu_end_inkernel_boot();
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); //執行init程式的程式碼,並從核心態返回至使用者態
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.");
}
do_execve系統呼叫實現init程式的功能
在kernel_init_freeable()
中會有操作:ramdisk_execute_command = "/init";
,kernel_init()中對應的部分如下:
/*
* 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;
可以看到kernel_init中是要去執行init程式的,init程式的程式碼都以可執行ELF檔案的形式存在的,kernel_init通過呼叫run_init_process()和try_to_run_init_process()介面來執行對應的可執行檔案,兩種原理都是一樣的,都是通過do_execve()系統呼叫來實現,可以對比以下兩個介面的原始碼:
static int run_init_process(const char *init_filename)
{
argv_init[0] = init_filename;
return do_execve(getname_kernel(init_filename),
(const char __user *const __user *)argv_init,
(const char __user *const __user *)envp_init);
}
static int try_to_run_init_process(const char *init_filename)
{
int ret;
ret = run_init_process(init_filename);
if (ret && ret != -ENOENT) {
pr_err("Starting init: %s exists but couldn't execute it (error %d)\n",
init_filename, ret);
}
return ret;
}
瞭解execve系統呼叫的同學肯定知道其中的原理,這裡就不作過多說明了,kernel_init就是這樣來實現init程式的功能,利用了1號程式的環境,跑的是init程式的程式碼,即嘗試執行 ramdisk 的“/init”,或者普通檔案系統上的“/sbin/init”、“/etc/init”、“/bin/init”、“/bin/sh”。不同版本的 Linux 會選擇不同的檔案啟動,只要有一個起來了就可以。
在這之後,就稱1號程式為init程式啦!
init程式實現從核心態到使用者態的切換
還有一個問題,那就是1號程式是由start_kernel()
中靜態建立的0號程式所建立的,隸屬於核心態,現在只是跑了init程式的程式碼,而init程式是執行在使用者態中的,所以還需要讓init程式從核心態切換到使用者態。
要注意: 一開始到使用者態的是 ramdisk 的 init程式,後來會啟動真正根檔案系統上的 init,成為所有使用者態程式的祖先。
這就得跟蹤一下run_init_process()
介面的實現了,直接上原始碼:
static int run_init_process(const char *init_filename)
{
argv_init[0] = init_filename;
return do_execve(getname_kernel(init_filename),
(const char __user *const __user *)argv_init,
(const char __user *const __user *)envp_init);
}
裡面是呼叫do_execve實現的,再跟蹤原始碼:
int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}
裡面還是呼叫do_execveat_common()
介面,繼續跟蹤原始碼:
/*
* sys_execve() executes a new program.
*/
static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
......
struct linux_binprm *bprm;
......
retval = exec_binprm(bprm);
......
}
重點是裡面的exec_binprm()
,繼續跟原始碼:
static int exec_binprm(struct linux_binprm *bprm)
{
pid_t old_pid, old_vpid;
int ret;
/* Need to fetch pid before load_binary changes it */
old_pid = current->pid;
rcu_read_lock();
old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
rcu_read_unlock();
ret = search_binary_handler(bprm);
if (ret >= 0) {
audit_bprm(bprm);
trace_sched_process_exec(current, old_pid, bprm);
ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
proc_exec_connector(current);
}
return ret;
}
重點是裡面的search_binary_handler()介面
,原始碼如下:
int search_binary_handler(struct linux_binprm *bprm)
{
......
struct linux_binfmt *fmt;
......
retval = fmt->load_binary(bprm);
......
}
EXPORT_SYMBOL(search_binary_handler);
重點是fmt->load_binary(bprm);
介面的實現,關於struct linux_binfmt *fmt;
,簡單介紹一下:
/*
* This structure defines the functions that are used to load the binary formats that
* linux accepts.
*/
struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
};
Linux中常用的可執行檔案的格式是ELF,所以我們去看一下ELF檔案的struct linux_binfmt
是如何定義的:
/* binfmt_elf.c@fs */
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};
所以上面的fmt->load_binary(bprm)
操作呼叫的就是load_elf_binary
介面,跟蹤原始碼:
static int load_elf_binary(struct linux_binprm *bprm)
{
unsigned long elf_entry;
struct pt_regs *regs = current_pt_regs();
......
start_thread(regs, elf_entry, bprm->p);
......
}
這裡的start_thread()
實現是架構相關的,可以根據X86架構的32位處理器程式碼來學習一下:
void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0);
regs->fs = 0;
regs->ds = __USER_DS;
regs->es = __USER_DS;
regs->ss = __USER_DS;
regs->cs = __USER_CS;
regs->ip = new_ip;
regs->sp = new_sp;
regs->flags = X86_EFLAGS_IF;
force_iret();
}
其中的struct pt_regs
成員如下:
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_ax;
/* Return frame for iretq */
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
struct pt_regs
就是在系統呼叫的時候,核心中用於儲存使用者態上下文環境的(儲存使用者態的暫存器),以便結束後根據儲存暫存器的值恢復使用者態。
為什麼start_thread()
中要設定這些暫存器的值呢?因為這裡需要由核心態切換至使用者態,使用系統呼叫的邏輯來完成使用者態的切換,可以參考下圖,整個邏輯需要先儲存使用者態的執行上下文,也就是儲存暫存器,然後執行核心態邏輯,最後恢復暫存器,從系統呼叫返回到使用者態。這裡由於init程式是由0號程式建立的1號程式kernel_init演變而來的,所以一開始就在核心態,無法自動儲存使用者態執行上下文的暫存器,所以手動儲存一下,然後就可以順著這套邏輯切換至使用者態了。
這裡很容易有一個疑惑,按照上面這個流程圖,使用者態與核心態的切換是由系統呼叫發起的,這裡並沒有實際使用系統呼叫,那如何用系統呼叫的邏輯使init程式切換回使用者態???
這裡我們直接手動強制返回系統呼叫,通過force_iret();
實現,看一下原始碼:
/*
* Force syscall return via IRET by making it look as if there was
* some work pending. IRET is our most capable (but slowest) syscall
* return path, which is able to restore modified SS, CS and certain
* EFLAGS values that other (fast) syscall return instructions
* are not able to restore properly.
*/
#define force_iret() set_thread_flag(TIF_NOTIFY_RESUME)
#define TIF_NOTIFY_RESUME 1 /* callback before returning to user */
所以,返回使用者態的時候,CS 和指令指標暫存器 IP 恢復了,指向使用者態下一個要執行的語句。DS 和函式棧指標 SP 也被恢復了,指向使用者態函式棧的棧頂。所以,下一條指令,就從使用者態開始執行了。即成功實現init程式從核心態到使用者態的切換。
一開始到使用者態的是 ramdisk 的 init程式,後來會啟動真正根檔案系統上的 init,成為所有使用者態程式的祖先。
為什麼要有ramdisk
ramdisk的作用
從上面kernel_init到init程式的演變,可以知道,init程式首選的就是/init
可執行檔案,也就是存在於ramdisk中的init程式,為什麼剛開始要用ramdisk的init呢?
因為init程式是以可執行檔案的形式存在的,檔案存在的前提就是有檔案系統,正常情況下檔案系統又是基於硬體儲存裝置的,比如硬碟。所以Linux中訪問檔案是建立在訪問硬碟的基礎上的,即基於訪問外設的基礎,既然要訪問外設,就要有驅動,而不同的硬碟驅動程式又各不相同,如果在啟動階段去訪問基於硬碟的檔案系統,就需要向核心提供各種硬碟的驅動程式,雖然可以直接將驅動程式放在核心中,但考慮到市面上數量眾多的儲存介質,如果把所有的驅動程式都考慮就去就會使得核心過於龐大!
為了解決這個痛點,可以先搞一個基於記憶體的檔案系統,訪問這個檔案系統不需要儲存介質的驅動程式,因為檔案系統就抽象在記憶體中,也就是ramdisk,在這個啟動階段,ramdisk就是根檔案系統。
那什麼時候可以由基於記憶體的根檔案系統ramdisk過渡到基於儲存介質的實際的檔案系統呢?在ramdisk中的/init
程式跑起來之後,/init 這個程式會先根據儲存系統的型別載入驅動,有了儲存介質的驅動就可以設定真正的根檔案系統了。有了真正的根檔案系統,ramdisk 上的 /init 會啟動基於儲存介質的檔案系統上的 init程式。
這個時候,真正的根檔案系統準備就緒,ramdisk中的init程式會啟動根檔案系統上的init程式,接下來就是各種系統初始化,然後啟動系統服務、啟動控制檯、顯示使用者登入頁面。
這裡!!!基於儲存介質的根檔案系統中的init程式,才是使用者態所有程式的實際祖先!!!
initrd與initfs
kthreadd
kthreadd函式是系統的2號程式,也是系統的第三個程式,負責所有核心態執行緒的排程和管理,是核心態所有執行執行緒的祖先。
int kthreadd(void *unused)
{
struct task_struct *tsk = current;
/* Setup a clean context for our children to inherit. */
set_task_comm(tsk, "kthreadd");
ignore_signals(tsk);
set_cpus_allowed_ptr(tsk, cpu_all_mask);
set_mems_allowed(node_states[N_MEMORY]);
current->flags |= PF_NOFREEZE;
cgroup_init_kthreadd();
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list))
schedule();
__set_current_state(TASK_RUNNING);
spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) {
struct kthread_create_info *create;
create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list);
spin_unlock(&kthread_create_lock);
create_kthread(create);
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
}
return 0;
}
四.References
- Linux.org
- 【譯】Linux啟動過程分析 | Jasonkent27 (nancyyihao.github.io)
- 07 | 從BIOS到bootloader:創業伊始,有活兒老闆自己上 (geekbang.org)
- GRUB (簡體中文) - ArchWiki (archlinux.org)
- Linux作業系統啟動管理器-GRUB_Linux教程_Linux公社-Linux系統入口網站 (linuxidc.com)
- grub2詳解(翻譯和整理官方手冊) - 駿馬金龍 - 部落格園 (cnblogs.com)
- 第14章 Linux開機詳細流程 - 駿馬金龍 - 部落格園 (cnblogs.com)
- systemd時代的開機啟動流程(UEFI+systemd) | 駿馬金龍 (junmajinlong.com)
- 08 | 核心初始化:生意做大了就得成立公司 (geekbang.org)
- Linux下1號程式的前世(kernel_init)今生(init程式)----Linux程式的管理與排程(六) - yooooooo - 部落格園 (cnblogs.com)