Linux 启动过程

2014-02-28 kernel

现在的 Linux 启动过程一般分成了两步,也就是首先调用 GRUB 作为通用的启动服务,然后可以选择 Windows 或者 Linux 加载。

接下来,看看 Linux 的加载过程。

启动加载

Bootloader 执行完之后,将内核加载到物理地址 0x100000(1MB) 处,并将执行权限交给了内核。然后执行的是 setup.bin 的入口函数 _start() ( arch/x86/boot/header.S,链接文件为 setup.lds ) 。

第一条是一个调转指令,调转到 start_of_setup (start_of_setup-1f是setup头部的长度),在这之间的代码是一个庞大的数据结构,与 bootparam.h 中的 struct setup_header 一一对应。这个数据结构定义了启动时所需的默认参数,其中一些参数可以通过命令选项 overwrite。很多值是在形成 bzImage 时,由 arch/x86/boot/tools/build 程序赋值。

接着实际是设置 C 语言环境,主要是设置堆栈,然后调转到 main()@arch/x86/boot/main.c,现在仍然运行在实模式。setup.bin 主要作用就是完成一些系统检测和环境准备的工作,其函数调用顺序为:

_start()@arch/x86/boot/header.S
  |-start_of_setup()@arch/x86/boot/header.S
    |-main()@arch/x86/boot/main.c
      |-copy_boot_params()                        // 将进行参数复制
      |-detect_memory()                           // 探测物理内存,第一次与内存相关
      |-go_to_protect_mode
        |-realmode_switch_hook()                  // 如果boot_params.hdr.realmode_swtch有非空的hook函数地址则执行之
        |-enable_a20()
        |-reset_coprecessor()                     // 重启协处理器
        |-protected_mode_jump()@arch/x86/boot/pmjump.S   // 进入 32-bit 模式

main.c 主要功能为检测系统参数如: Detect memory layoutset video mode 等,后面会详细分析,最后调用 goto_protect_mode,设置 32-bit 或 64-bit 保护段式寻址模式,注意: main.o 编译为16位实模式程序。

内核的日志信息可以通过 printk() 打印,不过只有信息等级小于 console loglevel 的值时,这些信息才会被打印到 console 上,可以通过如下方法修改日志等级。

  1. 修改内核启动时的参数选项,loglevel=level。
  2. 运行时执行如下命令 dmesg -n level 或者 echo $level > /proc/sys/kernel/printk 。
  3. 写程序使用 syslog 系统调用。

goto_protect_modea() 则会调用 protected_mode_jump(),进入 32-bit 的兼容模式,也就是 IA32-e 模式,在最后会执行 jmpl *%eax 调转到 32-bits 的入口,也就是 0x100000 处。

startup_32()

可以看到 arch/x86/boot/compressed/head_64.S 的入口函数 startup_32,该函数会解压缩 Kernel、设置内存地址转换、进入 64bit 模式。

关于转换的具体步骤,可以参考 《Intel 64 and IA-32 Architectures Software Developer’s Manual》 中的 9.8.5 Initializing IA-32e Mode 部分 。

然后会调转到 arch/x86/kernel/head_64.Sstartup_64

准备环境

这个函数主要是为第一个 Linux 进程 (进程0) 建立执行环境,该函数主要执行以下操作:

bootstrap

最先执行的是 arch/x86/boot/header.S (由最早的bootsect.S和setup.S修改而来),一般是汇编语言。最早的时候 Linux 可以自己启动,因此包含了 Boot Sector 的代码,但是现在如果执行的话只会输出 "bugger_off_msg" ,并重启,该段为 .bstext

现在的 Bootloader 会忽略这段代码。上述无效的 Boot Sector 之后还有 15Bytes 的 Real-Mode Kernel Header ,这两部分总计 512Bytes 。

512 字节之后,也就是在偏移 0x200 处,是 Linux Kernel 实模式的入口,也即 _start。第一条指令是直接用机器码写的跳转指令 (0xeb+[start_of_setup-1]),因此会直接跳转到 start_of_setup 。这个函数主要是设置堆栈、清零 bss 段,然后跳转到 arch/x86/boot/main.c:main()

