談談Linux系統啟動流程

GQ發表於2021-09-16

@

大體流程分析

涉及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的主要任務如下:

  1. 檢查CPU暫存器
  2. 檢查BIOS程式碼的完整性
  3. 檢查基本元件如DMA,計時器,中斷控制器
  4. 搜尋,確定系統主存大小
  5. 初始化BIOS
  6. 識別,組織,選擇出哪些裝置是可以啟動的

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

相關文章