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

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux 系统崩溃时,通常会发生以下情况: 1. 内核崩溃:内核崩溃是指 Linux 内核发生了致命错误,无法继续正常运行。这种情况下,系统会立刻停止运行,并显示一个崩溃信息,告诉用户发生了什么问题。这些信息通常包括错误代码、堆栈跟踪和其他详细信息。 2. 软件崩溃:软件崩溃是指用户正在运行的应用程序或服务发生了错误,导致它们崩溃或停止运行。这种情况下,系统会显示一个错误消息,通常是在应用程序崩溃时弹出的窗口。 3. 系统挂起:系统挂起是指在运行过程中,系统出现了某种问题,导致其无法响应用户的输入或操作。通常,这种情况下,系统会冻结,并显示一个错误消息,告诉用户发生了什么问题。 在分析 Linux 系统崩溃时,可以采取以下步骤: 1. 收集崩溃信息:当系统崩溃时,系统会生成一些错误信息,这些信息对于分析崩溃很有帮助。可以通过查看系统的日志文件、使用命令行工具如 dmesg 或 journalctl 来收集崩溃信息。 2. 分析崩溃信息:一旦收集了崩溃信息,就可以开始分析它们。可以通过查看错误代码、堆栈跟踪和其他详细信息来确定崩溃的原因。如果是软件崩溃,可以尝试重新启动应用程序或服务,或者卸载并重新安装它们。如果是内核崩溃,可能需要更深入的分析和修复。 3. 解决崩溃问题:一旦确定了崩溃的原因,就可以采取相应的措施来解决它。这可能涉及到更新或修复软件、更新驱动程序或内核,或者更改系统配置。在解决问题之前,最好备份系统数据,以防万一。 总之,分析 Linux 系统崩溃需要一定的技术知识和经验,但是通过收集和分析错误信息,以及采取适当的措施,可以解决大多数崩溃问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值