导航

Linux源代码阅读——内核引导

目录

  1. Linux 引导过程综述
  2. BIOS
  3. Boot loader
  4. 内核初始化:体系结构相关部分
  5. 内核初始化:体系结构无关部分

1 Linux引导过程综述

  1. BIOS
    在 i386 平台中,由 BIOS 作最初的引导工作,执行加电自检、初始化,读取引导设备的主引导扇区并执行。
  2. Boot loader(以 GRUB 为例)
    MBR 中的、紧随 MBR 后的 phase 1/1.5 boot loader 载入文件系统中的 phase 2 及其配置,显示操作系统选择菜单,执行用户命令,载入选定的操作系统内核与 initrd。
  3. 内核初始化:体系结构相关部分
    从 header.S 开始,到 main.c 初始化参数,再到 pm.c 进入保护模式,然后载入 vmlinuz 并自解压,在 startup_32.S 中开启分页机制、初始化中断向量表、检测 CPU 类型等,完成 x86 体系结构的保护模式初始化。这是本文重点。
  4. 内核初始化:体系结构无关部分
    分为核心数据结构初始化(start_kernel)和设备初始化两个阶段。
  5. 用户态初始化
    以下内容超出了本文范围。用户态的 init 程序:

2 BIOS

BIOS的主要功能概括来说包括如下几部分:

BIOS 的启动主要由 POST 过程与自举过程构成。

2.1 POST

当 PC 加电后,CPU 的寄存器被设为某些特定值。其中,指令指针寄存器(program counter)被设为 0xfffffff0。

CR1,一个32位控制寄存器,在刚启动时值被设为0。CR1 的 PE (Protected Enabled,保护模式使能) 位指示处理器是处于保护模式还是实模式。由于启动时该位为0,处理器在实模式中引导。在实模式中,线性地址与物理地址是等同的。

在实模式下,0xfffffff0 不是一个有效的内存地址,计算机硬件将这个地址指向 BIOS 存储块。这个位置包含一条跳转指令,指向 BIOS 的 POST 例程。

POST(Power On Self Test,加电自检)过程包括内存检查、系统总线检查等。如果发现问题,主板会蜂鸣报警。在 POST 过程中,允许用户选择引导设备。

POST 的最后一步是执行 INT 0x19 指令,开始自举过程。

POST 过程在 AWARD BIOS 的源码中在 BOOTROM.ASM 文件中 BootBlock_POST 函数过程中实现,主要步骤如下:

  1. 初始化各种主板芯片组
  2. 初始化键盘控制器
  3. 初始化中断向量、中断服务例程
  4. 初始化 VGA BIOS 控制器
  5. 显示 BIOS 的版本和公司名称
  6. 扫描各种介质容量并显示
  7. 读取 CMOS 的启动顺序配置
  8. 调用 INT 0x19 启动自举程序

2.2 自举过程

自举过程即为执行中断 INT 0x19 的中断服务例程 INT19_VECT 的过程 (Bootrom.asm)

主要功能为读取引导设备第一个扇区的前 512 字节(MBR),将其读入到内存 0x0000:7C00,并跳转至此处执行。

3 Boot loader

3.1 主引导扇区结构

硬盘第一个扇区的前 512 个字节是主引导扇区,由 446 字节的 MBR、64 字节的分区表和 2 字节的结束标志组成。

3.2 GRUB stage1

Linux 的启动方式包括 LILO、GRUB 等。这里结合 GRUB 源代码分析其引导过程。

