Linux系统启动分析

2023-11-12

大体流程分析

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux系统启动分析 的相关文章

  • Nasm 打印到下一行

    我用 nasm Assembly 编写了以下程序 section text global start start Input variables mov edx inLen mov ecx inMsg mov ebx 1 mov eax 4
  • Mono 和 WebRequest 速度 - 测试

    在 mono 4 6 2 linux 中 我注意到 wget 下载文件的速度与webclient DownloadString 所以我做了一个小测试来调查 为什么 wget 明显比 C 快 根据我自己的实验 使用 wget 下载 手动读取文
  • 从 Linux 命令行发送 SNMP 陷阱消息

    Folks 我需要从 Linux 命令行使用此命令 snmptrap 将自定义消息发送到陷阱侦听器 我需要根据用户设置在 v1 和 v2c 中发送相同的消息 这是我发现的 For v1 snmptrap v 1 c Tas hostname
  • 如何删除树莓派的相机预览

    我在我的 raspberryPi 上安装了 SimpleCv 并安装了用于使用相机板的驱动程序 uv4l 驱动程序 现在我想使用它 当我在 simpleCV shell Camera 0 getImage save foo jpg 上键入时
  • 无法为 Python 3.4 创建工作虚拟环境

    I 安装Python 3 4 2 https docs python org 3 using unix html building python和我的 Linux Mint 17 1 中的 Virtualenv 12 0 5 然后我尝试创建
  • 我在哪里可以学习如何使 C++ 程序与操作系统 (Linux) 交互

    我是一个 C 初学者 我想创建与操作系统交互的小程序 使用 Kubuntu Linux 到目前为止 我还没有找到任何教程或手册来让 C 与操作系统交互 在 PHP 中 我可以使用命令 exec 或反引号运算符来启动通常在控制台中执行的命令
  • 测试linux下磁盘空间不足

    我有一个程序 当写入某个文件的磁盘空间不足时 该程序可能会死掉 我不确定是否是这种情况 我想运行它并查看 但我的测试服务器不会很快耗尽空间 有什么办法可以嘲笑这种行为吗 看起来没有任何方法可以在 Ubuntu 中设置文件夹 文件大小限制 并
  • 使用 sed 将 old-link-url 替换为 new-link-url

    我正在 bash 中编写一个脚本 将 old link url 替换为 new link url 我的问题是 sed 由于斜杠而无法替换 url 如果我只输入一些文字就可以了 my code sed e s old link new lin
  • 变量作为 bash 数组索引?

    bin bash set x array counter 0 array value 1 array 0 0 0 for number in array do array array counter array value array co
  • 在 Ubuntu 上纯粹通过 bash 脚本安装 mysql 5.7

    我想要一个无需任何手动输入即可安装 MySQL 5 7 实例的 bash 脚本 我正在关注数字海洋教程 https www digitalocean com community tutorials how to install mysql
  • 如何在 Linux 中使用单行命令获取 Java 版本

    我想通过单个命令获取 Linux 中的 Java 版本 我是 awk 的新手 所以我正在尝试类似的事情 java version awk print 3 但这不会返回版本 我将如何获取1 6 0 21从下面的Java版本输出 java ve
  • Xenomai 中的周期性线程实时失败

    我正在创建一个周期性线程 它在模拟输出上输出方波信号 我正在使用 Xenomai API 中的 Posix Skin 和 Analogy 我使用示波器测试了代码的实时性能 并查看了方波信号 频率为 1kHz 的延迟 我应该实现 250us
  • Linux 文本文件操作

    我有一个格式的文件 a href a href a href a href 我需要选择 之后但 之前的文本 并将其打印在行尾 添加后 例如 a href http www wowhead com search Su a a a a a
  • 使用脚本自动输入 SSH 密码

    我需要创建一个自动向 OpenSSH 输入密码的脚本ssh client 假设我需要通过 SSH 进入myname somehost用密码a1234b 我已经尝试过 bin myssh sh ssh myname somehost a123
  • sqlite 插入需要很长时间

    我正在将不到 200 000 行插入到 sqlite 数据库表中 我只是在终端中通过 sqlite3 使用一个非常简单的 sql 文件 我打赌它已经运行了至少 30 分钟 这是正常现象还是我应该关闭该过程并尝试不同的方法 sqlite中的插
  • 无关的库链接

    我有一个可能有点愚蠢的问题 因为我很确定我可能已经知道答案了 假设你有静态库A 动态共享库B和你的linux下的程序C 假设库 A 调用库 B 中的函数 并且您的程序调用库 A 中的函数 现在假设 C 在 A 中调用的所有函数都不使用 B
  • 是否从页面缓存中的脏页面进行文件读取?

    当字节写入文件时 内核不会立即将这些字节写入磁盘 而是将这些字节存储在页缓存中的脏页中 回写缓存 问题是 如果在脏页刷新到磁盘之前发出文件读取 则将从缓存中的脏页提供字节 还是首先将脏页刷新到磁盘 然后进行磁盘读取以提供字节 将它们存储在进
  • 如何通过代理将套接字连接到http服务器?

    最近 我使用 C 语言编写了一个程序 用于连接到本地运行的 HTTP 服务器 从而向该服务器发出请求 这对我来说效果很好 之后 我尝试使用相同的代码连接到网络上的另一台服务器 例如 www google com 但我无法连接并从网络中的代理
  • Linux 上的 Python 3.6 tkinter 窗口图标错误

    我正在从 Python GUI 编程手册 学习 Python GUI 某项任务要求我通过将以下代码添加到我的配方中来更改窗口图标 Change the main windows icon win iconbitmap r C Python3
  • 无法仅在控制台中启动 androidstudio

    你好 我的问题是下一个 我下载了Android Studio如果我去 路径 android studio bin 我执行studio sh 我收到以下错误 No JDK found Please validate either STUDIO

