@
目录大体流程分析
涉及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)