GRUB 的引导过程分为 stage1、stage 1.5 和 stage 2。其中 stage1 和可能存在的 stage1.5 是为 stage2 做准备,stage2 像一个微型操作系统。

  1. BIOS 加载 GRUB stage1(如果安装到 MBR)到 0x00007C00.

  2. stage1 位于 stage1/stage1.S,汇编后形成 512 字节的二进制文件,写入硬盘的0面0道第1扇区。

    stage1 将0面0道第2扇区上的 512 字节读到内存中的0x00007000处,然后调用 COPY_BUFFER 将其拷贝到 0x00008000 的位置上,然后跳至 0x00008000 执行。这 512 字节代码来自 stage2/start.S,作用是 stage1_5 或者 stage2(编译时决定加载哪个)的加载器。

    /* start.S */
    blocklist_default_start:
    .long 2	 /* 从第3扇区开始*/
    blocklist_default_len:
    /* 需要读取多少个扇区 */
    #ifdef STAGE1_5
    .word 0	 /* 如果是 STAGE1_5,则不读入 */
    #else
    .word (STAGE2_SIZE + 511) >> 9 /* 读入 Stage2 所占的所有扇区 */
    #endif
    blocklist_default_seg:
    #ifdef STAGE1_5
    .word 0x220 /* 将 stage1.5 加载到 0x2200 */
    #else
    .word 0x820	/* 将 stage2 加载到 0x8200 */
    #endif
    
  3. 由于 stage1 和 start 不具备文件系统识别功能,stage 1.5 只能被存放在固定的扇区中。例如 e2fs_stage1_5 就被存放在0面0道第3扇区开始的一段连续空间里。(第一个主分区是从1面0道第1扇区开始的,stage 1.5 不会覆盖主分区内容)

    stage 1.5 能够读取文件系统,负责从文件系统中载入并执行 stage 2,即 GRUB 的核心映像。由于系统引导过程中不需要修改文件系统,因此只实现了文件系统的读取。

    可以说,stage 1.5 是 stage 1 与 stage 2 之间的桥梁,解决了文件系统这个“先有鸡还是先有蛋”的问题。

3.3 GRUB stage2

stage2 将系统切换到保护模式,设置 C 运行环境,寻找 config 文件,执行 shell 接受用户命令,载入选定的操作系统内核。

  1. stage2 的入口点是 asm.s
    #ifdef STAGE1_5
    # define	ABS(x)	((x) - EXT_C(main) + 0x2200)
    #else
    # define	ABS(x)	((x) - EXT_C(main) + 0x8200)
    #endif
    
    1. 初始化一些变量
    2. 跳转到 code_start
    3. 关中断,设置段寄存器和堆栈起始地址
    4. 从实模式切换到保护模式
    5. 清空 bss 段
    6. init_bios_info()
  2. 随后进入 stage2.c,执行 GRUB 的主要功能。

  3. 每个 GRUB 命令都要在 stage2/builtin.c 的 builtin_table 数组中登记:
    struct builtin
    {
        char *name;			/* 命令名称 */
        int (*func) (char *, int);	/* 命令执行时调用的函数指针 */
        int flags;			/* 标志,似乎未用到 */
        char *short_doc;		/* 短帮助 */
        char *long_doc;		/* 详细帮助 */
    };
    struct builtin *builtin_table[];
    
  4. 常用 GRUB 命令:
  5. stage2 中的文件系统驱动:

    每种文件系统都要按照 stage2/filesys.h 的定义在 stage2/disk_io.c 的 fsys_table 数组中登记:

    /* stage2/filesys.h */
    struct fsys_entry
    {
        char *name;                                         //文件系统名称
        int (*mount_func) (void);                           //挂载
        int (*read_func) (char *buf, int len);              //读文件
        int (*dir_func) (char *dirname);                    //打开文件
        void (*close_func) (void);                          //关闭文件
        int (*embed_func) (int *start_sector, int needed_sectors);  //不清楚
    };
    

    GRUB 调用 grub_open() 打开文件。grub_open 在 fsys_table 数组中逐个调用 fsys_entry::mount_func(),找到当前已挂载的文件系统,再用 fsys_entry::dir_func() 方法打开文件。

4 内核初始化:体系结构相关部分

4.1 内核映像结构

