Linux内核本身是一个程序,系统引导过程中的第一项任务就是把这个程序载入内存,以便执行它。内核的路径名是/boot/vmlinuz(在centos系中路径带版本信息的,例如/boot/vmlinuz-3.10.0-957.el7.x86_64)。
引导加载程序
现有的引导加载程序采用的是GRUB(以前的老版本还有LILO,已经废弃了,了解即可)。GRUB对于在同一主机上运行多个操作系统的用户,或者对从事内核开发的用户来说,GRUB优势明显。它对于频繁改变系统配置的用户来说也很友好。
grub命令行
grub的基本命令有reboot,find,root,kernel,help和boot命令。
reboot:软启动系统
find:在所有可以安装的分区上寻找一个文件
root:制定根设备(一个分区)
kernel:从根设备加载的内核
help:获得一条命令的交互性帮助信息
boot:以指定内核影像文件启动系统
内核启动选项示例:
init=/sbin/init:告诉内核用/sbin/init作为它的init程序
init=/bin/bash:只启动bash,在紧急恢复时有用
root=/dev/foo:告诉内核用/dev/foo作为根设备
single:引导进入单用户模式
init和运行级
init定义了7个"运行级(run level)",每一个级别都代表系统应该补充运行某些特定的服务:
1)0级是完全关闭系统的级别
2)1级或S级代表单用户模式
3)2到5级是多用户级别
4)6级是“重新引导(reboot)"的级别
实际上linux支持10个运行级,但是运行级7到9没有定义。现在大多数Linux发行版默认启动运行级5。
Ubuntu的启动脚本
启动脚本的主拷贝位于/etc/init.d下。每个脚本负责一个守护进程或者系统的某个特定方面,这里可以看一下init.d下的目录,作为系统管理员,要启动和停止各个服务,只要手工运行与之有关的init.d脚本就可以了。
CentOS的启动脚本
事实上整个Red Hat系列的发布版都类似,引导过程的配置都是通过/etc/sysconfig的配置文件来完成。另外CentOS有一个chkconfig命令来帮助用户管理服务。这个命令可以在系统中增删启动脚本,也可以管理这些脚本执行的运行级。
基础状态的初始化
内核的启动从入口函数 start_kernel() 开始。在 init/main.c 文件中,start_kernel 相当于内核的 main 函数。
这里会进行一系列的init操作:
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
char *command_line;
char *after_dashes;
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
debug_objects_early_init();
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);
early_security_init();
setup_arch(&command_line);
setup_boot_config();
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();
build_all_zonelists(NULL);
page_alloc_init();
pr_notice("Kernel command line: %s\n", saved_command_line);
/* parameters may set static keys */
jump_label_init();
parse_early_param();
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
NULL, set_init_arg);
if (extra_init_args)
parse_args("Setting extra init args", extra_init_args,
NULL, 0, -1, -1, NULL, set_init_arg);
/*
* These use large bootmem allocations and must precede
* kmem_cache_init()
*/
setup_log_buf(0);
vfs_caches_init_early();
sort_main_extable();
trap_init();
mm_init();
ftrace_init();
/* trace_printk can be enabled here */
early_trace_init();
/*
* 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();
/*
* Disable preemption - early bootup scheduling is extremely
* fragile until we cpu_idle() for the first time.
*/
preempt_disable();
if (WARN(!irqs_disabled(),
"Interrupts were enabled *very* early, fixing it\n"))
local_irq_disable();
radix_tree_init();
/*
* Set up housekeeping before setting up workqueues to allow the unbound
* workqueue to take non-housekeeping into account.
*/
housekeeping_init();
/*
* Allow workqueue creation and work item queueing/cancelling
* early. Work item execution depends on kthreads and starts after
* workqueue_init().
*/
workqueue_init_early();
rcu_init();
/* Trace events are available after this */
trace_init();
if (initcall_debug)
initcall_debug_enable();
context_tracking_init();
/* init some links before init_ISA_irqs() */
early_irq_init();
init_IRQ();
tick_init();
rcu_init_nohz();
init_timers();
hrtimers_init();
softirq_init();
timekeeping_init();
kfence_init();
/*
* For best initial stack canary entropy, prepare it after:
* - setup_arch() for any UEFI RNG entropy and boot cmdline access
* - timekeeping_init() for ktime entropy used in rand_initialize()
* - rand_initialize() to get any arch-specific entropy like RDRAND
* - add_latent_entropy() to get any latent entropy
* - adding command line entropy
*/
rand_initialize();
add_latent_entropy();
add_device_randomness(command_line, strlen(command_line));
boot_init_stack_canary();
time_init();
perf_event_init();
profile_init();
call_function_init();
WARN(!irqs_disabled(), "Interrupts were enabled early\n");
early_boot_irqs_disabled = false;
local_irq_enable();
kmem_cache_init_late();
/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong.
*/
console_init();
if (panic_later)
panic("Too many boot %s vars at `%s'", panic_later,
panic_param);
lockdep_init();
/*
* Need to run this when irqs are enabled, because it wants
* to self-test [hard/soft]-irqs on/off lock inversion bugs
* too:
*/
locking_selftest();
/*
* This needs to be called before any devices perform DMA
* operations that might use the SWIOTLB bounce buffers. It will
* mark the bounce buffers as decrypted so that their usage will
* not cause "plain-text" data to be decrypted when accessed.
*/
mem_encrypt_init();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok &&
page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",
page_to_pfn(virt_to_page((void *)initrd_start)),
min_low_pfn);
initrd_start = 0;
}
#endif
setup_per_cpu_pageset();
numa_policy_init();
acpi_early_init();
if (late_time_init)
late_time_init();
sched_clock_init();
calibrate_delay();
pid_idr_init();
anon_vma_init();
#ifdef CONFIG_X86
if (efi_enabled(EFI_RUNTIME_SERVICES))
efi_enter_virtual_mode();
#endif
thread_stack_cache_init();
cred_init();
fork_init();
proc_caches_init();
uts_ns_init();
key_init();
security_init();
dbg_late_init();
vfs_caches_init();
pagecache_init();
signals_init();
seq_file_init();
proc_root_init();
nsfs_init();
cpuset_init();
cgroup_init();
taskstats_init_early();
delayacct_init();
poking_init();
check_bugs();
acpi_subsystem_init();
arch_post_acpi_subsys_init();
kcsan_init();
/* Do the rest non-__init'ed, we're now alive */
arch_call_rest_init();
prevent_tail_call_optimization();
}
首先要有个创始进程,这里对应set_task_stack_end_magic(&init_task)。这里面有一个参数 init_task,它的定义是 struct task_struct init_task = INIT_TASK(init_task)。它是系统创建的第一个进程,我们称为 0 号进程。这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程,是进程列表的第一个。
然后我们需要有一套能处理各种系统信号的调度机制,这里面对应的函数是 trap_init(),里面设置了很多中断门(Interrupt Gate),用于处理各种中断。其中有一个 set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32),这是系统调用的中断门。系统调用也是通过发送中断的方式进行的。
接着我们要初始化内存,方便我们有个干活的场地,mm_init() 就是用来初始化内存管理??椤A硗馕颐腔剐枰鞫炔呗?,sched_init() 就是用于初始化调度??椤?br> mnt_init()->init_rootfs()。这里面有一行代码,register_filesystem(&rootfs_fs_type)。在 VFS 虚拟文件系统里面注册了一种类型,我们定义为 struct file_system_type rootfs_fs_type。文件系统是我们的项目资料库,为了兼容各种各样的文件系统,我们需要将文件的相关数据结构和操作抽象出来,形成一个抽象层对上提供统一的接口,这个抽象层就是 VFS(Virtual File System),虚拟文件系统。
最后,start_kernel() 调用的是arch_call_rest_init(),用来做其他方面的初始化工作,其中最重要的就是内核态和用户态祖先进程创建。
内核态用户态祖先进程创建
这里我们先来看一下内核态和用户态的区别。
内核态和用户态
我们的系统哪些是核心资源,哪些是非核心资源,操作需要分开的。x86 提供了分层的权限机制,把区域分成了四个 Ring,越往里权限越高,越往外权限越低。如下图所示:
操作系统很好地利用了这个机制,将能够访问关键资源的代码放在 Ring0,我们称为内核态(Kernel Mode);将普通的程序代码放在 Ring3,我们称为用户态(User Mode)。
当处于用户态的代码想要执行更高权限的指令,这种行为是被禁止的,要防止他们为所欲为。如果真的有访问的必要,需要通过系统调用统一的入口做请求。这个过程就是这样的:用户态 - 系统调用 - 保存寄存器 - 内核态执行系统调用 - 恢复寄存器 - 返回用户态,然后接着运行。
这里我们再看一下rest_init()的实现,如下所示:
noinline void __ref rest_init(void)
{
struct task_struct *tsk;
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.
*/
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
/*
* Pin init on the boot CPU. Task migration is not properly working
* until sched_init_smp() has been run. It will set the allowed
* CPUs for init to the non isolated CPUs.
*/
rcu_read_lock();
tsk = find_task_by_pid_ns(pid, &init_pid_ns);
set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
rcu_read_unlock();
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
/*
* Enable might_sleep() and smp_processor_id() checks.
* They cannot be enabled earlier because with CONFIG_PREEMPTION=y
* kernel_thread() would trigger might_sleep() splats. With
* CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled
* already, but it's stuck on the kthreadd_done completion.
*/
system_state = SYSTEM_SCHEDULING;
complete(&kthreadd_done);
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
schedule_preempt_disabled();
/* Call into cpu_idle with preempt disabled */
cpu_startup_entry(CPUHP_ONLINE);
}
用户态祖先初始化
这里首先是通过kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,也就是我们通常说的 1 号进程。1 号进程对于操作系统来讲,有“划时代”的意义。因为它将运行一个用户进程,是所有用户进程的起始,相当于用户态祖先进程。
内核态祖先初始化
那内核态的进程有没有一个人统一管起来呢?有的,rest_init 第二大事情就是第三个进程,就是 2 号进程。也就是内核态的祖先进程。这里kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) 又一次使用 kernel_thread 函数创建进程。
从内核态来看,无论是进程,还是线程,我们都可以统称为任务(Task),都使用相同的数据结构,平放在同一个链表中。这里的函数 kthreadd,负责所有内核态的线程的调度和管理,相当于内核态所有线程运行的祖先。