0%

Linux内核启动分析

Linux内核启动分析

start_kernel函数

  • start_kernel 通过调用众多的子函数来完成 Linux 启动之前的一些初始化工作,由于 start_kernel 函数里面调用的子函数太多,而这些子函数又很复杂,因此我们简单的来看一下一 些重要的子函数。

  • 精简后的内容大致如下

    • image-20220227110529062
    • image-20220227110555519
    • image-20220227110617809
    • image-20220227110631266
    • image-20220227110653054

启动过程中调用的函数分析

  • lockdep_init();,死锁检测,初始化两个hash表,尽可能早的执行

  • set_task_stack_end_magic(&init_task);,设置任务栈结束魔术数,用于栈溢出检测

  • smp_setup_processor_id();跟 SMP 有关(多核处理器),设置处理器 ID。

  • debug_objects_early_init();,做一些和 debug 有关的初始化

  • boot_init_stack_canary();栈溢出检测初始化

  • cgroup_init_early(), cgroup 初始化,cgroup 用于控制 Linux 系统资源

  • local_irq_disable()关闭当前 CPU 中断

  • boot_cpu_init();跟 CPU 有关的初始化

  • page_address_init(); 页地址相关的初始化

  • pr_notice("%s", linux_banner);打印 Linux 版本号、编译时间等信息

  • setup_arch(&command_line)架构相关的初始化,此函数会解析传递进来的ATAGS 或者设备树(DTB)文件。会根据设备树里面的 model 和 compatible 这两个属性值来查找Linux 是否支持这个单板。此函数也会获取设备树 中 chosen 节点下的 bootargs 属性值来得到命令 行参数,也就是 uboot 中的 bootargs 环境变量的值,获取到的命令行参数会保存到command_line 中。

  • mm_init_cpumask(&init_mm);看名字,应该是和内存有关的初始化

  • setup_command_line(command_line); 好像是存储命令行参数

  • setup_nr_cpu_ids();如果只是 SMP(多核 CPU)的话,此函数用于获取CPU 核心数量,CPU 数量保存在变量 nr_cpu_ids 中。

  • setup_per_cpu_areas(); 在 SMP 系统中有用,设置每个 CPU 的 per-cpu 数据

  • build_all_zonelists(NULL, NULL); 建立系统内存页区(zone)链表

  • page_alloc_init(); 处理用于热插拔 CPU 的页

  • pr_notice("Kernel command line: %s\n", boot_command_line); 打印命令行信息

  • parse_early_param(); 解析命令行中的 console 参数

  • setup_log_buf(0);设置 log 使用的缓冲区

  • pidhash_init(); 构建 PID 哈希表,Linux 中每个进程都有一个 ID,这个 ID 叫做 PID。通过构建哈希表可以快速搜索进程信息结构体。

  • vfs_caches_init_early();预先初始化 vfs(虚拟文件系统)的目录项和索引节点缓存

  • sort_main_extable();定义内核异常列表

  • trap_init(); 完成对系统保留中断向量的初始化

  • mm_init();内存管理初始化

  • sched_init();初始化调度器,主要是初始化一些结构体

  • preempt_disable();关闭优先级抢占

if (WARN(!irqs_disabled(), /* 检查中断是否关闭,如果没有的话就关闭中断 */
"Interrupts were enabled *very* early, fixing it\n"))
local_irq_disable();
  • idr_init_cache(); IDR 初始化,IDR 是 Linux 内核的整数管理机 制,也就是将一个整数 ID 与一个指针关联起来。
  • rcu_init();初始化 RCU,RCU 全称为 Read Copy Update(读-拷贝修改)
  • trace_init();跟踪调试相关初始化
  • radix_tree_init();基数树相关数据结构初始化
  • early_irq_init(); 初始中断相关初始化,主要是注册 irq_desc 结构体变量,因为 Linux 内核使用 irq_desc 来描述一个中断。
  • init_IRQ();中断初始化
  • tick_init(); tick 初始化
  • init_timers();初始化定时器
  • hrtimers_init();初始化高精度定时器
  • softirq_init(); 软中断初始化
  • time_init();初始化系统时间
  • local_irq_enable(); 使能中断
  • kmem_cache_init_late(); slab 初始化,slab 是 Linux 内存分配器
  • console_init(); 初始化控制台,之前 print 打印的信息都存放 缓冲区中,并没有打印出来。只有调用此函数初始化控制台以后才能在控制台上打印信息
  • lockdep_info();如果定义了宏 CONFIG_LOCKDEP,那么此函数打印一些信息
  • locking_selftest()锁自测
  • kmemleak_init();kmemleak 初始化,kmemleak 用于检查内存泄漏
  • calibrate_delay(); 测定 BogoMIPS 值,可以通过 BogoMIPS 来判断 CPU 的性能.BogoMIPS 设置越大,说明 CPU 性能越好。
  • pidmap_init(); PID 位图初始化
  • anon_vma_init(); 生成 anon_vma slab 缓存
  • cred_init();为对象的每个用于赋予资格(凭证)
  • fork_init(); 初始化一些结构体以使用 fork 函数
  • proc_caches_init();给各种资源管理结构分配缓存
  • buffer_init();初始化缓冲缓存
  • key_init();初始化密钥
  • security_init(); 安全相关初始化
  • vfs_caches_init(totalram_pages); 为 VFS 创建缓存
  • signals_init();初始化信号
  • page_writeback_init(); 页回写初始化
  • proc_root_init(); 注册并挂载 proc 文件系统
  • cpuset_init();初始化 cpuset,cpuset 是将 CPU 和内存资源以逻辑性和层次性集成的一种机制,是 cgroup 使用的子系统之一
  • cgroup_init(); 初始化 cgroup
  • taskstats_init_early();进程状态初始化
  • check_bugs();检查写缓冲一致性