main() 函数主要完成一些必要的清理工作,例如探测内存的分布、设置 Video Mode 等,最后调用 go_to_protected_ mode()@arch/x86/boot/pm.c

初始化

在进入保护模式之前,还需要一些初始化操作,最主要的两项是 中断 和 内存。

  • 中断 在实模式中,中断向量表保存在地址 0 处,而在保护模式中,中断向量表的地址保存在寄存器 IDTR 中。
  • 内存 实模式和保护模式的从逻辑地址到实地址的转换不同。在保护模式中,需要通过 GDTR 定位全局描述符表(Global Descriptor Table) 。

因此,进入保护模式之前,在 go_to_protected_mode() 中会通过调用 setup_idt()setup_gdt() 创建临时的中断描述符表和全局描述符表。最后通过 protected_mode_jump() (同样为汇编语言) 进入保护模式。

protected_mode_jump() 会设置 CR0 寄存器的 PE 位。此时,不需要分页功能,因此分页功能是关闭的。最主要的是现在不会受 640K 的限制, RAM 的访问空间达到了 4GB 。

然后会调用 32-bit 的内核入口,即 startup_32 。该函数会初始化寄存器,并通过 decompress_kernel() 解压内核。

decompress_kernel() 输出 "Decompressing Linux..." ,然后 in-place 解压,解压后的内核镜像会覆盖上图中的压缩镜像。因此,解压后的内容也是从 1MB 开始。解压完成后将会输出 "done.",然后输出 "Booting the kernel."

此时将会跳转到保护模式下的入口处 (0x100000),该入口同样为 startup_32 ,但与上面的在不同的目录下。

第二个 startup_32 同样为汇编语言,包含了 32-bit 的初始化函数。包括清零保护模式下的 Kernel bss 段、建立全局描述符、建立中断描述符表,然后跳转到目标相关的启动入口,start_kernel() ,过程如下所示。

bootstrap protect mode

正式启动

start_kernel()@init/main.c 的大部分代码为 C ,而且与平台相关。该函数会初始化内核的各个子系统和数据结构,包括了调度器(Scheduler)、内存、时钟等。

然后 start_kernel() 会调用 rest_init() ,该函数将会创建一个内核线程,并将 kernel_init() 作为一个入口传入。

dmesg 中的信息是从 start_kernel() 之后记录的。

像 USB、ACPI、PCI 这样的系统,会通过 subsys_initcall() 定义一个入口,当然还有一些其它的类似入口,可以参考 include/linux/init.h ,实际上时在内核镜像中定义了 .initcall.init 字段 (连接脚本见vmlinux.lds)。

这些函数会在 do_initcalls() 中调用。

rest_init() 随后会调用 schedule() 启动任务调度器,并通过 cpu_idle() 进入睡眠,这是 Linux 中的空闲线程。当没有可运行的进程时,该线程会调用,否则运行可运行的线程。

此时,之前启动的线程将会替代进程0(process 0) 即空闲线程。kernel_init() 将会初始化其他的 CPUs ,在此之前,这些 CPUs 并没有运行。负责初始化的 CPU 被称为 boot processor ,此时启动的 CPUs 被称为 application processors ,这些 CPUs 同样从实模式启动,因此也需要类似的初始化。

最后 kernel_init() 调用 init_post() ,该函数将会尝试执行一个用户进程,执行顺序如下 /sbin/init/etc/init/bin/init/bin/sh 。如果所有的都失败了,那么内核将会停止。

此时执行的进程 PID 为 1 ,它将会检查配置文件,然后运行相应的进程,如 X11 Windows、登录程序、网络服务等。

至此,全部启动过程完成。

bootstrap all

详细流程

从代码角度,介绍启动时的调用流程。

