谈谈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的主要任务如下:
- 检查CPU寄存器
- 检查BIOS代码的完整性
- 检查基本组件如DMA,计时器,中断控制器
- 搜寻,确定系统主存大小
- 初始化BIOS
- 识别,组织,选择出哪些设备是可以启动的
BIOS工作在CPU和IO设备之间,因此他总是能知道计算机的所有硬件信息。如果任何的硬盘或IO设备发生变化,只需更新BIOS即可。BIOS被存储在RRPROM/FLASH内存中,BIOS不能存储在硬盘或者其他设备中,因为BIOS是管理这些设备的。BIOS使用汇编语言编写。
二.BootLoader (GRUB)
2.1 What's MBR?
BIOS确认硬件没有问题之后,就要加载执行BootLoader了,BootLoader一般放在外部的存储介质中比如磁盘,也就是我们俗称的启动盘(OS也装在其中),BootLoader并不是一次就可以全部加载的,首先会去寻找加载MBR中的代码(Master Boot Record),MBR是启动盘上的第一个扇区,大小512Bytes。
因为我们在给磁盘分区的时候,第一个扇区一般会保留一些初始化启动代码,这里的MBR就是磁盘分区的第一个扇区,最后以Magic Number 0XAA55结束(表示这是一个启动盘的MBR扇区),MBR中的分布如下:
当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
- Linux.org
- 【译】Linux启动过程分析 | Jasonkent27 (nancyyihao.github.io)
- 07 | 从BIOS到bootloader:创业伊始,有活儿老板自己上 (geekbang.org)
- GRUB (简体中文) - ArchWiki (archlinux.org)
- Linux操作系统启动管理器-GRUB_Linux教程_Linux公社-Linux系统门户网站 (linuxidc.com)
- grub2详解(翻译和整理官方手册) - 骏马金龙 - 博客园 (cnblogs.com)
- 第14章 Linux开机详细流程 - 骏马金龙 - 博客园 (cnblogs.com)
- systemd时代的开机启动流程(UEFI+systemd) | 骏马金龙 (junmajinlong.com)
- 08 | 内核初始化:生意做大了就得成立公司 (geekbang.org)
- Linux下1号进程的前世(kernel_init)今生(init进程)----Linux进程的管理与调度(六) - yooooooo - 博客园 (cnblogs.com)
谈谈Linux系统启动流程的更多相关文章
- linux基础-附件1 linux系统启动流程
附件1 linux系统启动流程 最初始阶段当我们打开计算机电源,计算机会自动从主板的BIOS(Basic Input/Output System)读取其中所存储的程序.这一程序通常知道一些直接连接在主 ...
- Linux系统启动流程及安装命令行版本
Debian安装 之前也安装过很多次linux不同版本的系统,但安装后都是直接带有桌面开发环境的版本,直接可以使用,正好最近项目不是很忙,想一直了解下Linux的整个启动流程,以及如何从命令行模式系统 ...
- 【转载】Linux系统启动流程
原文:Linux系统启动流程 POST(Power On Self Test/上电自检)-->BootLoader(MBR)-->Kernel(硬件探测.加载驱动.挂载根文件系统./sbi ...
- Linux系统启动流程及grub重建(1)
日志系统 Linux系统启动流程 PC: OS(Linux) POST-->BIOS(Boot Sequence)-->MBR(bootloader,446)-->Kernel--& ...
- Linux 入门记录:十八、Linux 系统启动流程 + 单用户修改 root 密码 + GRUB 加密
一.系统启动流程 一般来说,Linux 系统的启动流程是这样的: 1. 开机之后,位于计算机主板 ROM 芯片上的 BIOS 被最先读取,在进行硬件和内存的校验以及 CPU 的自检没有异常后, BIO ...
- Linux系统启动流程(重要!)
Linux系统启动流程 从上至下为: BIOS MBR:Boot Code 执行引导程序-GRUB(操作系统) 加载内核 执行init run level 1.BIOS(Basic Input ...
- Linux系统启动流程分析
作者:郭孝星 微博:郭孝星的新浪微博 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells Github:https://github.co ...
- linux 系统启动流程
原著资料网址:http://wenku.baidu.com/view/414127fdf705cc1755270997.html (版权归原作者所有) Linux系统的启动分5个阶段,每个阶段都完成不 ...
- [ 总结 ] Linux系统启动流程
Linux系统启动过程分析: 按下电源 --> BIOS自检 --> 系统引导(lilo/grub) --> 启动内核 --> 初始化系统 --> 用户登录 1. BIO ...
随机推荐
- 寄生线虫免疫学研究新路径!华中农业大学胡敏团队报道寄生线虫N-糖基化修饰图谱
N-糖基化修饰是真核生物中一种重要的蛋白质翻译后修饰,在许多生物学过程中起着关键作用,包括蛋白质折叠.受体-配体相互作用.免疫应答和疾病发病机制等.近年来,高精度质谱技术的出现促进了糖组和糖蛋白质组的 ...
- 腾讯技术团队整理,为什么 Flutter 能最好地改变移动开发
导语 | Flutter 框架是当下非常热门的跨端解决方案,能够帮助开发者通过一套代码库高效构建多平台精美应用,支持移动.Web.桌面等多端开发.但仍然有很多产品.设计.甚至开发同学并不了解 Flut ...
- UVa120 煎饼(选择排序思想)
题目背景 给你一迭薄煎饼,请你写一个程式来指出要如何安排才能使这些薄煎饼由上到下依薄煎饼的半径由小到大排好.所有的薄煎饼半径均不相同. 要把薄煎饼排好序需要对这些薄煎饼做翻面(flip)的动作.方法是 ...
- Vue-axios 封装了一手好axios:)
请求方式 很多种请求方式,重点还是第一种吧 下载 npm install axios --save 下载完成 直接导入 import axios from 'axios' 简单配置 axios({ u ...
- JVM-超全图
- 利用ST-LINK配合ST-LINK Utility 将bin文件下载到STM32的FLASH中
文章目录 背景 1.连接ST-LINK V2与单片机 2.配置工程 3.配置ST-LINK Utility 4.烧录bin文件 背景 项目需求,要把字模文件导入到32中FLASH的指定地址,使用了ST ...
- 【UGUI源码分析】Unity遮罩之RectMask2D详细解读
遮罩,顾名思义是一种可以掩盖其它元素的控件.常用于修改其它元素的外观,或限制元素的形状.比如ScrollView或者圆头像效果都有用到遮罩功能.本系列文章希望通过阅读UGUI源码的方式,来探究遮罩的实 ...
- msp432搭建平衡小车(二)
前言 上一节掌握了使用pwm驱动电机,接下来介绍如何使用msp432读取mpu6050数据 正文 首先我们得知道mpu6050通信方式,由于mpu6050只能用i2c通信,所以学会使用msp432的i ...
- MVVMLight学习笔记(二)---MVVMLight框架初探
一.MVVM分层概述 MVVM中,各个部分的职责如下: Model:负责数据实体的结构处理,与ViewModel进行交互: View:负责界面显示,与ViewModel进行数据和命令的交互: View ...
- 06.SpringMVC之参数绑定
默认支持的参数类型一 HttpServletRequest .HttpServletResponse .HttpSession.java.security.Principal.Locale .Inpu ...