rest_init 函数

  • image-20220227114640698

  • image-20220227114923703

  • 执行内容

    • 第 387 行,调用函数 rcu_scheduler_starting,启动 RCU 锁调度器

    • 第 394 行,调用函数 kernel_thread 创建 kernel_init 进程,也就是大名鼎鼎的 init 内核进程init 进程的 PID 为 1。init 进程一开始是内核进程(也就是运行在内核态),后面 init 进程会在根 文件系统中查找名为“init”这个程序,这个“init”程序处于用户态,通过运行这个“init”程 序,init 进程就会实现从内核态到用户态的转变。

    • 第 396 行,调用函数 kernel_thread 创建 kthreadd 内核进程,此内核进程的 PID 为 2kthreadd 进程负责所有内核进程的调度和管理

    • 第 409 行,最后调用函数 cpu_startup_entry 来进入 idle 进程,cpu_startup_entry 会调用 cpu_idle_loop,cpu_idle_loop 是个 while 循环,也就是 idle 进程代码。idle 进程的 PID 为 0,idle 进程叫做空闲进程,如果学过 FreeRTOS 或者 UCOS 的话应该听说过空闲任务。idle 空闲进程 就和空闲任务一样,当 CPU 没有事情做的时候就在 idle 空闲进程里面“瞎逛游”,反正就是给 CPU 找点事做。当其他进程要工作的时候就会抢占 idle 进程,从而夺取 CPU 使用权。其实大 家应该可以看到 idle 进程并没有使用 kernel_thread 或者 fork 函数来创建,因为它是有主进程演 变而来的。 在 Linux 终端中输入“ps -A”就可以打印出当前系统中的所有进程,其中就能看到 init 进 程和 kthreadd 进程,如图

    • image-20220227120610800

    • 没有显示 PID 为 0 的 idle 进程,那是因为 idle 进程是内核进程。

init进程

  • kernel_init 函数就是 init 进程具体做的工作,定义在文件 init/main.c 中,函数内容如下:

  • image-20220227122017680

  • image-20220227122029865

    • 第 932 行,kernel_init_freeable 函数用于完成 init 进程的一些其他初始化工作,稍后再来具 体看一下此函数。
    • 第 940 行,ramdisk_execute_command 是一个全局的 char 指针变量,此变量值为“/init”, 也就是根目录下的 init 程序。ramdisk_execute_command 也可以通过 uboot 传递,在 bootargs 中 使用“rdinit=xxx”即可,xxx 为具体的 init 程序名字。
    • 第 943 行,如果存在“/init”程序的话就通过函数 run_init_process 来运行此程序。
    • 第 956 行,如果 ramdisk_execute_command 为空的话就看 execute_command 是否为空,反 正不管如何一定要在根文件系统中找到一个可运行的 init 程序。execute_command 的值是通过 uboot 传递,在 bootargs 中使用“init=xxxx”就可以了,比如“init=/linuxrc”表示根文件系统中 的 linuxrc 就是要执行的用户空间 init 程序。
    • 第 963~966 行,如果 ramdisk_execute_command 和 execute_command 都为空,那么就依次 查找“/sbin/init”、“/etc/init”、“/bin/init”和“/bin/sh”,这四个相当于备用 init 程序,如果这四 个也不存在,那么 Linux 启动失败!
    • 第 969 行,如果以上步骤都没有找到用户空间的 init 程序,那么就提示错误发生!

kernel_init_freeable 函数

  • kernel_init 会调用此函数来做一些 init 进程初始化工作。kernel_init_freeable 定义在文件 init/main.c 中
  • 缩减后的函数内容如下
  • image-20220227122233775
  • image-20220227122256401
    • 第 1002 行,do_basic_setup 函数用于完成 Linux 下设备驱动初始化工作!非常重要。 do_basic_setup 会调用 driver_init 函数完成 Linux 下驱动模型子系统的初始化。
    • 第 1005 行,打开设备“/dev/console”,在 Linux 中一切皆为文件!因此“/dev/console”也 是一个文件,此文件为控制台设备。每个文件都有一个文件描述符,此处打开的“/dev/console” 文件描述符为 0,作为标准输入(0)
    • 第 1008 和 1009 行,sys_dup 函数将标准输入(0)的文件描述符复制了 2 次,一个作为标准 输出(1),一个作为标准错误(2)。这样标准输入、输出、错误都是/dev/console 了。console 通过 uboot 的 bootargs 环境变量设置,“console=ttymxc0,115200”表示将/dev/ttymxc0 设置为 console, 也就是 I.MX6U 的串口 1。当然,也可以设置其他的设备为 console,比如虚拟控制台 tty1,设 置 tty1 为 console 就可以在 LCD 屏幕上看到系统的提示信息。
    • 第 1020 行,调用函数 prepare_namespace 来挂载根文件系统。根文件系统也是由命令行参 数指定的,就是 uboot 的 bootargs 环境变量。比如“root=/dev/mmcblk1p2 rootwait rw”就表示根 文件系统在/dev/mmcblk1p2 中,也就是 EMMC 的分区 2 中。