MOOC课程《Linux内核分析》——start_kernel();Idle进程与Init进程

时间:2021-07-27 06:18:51

许松,原创作品转载请注明出处。
《Linux内核分析》MOOC课程

Linux-Kernel源代码树的目录结构可以参见百度文库:Linux源代码目录树结构,这里不再赘述。我们当前主要关注arc/x86initkernelipc等。

linux内核启动相关的代码基本都在init目录下。内核初始化开始于init目录下的main.c文件中的start_kernel函数(位于第500行~第680行)。

lockdep_init();

首先初始化lockdep hash table(第3975行)。lockdep_hash_table用来解决OS中死锁问题的。lockdep的介绍可以参考文章:死锁检测模块lockdep简介以及arm linux启动流程二

set_task_stack_end_magic(&init_task);
// 该函数对应源码是:
// void set_task_stack_end_magic(struct task_struct *tsk)
// {
// unsigned long *stackend;

// stackend = end_of_stack(tsk);
// *stackend = STACK_END_MAGIC; /* for overflow detection */
// }
// end_of_stack(p)是一个宏:
// #define end_of_stack(p) (unsigned long *)((void *)(p) + IA64_RBS_OFFSET)

该句对初始进程(也就是0号进程)的内核栈进行了处理,STACK_END_MAGIC则是用来对内核栈溢出进行检测的。下图可以看到init_task的pid = 0。
MOOC课程《Linux内核分析》——start_kernel();Idle进程与Init进程

smp_setup_processor_id();//函数体为空,不做工作

这个函数是针对SMP硬件体系的,用来获取当前工作的CPU的ID。对于单核平台,该函数定义为空。

debug_objects_early_init();

对调试对象(自旋锁以及静态对象池)进行初始化。

boot_init_stack_canary();

初始化堆栈保护的canary值,用来防止栈溢出攻击。

cgroup_init_early();

该函数进行控制组(control group)的早期初始化。control group的有概念可参考文章:Linux cgroup机制分析之框架分析

local_irq_disable();
early_boot_irqs_disabled = true;

关闭当前CPU的所有中断响应。之后本CPU上的中断将会被关闭,直到所需的步骤完成之后才能再次开中断。

boot_cpu_init();
// 函数对应源码是:
// static void __init boot_cpu_init(void)
// {
// int cpu = smp_processor_id();
// /* Mark the boot cpu "present", "online" etc for SMP and UP case */
// set_cpu_online(cpu, true);
// set_cpu_active(cpu, true);
// set_cpu_present(cpu, true);
// set_cpu_possible(cpu, true);
// }

该函数获取当前工作的CPU的ID,并将其应用到当前环境中,也就是激活当前CPU。

page_address_init();

初始化高端内存映射表。

setup_arch(&command_line);

该函数位于arch/x86/kernel/setup.c中。对于不同的架构,Linux提供了不同的内核初始化方式。

接下来内核进行以下动作:

  • mm_init_owner(&init_mm, &init_task) 初始化内核本身是用的内存管理结构体系。init_mm定义了整个kernel的内存空间。
  • mm_init_cpumask(&init_mm) 每一个任务都有一个mm_struct结构来管理内存空间,init_mm是内核的mm_struct。
  • setup_command_line(command_line); 对cmdline进行备份和保存。
  • setup_nr_cpu_ids() 设置有多少个nr_cpu_ids结构。
  • setup_per_cpu_areas() 为系统中的每一个CPU的per_cpu变量申请空间,同时拷贝初始化数据
  • smp_prepare_boot_cpu(); 为SMP系统里引导CPU(boot-cpu)进行准备工作。在ARM系统中是空函数。
  • build_all_zonelists(NULL,NULL) 设置内存管理相关的node(每个CPU一个内存node)和其中的Zone,以完成内存管理子系统的初始化,并设置bootmem分配器 。
  • page_alloc_init() 设置内存页分配器。
  • parse_early_param() 解析cmdline中的启动参数。
  • jump_label_init() …待查找
  • setup_log_buf(0) 使用bootmem分配一个记录启动信息的缓冲区。
  • pidhash_init() 进程ID的HASH表初始化,用bootmem分配并初始化PID散列表。由pid分配器管理空闲和已指派的pid。
  • vfs_caches_init_early() 前期虚拟文件系统(vfs)的缓存初始化。
  • sort_main_extable() 对内核异常表按照异常向量号大小排序以加速访问。
  • trap_init() 对内核异常进行初始化。ARM系统中是空函数。
  • mm_init() 标记那些内存可以使用,并高度系统有多少内存可以使用(除了内核已使用内存)
  • sched_init() 对进程调度器的数据结构进行初始化,创建运行队列,设置当前任务的空闲线程,当前任务的调度策略为CFS。
  • preempt_disable() 关闭优先级调度。
  • idr_init_cache() 为IDR机制分配缓存。
  • rcu_init() 初始化直接读拷贝更新的锁机制。RCU(Read_Copy_Upate)。
  • radix_tree_init() 内核radis树算法初始化。
  • early_irq_init() 前期外部中断描述符初始化。
  • init_IRQ() 对应架构特定的中断初始化函数。
  • prio_tree_init() 初始化内核基于radix树的优先级搜索树(PST),初始化结构体。
  • init_timers() 初始化引导CPU的时钟相关的数据结构,注册时钟的回调函数,当时钟到达时可以回调时钟处理函数,最后初始化时钟软件中断处理。
  • hrtimers_init() 初始化高精度的定时器,并设置回调函数。
  • softirq_init() 初始化软中断。
  • timekeeping_init() 初始化系统时钟计时,并且初始化内核里与时钟计时相关的变量。
  • time_init() 初始化系统时钟。
  • sched_colock_postinit()
  • perf_event_init() CPU性能监视机制初始化。
  • profile_init() 分配内和性能统计保存的内存,以便统计的性能变量可以保存到这里。
  • call_function_init() 初始化所有CPU的call_single_queue,同时注册CPU热插拔通知函数到CPU通知链中。
early_boot_irqs_disabled = false;
local_irq_enable();

对应前面的loacl_irq_disable(),打开CPU的中断。

  • kmem_cahce_init_late() 内核启动时使用临时内存分配器bootmem,之后由slab接管。kmem_cache_init_late()初始化了slab分配器(内核高速缓存分配器),这意味着bootmem的结束,同时内核的内存管理系统正式开始工作。bootmem的分配必须先于kmem_cache_init_late()。
  • console_init() 初始化控制台,从这个函数之后就可以输出内容到控制台了。在此之前的输出保存在输出缓冲区中,这个函数被调用之后就马上把之前的内容输出出来。
  • lockdep_info() 打印依赖锁的信息,用来调试锁。
  • locking_selftest() 测试锁的API是否使用正常,进行自我检测。
  • page_cgroup_init() 初始化容器组的页面内存分配。mem_cgroup是cgroup体系中提供的用于memory隔离的功能。
  • debug_objects_mem_init()
  • kmemleak_init() 内核内存泄露检测机制初始化。
  • setup_per_cpu_pageset() 创建每个CPU的高速缓存几何数组并初始化。
  • numa_policy_init() 初始化NUMA的内存访问策略。NUMA,NonUniform Memory AccessAchitecture(非一致性内存访问)的缩写,主要用来提高多个CPU的内存访问速度。多个CPU访问同一个节点的内存速度比访问多个节点的内存速度大得多。
  • sched_clock_init() 对每个CPU进行系统进程调度时钟初始化。
  • calibrate_delay() 主要计算CPU需要校准的时间。
  • pidmap_init() 进程位图初始化。
  • anon_vam_init() 初始化EFI的接口,并进入虚拟模式。
  • acpi_early_init()
  • cred_init() 线程信息的缓存初始化。
  • fork_init(totalram_pages) 根据当前物理内存计算出来可以创建进程(线程)的最大数量,并进行进程环境初始化,为task_struct分配空间。
  • proc_caches_init() 进程缓存初始化。
  • buffer_init() 初始化文件系统的缓冲区,并计算最大可以使用的文件缓存。
  • key_init() 初始化内核安全键管理列表和结构,内核秘钥管理系统。
  • security_init() 初始化内核安全管理框架,以便提供文件\登陆等权限。
  • dgb_late_init() 内核调试系统后期初始化。
  • vfs_caches_init(totalram_pages) 虚拟文件系统进行缓存初始化,提高虚拟文件系统的访问速度。
  • signals_init() 初始化信号量队列缓存。
  • page_writeback_init() 页面写机制初始化
  • proc_root_init() 初始化系统进程文件系统,主要提供内核与用户进行交互的平台,方便用户实时查看进程的信息。
  • cgroup_init() 进程控制组正式初始化,主要用来为进程和它的子进程提供性能控制。
  • cpuset_init() 初始化CPUSET。CPUSET主要为控制组提供CPU和内存节点的管理的结构。
  • taskstats_init_early() 任务状态早期初始化,为结构体获取高速缓存,并初始化互斥机制。
  • delayacct_init() 任务延迟机制初始化,初始化每个任务延时计数。当一个任务等待CPU或者IO同步的时候,都需要计算等待时间。
  • check_bugs() 检查CPU配置、FPU等是否非法使用不具备的功能,检查CPU BUG,软件规避BUG。
  • sfi_init_late() SFI初始程序晚期设置函数
  • ftrace_init() 功能跟踪调试机制初始化,初始化内核跟踪模块。ftrace的作用是帮助开发人员了解Linux内核的运行时行为,以便于进行故障调试或者性能分析。

在上面的过程中我们并没有发现有关新进程创建的步骤,那么这个步骤就只能在最后一个函数调用中产生了:

rest_init();

该函数的函数体同样位于main.c中:

static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
kernel_thread(kernel_init, NULL, CLONE_FS);
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();
complete(&kthreadd_done);
init_idle_bootup_task(current);
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);
}

让我们略过RCU(RCU是一种数据同步的方式,详解可参见文章:linux内核 RCU机制详解。)的启动等一系列动作,直奔与pid相关的kernel_thread函数:

pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)

该函数调用了do_fork函数:

long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)

我们需要注意的是这两个函数的执行过程,这可以与我们在上节课中学习到的知识进行印证:

  • kernel_thread函数将它的第一个参数函数指针传递给了do_fork作为第二个参数;
  • do_fork函数依次调用了copy_process函数、wake_up_new_task函数来fork一个新的进程。

当执行完语句:pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);之后,就可以看到新进程的pid的值了:
MOOC课程《Linux内核分析》——start_kernel();Idle进程与Init进程

然而rest_init函数在此之后又调用了下面的代码:

/*
1. The boot idle thread must execute schedule()
2. 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);

代码注释已经说得很明白,最后的cpu_startup_entry函数是idel进程相关的。这个函数调用了另外两个函数:

  • arch_cpu_idle_prepare(void);
  • cpu_idle_loop();

由于parch_cpu_idle_prepare是一个空函数,故而重点在于cpu_idle_loop函数。
从源代码中我们可以看到这个函数就是一个while(1)的循环,但是这与0号进程有什么关系呢?
让我们回到函数init_idle_bootup_task(current):
MOOC课程《Linux内核分析》——start_kernel();Idle进程与Init进程
准备给idle进程的task_struct正是0号进程的task_struct!

至此,我们可以看到,系统中常驻的Idle进程实际上就是0号进程,它是在内核初始化过程中生成的最为原始的进程,并且很显然是一个纯正的内核进程。它在内核初始化结束的最后阶段使用do_fork来生成出1号进程,并作为Idle进程常驻内存。

参考:Start_kernel函数分析