Linux内核启动流程(下)

嵌入式技术

1335人已加入

描述

本篇是通用内核启动阶段,一般是C语言实现。

承接上篇,start_kernel函数板级引导阶段进入通用内核启动阶段的第一个函数,从编程语言角度理解,也是汇编进入C语言的第一个函数。

该函数定义在init/main.c文件内,它主要完成Linux启动之前的一些初始化工作,该函数内调用的子函数非常多,大多子函数十分复杂,我们先从整体理解初始化流程,后续结合每个模块再回头理解子函数初始化的意义。

1、start_kernel函数添加注释,根据注释来理解

asmlinkage __visible void __init start_kernel(void)
{
  char *command_line; // 存放BootLoader的传参
  char *after_dashes;


  set_task_stack_end_magic(&init_task); // 设置任务堆栈结束幻数,可以检测堆栈溢出
  smp_setup_processor_id();   // 如果非SMP则为空函数,是SMP则设置处理器ID
  debug_objects_early_init(); // debug提前初始化


  cgroup_init_early(); // control group 提前初始化


  local_irq_disable(); // 关闭当前CPU中断
  early_boot_irqs_disabled = true; // 系统中断关闭标志,当early init完成后,设置为false


  /*
   * Interrupts are still disabled. Do necessary setups, then
   * enable them.
   */
  boot_cpu_init();     // CPU相关初始化,CPU位图管理
  page_address_init(); // 页地址初始化,主要是初始化高端内存映射表
  pr_notice("%s", linux_banner); // 打印Linux版本信息和kernel编译时间等信息
  early_security_init(); // LSM 早期初始化
  setup_arch(&command_line); // 和ARM架构相关,解析ATAGS或设备树,解析的参数放入command_line
  setup_command_line(command_line); // 保存命令行,日后再用
  setup_nr_cpu_ids(); // 获取nr_cpu_ids个数,即CPU核数量
  setup_per_cpu_areas(); // 设置SMP体系每个CPU使用的内存空间,并拷贝初始化段内的数据
  smp_prepare_boot_cpu();  /* arch-specific boot-cpu hooks */
  boot_cpu_hotplug_init(); // 初始化CPU热插拔


  build_all_zonelists(NULL); // 设置内存管理相关的node(节点,每个CPU一个内存节点)和其中的zone(内存域,包含于节点中,如)数据结构,以完成内存管理子系统的初始化,并设置bootmem分配器
  page_alloc_init(); // 设置内存页分配通知器


  pr_notice("Kernel command line: %s\\n", boot_command_line);
  /* parameters may set static keys */
  jump_label_init();
  parse_early_param(); // 解析boot_command_line的参数
  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);


  /*
   * These use large bootmem allocations and must precede
   * kmem_cache_init()
   */
  setup_log_buf(0); // 使用boot mem分配一个记录启动信息的缓冲区
  vfs_caches_init_early(); // 前期虚拟文件系统(vfs)的缓存初始化
  sort_main_extable(); // 对内核异常表(exception_table)按照异常向量号大小进行排序,以便加速访问
  trap_init(); // 对内核陷阱异常进行初始化,在ARM系统里是空函数,没有任何的初始化
  mm_init(); // 内存初始化,标记可使用内存,告知系统还剩多少内存可使用


  ftrace_init(); // ftrace用于内核函数的trace功能


  /* trace_printk can be enabled here */
  early_trace_init(); // 为trace_printk等分配buffer


  /*
   * 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(); // 对进程调度器的数据结构进行初始化,创建运行队列,设置当前任务的空线程,当前任务的调度策略为CFS调度器
  /*
   * 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(); // 内核radis 树算法初始化


  /*
   * 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(); // Read Copy Update 初始化


  /* Trace events are available after this */
  trace_init(); // trace event的初始化


  if (initcall_debug)
    initcall_debug_enable();


  context_tracking_init();
  /* init some links before init_ISA_irqs() */
  early_irq_init(); // 前期外部中断描述符初始化,主要初始化数据结构
  init_IRQ(); // 调用machine_desc- >init_irq()对中断初始化
  tick_init(); // 初始化内核时钟系统
  rcu_init_nohz();
  init_timers(); // 初始化引导CPU的时钟相关的数据结构体,和初始化时钟软中断
  hrtimers_init(); // 初始化高精度的定时器
  softirq_init(); // 初始化软中断(tasklet和hi)
  timekeeping_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(); // system timer或arch timer初始化
  printk_safe_init();
  perf_event_init(); // CPU性能监视机制初始化
  profile_init(); // 分配内核性能统计保存的内存
  call_function_init(); // 初始化PerCPU的call_single_queue,注册CPU热插拔通知函数到CPU通知链
  WARN(!irqs_disabled(), "Interrupts were enabled early\\n");


  early_boot_irqs_disabled = false;
  local_irq_enable(); // 打开本地CPU的中断


  kmem_cache_init_late(); // 内核内存缓存(slab分配器)的后期初始化,之后可使用通用内存缓存


  /*
   * 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(); // 自测锁的API是否使用正常


  /*
   * 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 // 检查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(); // 预先创建每个CPU的高速缓存集合数组并初始化
  numa_policy_init(); // 初始化NUMA的内存访问策略
  acpi_early_init(); // 初始化ACPI电源管理(高级配置及电源接口)
  if (late_time_init)
    late_time_init(); // 运行时钟后期初始化
  sched_clock_init(); // 每个CPU进行系统进程调度时钟初始化
  calibrate_delay(); // 计算CPU需要校准的时间
  pid_idr_init(); // 初始化 idr 
  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(); // 根据当前物理内存计算出来可以创建进程(线程)的最大数量,并进行进程环境初始化,为task_struct分配空间
  proc_caches_init(); // 进程缓存初始化,为进程初始化创建机制所需的其他数据结构申请空间
  uts_ns_init(); 
  buffer_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(); // 初始化CPUSET,CPUSET主要为控制组提供CPU和内存节点的管理的结构
  cgroup_init(); // 进程控制组正式初始化,主要用来为进程和其子程提供性能控制
  taskstats_init_early(); // 任务状态早期初始化,为结构体获取高速缓存,并初始化互斥机制。任务状态主要向用户提供任务的状态信息
  delayacct_init(); // 任务延迟机制初始化,初始化每个任务延时计数


  poking_init();
  check_bugs(); // 检查CPU配置、FPU等是否非法使用不具备的功能,检查CPU BUG,软件规避BUG


  acpi_subsystem_init();
  arch_post_acpi_subsys_init();
  sfi_init_late(); // SFI 初始程序晚期设置函数


  /* Do the rest non-__init'ed, we're now alive */
  arch_call_rest_init(); // 剩余的初始化,接下来内核开始工作了
}

2、rest_init剩余初始化

arch_call_rest_init函数内调用rest_init完成剩余初始化,并让CPU进入idle。

rest_init函数比较简单,分为以下几个内容:

  • 开启RCU
  • 调用kernel_thread创建kernel_init和kthreadd内核线程
  • 调用schedule_preempt_disabled->schedule()开启内核调度
  • 调用cpu_startup_entry->do_idle()结束内核启动。
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);
}

3、总体流程图如下,下一篇阅读1号进程和2号进程

C语言

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分