start_kernel()
 |-smp_setup_processor_id()                   返回启动时的CPU号
 |-local_irq_disable()                        关闭当前CPU的中断
 |-setup_arch()                               完成与体系结构相关的初始化工作
 | |-setup_memory_map()                       建立内存图
 | |-e820_end_of_ram_pfn()                    找出最大的可用页帧号
 | |-init_mem_mapping()                       初始化内存映射机制
 | |-initmem_init()                           初始化内存分配器
 | |-x86_init.paging.pagetable_init()         建立完整的页表
 |-parse_early_param()
 | |-parse_args()                             调用两次parse_args()处理bootloader传递的参数
 |-parse_args()
 |-init_IRQ()                                 硬件中断初始化
 |-softirq_init()                             软中断初始化
 |
 |-vfs_caches_init_early()
 |-vfs_caches_init()                          根据参数计算可以作为缓存的页面数,并建立一个存放文件名称的slab缓存
 | |-kmem_cache_create()                      创建slab缓存
 | |-dcache_init()                            建立dentry和dentry_hashtable的缓存
 | |-inode_init()                             建立inode和inode_hashtable的缓存
 | |-files_init()                             建立filp的slab缓存,设置内核可打开的最大文件数
 | |-mnt_init()                               完成sysfs和rootfs的注册和挂载
 |   |-kernfs_init()
 |   |-sysfs_init()                           注册挂载sysfs
 |   | |-kmem_cache_create()                  创建缓存
 |   | |-register_filesystem()
 |   |-kobject_create_and_add()               创建fs目录
 |   |-init_rootfs()                          注册rootfs文件系统
 |   |-init_mount_tree()                      建立目录树,将init_task的命名空间与之联系起来
 |     |-vfs_kern_mount()                     挂载已经注册的rootfs文件系统
 |     | |-alloc_vfsmnt()
 |     |-create_mnt_ns()                      创建命名空间
 |     |-set_fs_pwd()                         设置init的当前目录
 |     |-set_fs_root()                        以及根目录
 |
 |-rest_init()
   |-kernel_init()                            通过kernel_thread()创建独立内核线程
   | |-kernel_init_freeable()
   | | |-do_basic_setup()
   | | | |-do_initcalls()                     调用子模块的初始化
   | | |   |-do_initcall_level()
   | | |     |-do_one_initcall()              调用一系列初始化函数
   | | |       |-populate_rootfs()
   | | |         |-unpack_to_rootfs()
   | | |
   | | |-prepare_namespace()
   | |   |-wait_for_device_probe()
   | |   |-md_run_setup()
   | |   |-initrd_load()                      加载initrd
   | |   | |-create_dev()
   | |   | |-rd_load_image()
   | |   |   |-identify_ramdisk_image()       检查映像文件的magic确定格式minuxext2等;并返回解压方法
   | |   |   | |-decompress_method()
   | |   |   |-crd_load()                     解压
   | |   |     |-deco()
   | |   |
   | |   |-mount_root()
   | |
   | |-run_init_process()                     执行init,会依次查看ramdisk、命令行指定、/sbin/init等
   |
   |-kthreadd()                               同样通过kernel_thread()创建独立内核线程

初始化

在初始化时通常可以分为两种:A) 一种是关键而其必须按照特定顺序来完成,通常在 start_kernel() 中直接调用;B) 以子系统、模块实现,通过 do_initcalls() 完成。

do_initcalls() 中调用时,会按照等级,从 level0 ~ level7 来初始化,其宏定义在 include/linux/init.h 中实现,简单分为了两类,内核以及模块的实现。

下面以 inet_init 的初始化为例,末行为最后的展开格式。

fs_initcall(inet_init);                                           // net/ipv4/af_inet.c
#define fs_initcall(fn) __define_initcall(fn, 5)                  // include/linux/init.h
#define __define_initcall(fn, id) \                               // 同上
      static initcall_t __initcall_##fn##id __used \
      __attribute__((__section__(".initcall" #id ".init"))) = fn

static initcall_t __initcall_inet_init5 __used __attribute__((__section__(".initcall5.init"))) = inet_init;

do_initcalls() 是从特定的内存区域取出初始化函数的指针,然后调用该函数,通过 "vmlinux.lds.h" 定义的宏。

参考