談談Linux系統啟動流程


@

大體流程分析

涉及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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM