您現在的位置是:首頁 > 綜合

從零開始帶你搞懂Linux系統啟動流程

  • 由 充滿元氣的java愛好者 發表于 綜合
  • 2022-02-15
簡介img中跑的就是GURB(BootLoader),會根據配置資訊讓使用者選擇kernel,載入指定的kernel並傳遞核心啟動引數將真正的作業系統的kernel映象載入執行,Linux Kernel的啟動入口是 start_kernel()

扇區編輯裡面程式碼都是什麼意思

大體流程分析

涉及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上電真實模式的地址空間對映如下:

從零開始帶你搞懂Linux系統啟動流程

可以看出,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中的分佈如下:

從零開始帶你搞懂Linux系統啟動流程

當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的組成如示:

從零開始帶你搞懂Linux系統啟動流程

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命令,如下:

從零開始帶你搞懂Linux系統啟動流程

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 函式!

我簡單畫了一個框架,便於理解:

從零開始帶你搞懂Linux系統啟動流程

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()實現:

從零開始帶你搞懂Linux系統啟動流程

其中系統呼叫的中斷門是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位處理器程式碼來學習一下:

voidstart_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演變而來的,所以一開始就在核心態,無法自動儲存使用者態執行上下文的暫存器,所以手動儲存一下,然後就可以順著這套邏輯切換至使用者態了。

從零開始帶你搞懂Linux系統啟動流程

這裡很容易有一個疑惑,按照上面這個流程圖,使用者態與核心態的切換是由系統呼叫發起的,這裡並沒有實際使用系統呼叫,那如何用系統呼叫的邏輯使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

從零開始帶你搞懂Linux系統啟動流程

從零開始帶你搞懂Linux系統啟動流程

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;}

Top