根据 Linux/I386 启动协议(Documentation/i386/boot.txt),x86 体系结构大内核内存使用如下:

For a modern bzImage kernel with boot protocol version >= 2.02, a
memory layout like the following is suggested:

        ~                        ~   
        |  Protected-mode kernel |
100000  +------------------------+
        |  I/O memory hole       |   
0A0000  +------------------------+
        |  Reserved for BIOS     |      Leave as much as possible unused
        ~                        ~   
        |  Command line          |      (Can also be below the X+10000 mark)
X+10000 +------------------------+
        |  Stack/heap            |      For use by the kernel real-mode code.
X+08000 +------------------------+    
        |  Kernel setup          |      The kernel real-mode code.
        |  Kernel boot sector    |      The kernel legacy boot sector.
X       +------------------------+
        |  Boot loader           |      <- Boot sector entry point 0000:7C00
001000  +------------------------+
        |  Reserved for MBR/BIOS |
000800  +------------------------+
        |  Typically used by MBR |
000600  +------------------------+ 
        |  BIOS use only         |   
000000  +------------------------+

根据 arch/x86/boot/Makefile,bzImage 大内核映像由 setup.elf 和 vmlinux 组成,而 vmlinux 又由 setup.bin 和 vmlinux.bin 组成。vmlinux.bin 会进行压缩存储,变成 vmlinux.bin.gz。因此 bzImage 由 setup.elf、setup.bin、vmlinux.bin.gz 三部分组成。

Line 28: targets         := vmlinux.bin setup.bin setup.elf zImage bzImage
Line 29: subdir-         := compressed
Line 30: 
Line 31: setup-y         += a20.o cmdline.o copy.o cpu.o cpucheck.o edd.o
Line 32: setup-y         += header.o main.o mca.o memory.o pm.o pmjump.o
Line 33: setup-y         += printf.o string.o tty.o video.o video-mode.o version.o

其中 setup-y 就是 setup.elf,其中引用的 header.o 是从 header.S 汇编而来的。

Line 77: $(obj)/bzImage: IMAGE_OFFSET := 0x100000
Line 86: $(obj)/zImage $(obj)/bzImage: $(obj)/setup.bin \
Line 87:                               $(obj)/vmlinux.bin $(obj)/tools/build FORCE
Line 88:         $(call if_changed,image)
Line 89:         @echo 'Kernel: $@ is ready' ' (#'`cat .version`')'
Line 90:
Line 91: OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S

大内核情况下的内存分布图:

        |  vmlinux               |   
100000  +------------------------+
        |  setup.elf的setup部分   |
090200  +------------------------+
        |  setup.elf的启动扇区     |
090000  +------------------------+
        |  BootLoader            |
007c00  +------------------------+
        |                        |
000000  +------------------------+

在进入源代码的世界之前,我们先看看用于控制 arch/x86/boot 下代码进行链接的 setup.ld。

ld 文件用于控制 ld 的链接过程:

