很多朋友都想知道linux如何启动服务?下面就一起来看看吧!
linux如何启动服务
1、打开linux系统,在linux的桌面的空白处右击。
2、在弹出的下拉选项里,点击打开终端。
3、在终端中使用命令ntsysv 然后回车即可。
从零开始带你搞懂Linux系统启动流程
大体流程分析
涉及linux的源码版本为linux-4.9.282。
计算机系统上电之后,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 POSTBIOS中主要做两件事:
其中最主要的就是POST,POST主要是判断一些硬件接口读写是否正常,检查系统硬件是否存在并加载一个BootLoader,POST的主要任务如下:
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.imgGrub2会先安装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.imgcore.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从实模式到保护模式的切换,要干很多事情,不仅仅是寻址范围的扩大,还涉及到很多权限相关的问题,这里简单罗列一下切换到保护模式做的事情:
这样一来,CPU就切换到了保护模式,有了足够的寻址范围来执行接下来的程序, startup_raw.S会对kernel.img进行解压,然后去运行kernel.img中的代码,注意这里的kernel.img指的是GURB的kernel,并不是操作系统的Kernel,因为我们需要运行GURB来引导加载操作系统的Kernel。
kernel.img 选择加载 Linux Kernel Imagekernel.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 Init3.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);中的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 [x]\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: 6llx\n\tx2: 6llx\n\tx3: 6llx\n" "This indicates a broken bootloader or old kernel\n", boot_args[1], boot_args[2], boot_args[3]); }}
在setup_arch()中主要做的事有:
trap_init()里面设置了很多中断门,用来处理各种中断服务,这个函数的实现是体系相关的,下面是X86架构的trap_init()实现:
其中系统调用的中断门是set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_compat);
mm_init()mm_init()初始化内存管理模块,包括了:
/* * 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()中,主要的工作有以下两点:
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()的源码:
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位处理器代码来学习一下:
voidstart_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,成为所有用户态进程的祖先。
为什么要有ramdiskramdisk的作用从上面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与initfskthreaddkthreadd函数是系统的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;}