随机推荐

  • JAVA代码添加License

    在开源代码的时候 我们经常会在代码顶部添加License信息 每个文件复制粘贴显然是比较麻烦的 我们可以在工具中进行配置 在创建新的类的时候自动为我们添加相关信息 以eclipse为例 进入Preference gt Java gt Cod
  • SQL笔记(一)

    1 初识MySQL JavaEE 企业级Java开发 Web 前端 页面 展示 数据 后台 连接点 连接数据库JDBC 链接前端 控制 控制视图跳转 和给前端传递数据 数据库 存数据 ITxt Excel word 只会写代码 学好数据库
  • sqli-labs (less-39)

    sqli labs less 39 输入id 1 http 127 0 0 1 sql1 Less 39 id 1 根据错误显示判断为数字型注入 这里我们就不讲使用union注入的方法 前面的关卡讲了很多union注入的方法 我们直接使用堆
  • word:表格中的文字居中

    如 操作 如下图 选择布局 点击2就可以把表格居中了 居中结果
  • 实时ETL解决方案总结

    问题导读1 实时ETL可以选择哪些架构部件 2 实时ETL有哪些实现方法 3 实时ETL有哪些难点 1 简述在架构实时ETL时的可以选择的架构部件 答 在建立数据仓库时 ETL通常都采用批处理的方式 一般来说是每天的夜间进行跑批 随着数据仓
  • 【MyBatis】一天之内快速掌握MyBatis的增删改查

    MyBatis 文章介绍 一共不到8000字 一天之内可以快速学会mybatis的增删改查 以及经常使用的操作 不讲废话 只将眼光聚焦到实操上 也可以搭配黑马的视频观看 看完之后 可以自己找个mybatis源码课继续学习 但是一般的业务代码
  • Eclipse的安装与基本操作(详解配图)

    不为失败找理由 只为成功找方法 所有的不甘 都是因为还心存梦想 在你放弃之前 好好拼一把 只怕心老 不怕路长 文章目录 一 简介 二 下载 三 使用Eclipse编写第一个Java程序 四 Eclipse的基本操作 1 背景 2 字体 五
  • Windows10系统MySQL服务器简单安装

    Windows10系统MySQL服务器简单安装 Mysql下载 安装 修改MySQL数据保存地址 修改服务 初始化数据库 修改root密码 新增用户 Mysql下载 MySql官网下载 下载地址 https dev mysql com do
  • 创建M32F103C8T6的工程文件之后编译为什么一堆错误?

    首先先看错误的提示是不是以下的显示 Rebuild started Project STM32F103C8T6 Using Compiler V6 15 folder F keil ARM ARMCLANG Bin Rebuild targ
  • 关于vxe-table全局引入的问题

    主要讲解一下vxe table全局引入然后使用碰到的问题 0 vxe table的官网地址 1 基本环境 1 vue版本为3 x以上 我的是3 2 13 2 依赖库 xe utils 注意 这篇博客的是vue3的脚手架搭建的 如果需要看低版
  • 毕业设计-机器视觉的疲劳驾驶检测系统-python-opencv

    目录 前言 课题背景和意义 实现技术思路 实现效果图样例 前言 大四是整个大学期间最忙碌的时光 一边要忙着备考或实习为毕业后面临的就业升学做准备 一边要为毕业设计耗费大量精力 近几年各个学校要求的毕设项目越来越难 有不少课题是研究生级别难度
  • vue-admin-template,连接自己后台,二次开发必看

    第一步 找到 env development文件做如下修改 just a flag ENV development base api VUE APP BASE API api 第二步 找到 vue config js 配置跨域 关闭mock
  • Ubuntu 20版本将动态ip修改为静态ip时,ping 不通网络

    问题描述 在对Ubuntu 20版本将动态ip修改为静态ip时 ping www baidu com ping不通了 火狐浏览器没有了网路 下载不了东西 一直卡在这里不动 问题出在哪里还是配置ip dns 网关的问题 如果我们在当初安装ub
  • Spring:从零开始的Cloud生活(零)——Eureka 服务治理

    目录 Spring 从零开始的Cloud生活 零 Eureka 服务治理 1 Netfilx Eurake 2 搭建服务注册中心 3 服务提供者 4 高可用注册中心 5 服务发现和消费 之前对于SpringCloud都是一知半解的状态 现在
  • MySQL中的模糊查询

    1 表示任意0个或多个字符 可匹配任意类型和长度的字符 有些情况下若是中文 请使用两个百分号 表示 比如 SELECT FROM user WHERE u name LIKE 三 将会把u name为 张三 张猫三 三脚猫 唐三藏 等等有
  • python调用turtle(海龟画图),画一个正方形

    调用海龟画图 画一个正方形 方法一 调用海龟画图 import turtle bob turtle Turtle print bob 定义画图的方向 此处画了一个直角 bob fd 100 bob lt 90 加入以下步骤画了一个正方形 b
  • Linux Shell学习简单小结(更新中……)

    if fi bin bash 删除文件 和 新建文件 file readme function delFile if e file then rm f file echo del file fi function addFile if f
  • YOLO系列发展史

    YOLO You Only Look Once 是一种目标检测算法 由Joseph Redmon等人在2015年提出 它的主要思想是将目标检测任务看作是一个回归问题 并且可以在一个神经网络中同时预测目标的位置和类别 自2015年YOLO第一
  • ChatGPT会取代互联网程序员吗?

    ChatGPT会取代互联网程序员吗 ChatGPT是一个基于GPT 3模型的自然语言对话系统 它可以与用户进行自然 流畅 智能的对话交互 回答用户的问题 提供用户所需的信息 甚至生成一些有趣和创意的内容 ChatGPT最近火爆全网 一时间C
  • Linux系统启动分析

    文章目录 大体流程分析 一 BIOS 1 1 BIOS简介 1 2 POST 二 BootLoader GRUB 2 1 What s MBR 2 2 What s GRUB 2 3 boot img 2 4 core img lzma d