每个对象文件有一个节(section)列表、一个符号列表,一个符号可以是已定义或未定义的。每个已定义的符号有地址。未定义的符号则要在链接时从其他文件中寻找其定义。

  1. 指定输出文件格式
    OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
  2. 指定目标体系结构
    OUTPUT_ARCH(i386)
  3. 设置入口点
    ENTRY(_start)
  4. 输入文件各节到输出文件的映射
    SECTIONS
    {
    . = 0				// 从 0 开始
    .bstext : { *(.bstext) }	// 所有输入文件的 .bstext 节组合成输出文件的 .bstext 节
    .bsdata : { *(.badata) }	// 所有输入文件的 .bsdata 节...
    . = 497				// 填充 512 字节的 bootloader(见4.2节 header.S)
    .header : { *(.header) }
    

    在每一部分(header、rodata、data、bss、end)之间,对齐 16 字节内存边界:

    . = ALIGN(16);

    最后用断言保证链接后的目标文件不太大,且偏移量正确。

4.2 header.S

start2:
	movw	%cs, %ax        # CS = 0x7c00
	movw	%ax, %ds	# 初始化段寄存器
	movw	%ax, %es
	movw	%ax, %ss
	xorw	%sp, %sp
	sti			# 开中断
	cld			# di++, si++
................................
msg_loop:			# 打印字符例程
................................
bs_die:				# 错误处理例程
        .ascii  "Direct booting from floppy is no longer supported.\r\n"
        .ascii  "Please use a boot loader program instead.\r\n"
        .ascii  "\n"
        .ascii  "Remove disk and press any key to reboot . . .\r\n"
        .byte   0

这段代码编译链接后,会生成 512 字节的 bootsector,其中 .section ".header", "a" 中的变量共 15 字节。注意到 setup.ld (Linker script for the i386 setup code) 中加入了 497 字节的空白,事实上恰好凑够 512 字节。

事实上,上一节我们提到,MBR 是由 GRUB 写入的,因此这里的 bootsector 对于硬盘启动是用不到的。GRUB 等 boot loader 将 setup.elf 读到 0x90000 处,将 vmlinux 读到 0x100000 处,然后跳转到 0x90200 开始执行,恰好跳过了 512 字节的 bootsector。

有意思的是,从软盘启动时,header.S 生成的 bootsector 做的惟一一件事就是打印错误信息(bs_die),不支持从软盘启动。

下面就是 0x90200(_start)了,目的就是跳到 start_of_setup。

         # Part 2 of the header, from the old setup.S
................................
# End of setup header #####################################################

上面这两行之间的代码是一个庞大的数据结构,与 include/asm/bootparam.h 中的 struct setup_header 一一对应。这个数据结构定义了启动时所需的默认参数,其中一些参数可以通过命令选项 overwrite。下表列出了一些参数的意义。

名称偏移大小(字节)意义
root_flags0x1f22根目录是否只读,可用 ro 或 rw 选项指定
root_dev0x1fc2默认的 root 设备,即 /boot 所在目录,可用 root= 选项指定
boot_flag0x1fe20xAA55,即主引导扇区结束标志
header0x2024HdrS (0x53726448),内核标志
version0x2062启动协议版本号: major * 64 + minor
kernel_version0x20e2内核版本号
type_of_loader0x2101Boot loader ID: Boot loader ID * 64 + Version No.
Boot loader IDs:
0 LILO
1 Loadlin
2 bootsect-loader
3 SYSLINUX
4 EtherBoot
5 ELILO
7 GRuB
8 U-BOOT
9 Xen
A Gujin
B Qemu
loadflags0x2111启动选项的掩码。
  • Bit 0: LOADED_HIGH (1表示保护模式代码加载到 0x100000)
  • Bit 7: CAN_USE_HEAP (为1表示 heap_end_ptr 有效)
code32_start0x2144内核解压缩前立即跳转到的 32 位 flat-mode 入口
ramdisk_image0x2184initramfs 的 32 位线性地址
cmd_line_ptr0x2284内核命令行的 32 位线性地址

下面我们迎来了真正的起点(start_of_setup),主要流程为:

  1. 复位硬盘控制器
  2. 如果 %ss 无效,重新计算栈指针
  3. 初始化栈,开中断
  4. 将 cs 设置为 ds,与 setup.elf 的入口地址一致
  5. 检查主引导扇区末尾标志,如果不正确则跳到 setup_bad
  6. 清空 bss 段
  7. 跳到 main(定义在 boot/main.c)

4.3 初始化与保护模式

我们终于暂时离开了汇编代码,走进 “主要” 的启动部分。这一部分在 arch/x86/boot/main.c 中。

main() 中的几个函数调用都有比较详细的注释,主要作用是初始化 boot_params,将来会经常被用到。

include/asm/bootparam.h 中定义的 boot_params 结构体 (即 zeropage) 在此完成初始化:

go_to_protected_mode() 进入保护模式,代码在 boot/pm.c。

  1. realmode_switch_hook():boot_params.hdr 中有 realmode_swtch,记录了 hook 函数地址,如果有的话就执行之
  2. reset_coprecessor(): 重启协处理器
  3. make_all_interrupts(): 关闭所有旧 PIC 上的中断。其中的 io_delay 等待 I/O 操作完成。
  4. setup_idt(): 初始化中断描述符表 (空的)
  5. setup_gdt(): 初始化 GDT:

    其中 GDT_ENTRY_BOOT_CS 和 GDT_ENTRY_BOOT_DS 基地址都为零,段限长都是 4G。

    下面是 GDT 数据结构示意:

  6. protected_mode_jump(): 汇编代码,下面分析。传参说明:进入保护模式后将采用段访问内存地址,因此要将传入的参数转换为线性地址。

下面进入 boot/pmjump.S 中的 protected_mode_jump。

 29 protected_mode_jump:
 30         movl    %edx, %esi              # Pointer to boot_params table
 31 
 32         xorl    %ebx, %ebx
 33         movw    %cs, %bx                # 将实模式的代码段放入 bx
 34         shll    $4, %ebx                # 转换为线性地址
 35         addl    %ebx, 2f                # 将 in_pm32 的实模式地址转换为线性地址
 36 
 37         movw    $__BOOT_DS, %cx         # ds 段选择子
 38         movw    $__BOOT_TSS, %di        # tss 段选择子
 39 
 40         movl    %cr0, %edx
 41         orb     $X86_CR0_PE, %dl        # Protected mode
 42         movl    %edx, %cr0              # 将 cr0 的0位置0是进入保护模式的标志
 43         jmp     1f                      # Short jump to serialize on 386/486
 44 1:
 45         # 下面这段作用是跳转到 in_pm32,由于已经在保护模式,所以需要考虑段的问题
 46         # Transition to 32-bit mode
 47         .byte   0x66, 0xea              # ljmpl opcode
 48 2:      .long   in_pm32                 # offset
 49         .word   __BOOT_CS               # segment
 50 
 51         .size   protected_mode_jump, .-protected_mode_jump
 52 
 53         .code32
 54         .type   in_pm32, @function
 55 in_pm32:        # 下面的注释挺清楚,就不翻译了
 56         # Set up data segments for flat 32-bit mode
 57         movl    %ecx, %ds
 58         movl    %ecx, %es
 59         movl    %ecx, %fs
 60         movl    %ecx, %gs
 61         movl    %ecx, %ss
 62         # The 32-bit code sets up its own stack, but this way we do have
 63         # a valid stack if some debugging hack wants to use it.
 64         addl    %ebx, %esp
 65 
 66         # Set up TR to make Intel VT happy
 67         ltr     %di                     # 这个比较有意思
 68 
 69         # Clear registers to allow for future extensions to the
 70         # 32-bit boot protocol
 71         xorl    %ecx, %ecx
 72         xorl    %edx, %edx
 73         xorl    %ebx, %ebx
 74         xorl    %ebp, %ebp
 75         xorl    %edi, %edi
 76 
 77         # Set up LDTR to make Intel VT happy
 78         lldt    %cx                     # 又是一个骗 CPU 的东西

 79         # eax 是 protected_mode_jump 的第一个参数,即 header.S 中定义的 boot_params.hdr.code32_start,即 vmlinux 的入口地址
 80         jmpl    *%eax                   # Jump to the 32-bit entrypoint
 81 
 82         .size   in_pm32, .-in_pm32

4.4 自解压内核

上节末尾的 jmpl 指令把我们带入了 vmlinux 的世界。注意到,vmlinux 是压缩存储的,因此内核首先的工作就是把真正的内核解压出来。

根据 Makefile,linux 内核文件有以下几种:

循着 Makefile 的踪迹,我们找到了 arch/x86/boot/compressed/head_32.S,这就是大内核模式下 0x100000 开始的内存内容。

  1. 找到 vmlinux 的入口地址,并将其存入 ebp。
  2. 如果设置了可重入内核,就将 ebp 按照 kernel_alignment 对齐,放入 ebx。
  3. 确定解压内核的内存地址
  4. 设置栈
  5. 将 vmlinux 复制到安全地区(ebx 指定的地方):保存 esi 到栈中,首先计算出需要复制的字节数目,然后4个字节为一组地复制过去,再从栈中恢复 esi。
  6. 进入 relocated,清空 BSS,初始化解压函数所用的栈
  7. 将 decompress_kernel 所用的参数入栈:内核加载地址、内核长度、压缩内核安全地址、堆地址、启动参数结构体指针。
  8. 调用 decompress_kernel 解压内核
  9. 如果设置了可重入内核,进行一些 relocate
  10. 跳转到解压后的内核。

至此,arch/x86/boot 下的流程基本分析完毕。

4.5 startup_32

vmlinux 是从哪里来的呢?不知道是否是 Linus 有意为我们增加难度 (其实是我对 make 不熟悉),生成 vmlinux 的命令在源码根目录的隐藏文件 .vmlinux.cmd 中。

md_vmlinux := ld -m elf_i386 --build-id -o vmlinux -T arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_32.o arch/x86/kern
el/head32.o arch/x86/kernel/init_task.o  init/built-in.o --start-group  usr/built-in.o  arch/x86/mach-generic/built-in.o
arch/x86/kernel/built-in.o  arch/x86/mm/built-in.o  arch/x86/mach-default/built-in.o  arch/x86/crypto/built-in.o  arch/x86
/vdso/built-in.o  kernel/built-in.o  mm/built-in.o  fs/built-in.o  ipc/built-in.o  security/built-in.o  crypto/built-in.o 
block/built-in.o  lib/lib.a  arch/x86/lib/lib.a  lib/built-in.o  arch/x86/lib/built-in.o  drivers/built-in.o  sound/built 
-in.o  arch/x86/pci/built-in.o  arch/x86/oprofile/built-in.o  arch/x86/power/built-in.o  net/built-in.o --end-group .tmp_k
allsyms2.o

真正的内核入口是 arch/x86/kernel/head_32.S (为什么也叫 head_32.S?)

汇编函数 startup_32 依次完成以下动作:

  1. 初始化参数

  2. 开启分页机制

    尽管我们已经在保护模式中,但只有段机制而没有启用页机制。这里设置全局页目录与页表项,并开启分页机制。

    下图示意了 Linux 的分页机制(From ULK)。

  3. 初始化 Eflags

  4. 初始化中断向量表

    在实模式中,已经初始化了 IDT,不过现在我们要对保护模式再做一次这样的工作。由于这段代码比较长,放在了单独的函数里。

    485 setup_idt:
    		# 默认中断处理例程,后面有定义,做一件事情:如果开启了 CONFIG_PRINTK,就通过 printk 输出内核信息。
    486         lea ignore_int,%edx
    		# 这里是内核代码段,注意已经是保护模式了,所以要用代码段选择子
    487         movl $(__KERNEL_CS << 16),%eax
    488         movw %dx,%ax            /* selector = 0x0010 = cs */
    489         movw $0x8E00,%dx        /* interrupt gate - dpl=0, present */
    490 
            # 载入 IDT 表的首地址
    491         lea idt_table,%edi
            # 共有 256 个中断向量
    492         mov $256,%ecx
    493 rp_sidt:
            # 这是一个循环,用默认中断处理例程初始化 256 个中断向量
    494         movl %eax,(%edi)
    495         movl %edx,4(%edi)
    496         addl $8,%edi
    497         dec %ecx
    498         jne rp_sidt
    499 
    		# 设置几个已定义的中断向量
    		# 宏定义
    500 .macro  set_early_handler handler,trapno
    501         lea \handler,%edx
    502         movl $(__KERNEL_CS << 16),%eax
    503         movw %dx,%ax
    504         movw $0x8E00,%dx        /* interrupt gate - dpl=0, present */
    505         lea idt_table,%edi
    506         movl %eax,8*\trapno(%edi)
    507         movl %edx,8*\trapno+4(%edi)
    508 .endm
    509 		# 预先设置的中断向量
    510         set_early_handler handler=early_divide_err,trapno=0			# 被零除
    511         set_early_handler handler=early_illegal_opcode,trapno=6		# 操作码异常
    512         set_early_handler handler=early_protection_fault,trapno=13		# 保护错误
    513         set_early_handler handler=early_page_fault,trapno=14		# 缺页异常
    514		# 后面一段代码定义了这四个中断向量的中断处理例程。
    		# 它们都调用了 early_fault,即将当前状态、中断向量号等信息通过 early_printk 或 printk 输出。
    515         ret
    
  5. 检查处理器类型

  6. 载入 GDT、IDT

  7. i386_start_kernel

    如果是 SMP 架构,则由第一个 CPU 调用 start_kernel,其余 CPUs 调用 initialize_secondary

    跳转到 i386_start_kernel(在 arch/x86/kernel/head32.c)

head_32.S 中的其余代码是 BSS 段、数据段。

其中,下面这段数据描述了发生未知异常时内核输出的调试信息。

655 int_msg:
656         .asciz "Unknown interrupt or fault at EIP %p %p %p\n"
657 
658 fault_msg:
659 /* fault info: */
660         .ascii "BUG: Int %d: CR2 %p\n"
661 /* pusha regs: */
662         .ascii "     EDI %p  ESI %p  EBP %p  ESP %p\n"
663         .ascii "     EBX %p  EDX %p  ECX %p  EAX %p\n"
664 /* fault frame: */
665         .ascii "     err %p  EIP %p   CS %p  flg %p\n"
666         .ascii "Stack: %p %p %p %p %p %p %p %p\n"
667         .ascii "       %p %p %p %p %p %p %p %p\n"
668         .asciz "       %p %p %p %p %p %p %p %p\n"

下图为 x86 体系结构下的段描述符格式(From ULK)。

arch/x86/kernel/head32.c 中的 i386_start_kernel 只有一条语句 start_kernel(),将跳转到体系结构无关部分的 init/main.c line 534,执行核心数据结构初始化。

5 内核初始化:体系结构无关部分

5.1 核心数据结构初始化

start_kernel 为什么值得开启新的一章呢?因为我们已经跳出了体系结构相关部分,离开了复杂的汇编代码,可以在 C 语言的世界里自由翱翔了。

本节摘抄自参考文献:Linux启动过程综述

start_kernel()中调用了一系列初始化函数,以完成kernel本身的设置。这些动作有的是公共的,有的则是需要配置的才会执行的。

至此,基本的核心环境已经建立起来了。

5.2 设备初始化

本节摘抄自参考文献:Linux启动过程综述

init()函数作为核心线程,首先锁定内核(仅对SMP机器有效),然后调用 do_basic_setup()完成外设及其驱动程序的加载和初始化。过程如下:

至此do_basic_setup()函数返回init(),在释放启动内存段(free_initmem())并给内核解锁以后,init()打开/dev/console设备,重定向stdin、stdout和stderr到控制台,最后,搜索文件系统中的init程序(或者由init=命令行参数指定的程序),并使用 execve()系统调用加载执行init程序。

init()函数到此结束,内核的引导部分也到此结束了,这个由start_kernel()创建的第一个线程已经成为一个用户模式下的进程了。此时系统中存在着六个运行实体:

参考文献

涉及的代码:


Copyright © 2012 李博杰 PB10000603

This document is available from http://home.ustc.edu.cn/~boj/courses/linux_kernel/1_boot.html