Linux内核启动过程(Linux 2.4)

这篇博客详细介绍了Linux 2.4内核的启动过程,从CPU的初始化开始,包括实模式到保护模式的转变、引导扇区加载、内核解压和内存映射的设置,再到系统的32位保护模式启动,系统初始化的三个阶段,如CPU寄存器设置、页式内存管理和中断向量表的构建。文章深入探讨了内核如何逐步接管硬件资源,为后续的操作系统运行做好准备。

CPU初始化

  • CPU加电, RESET重重寄存器数据 CS:0xffff IP:0

  • 进入实地址模式(0-0xfffff) seg(16bit)+offset(16bit): seg<<4 + offset (0~1MB)

  • 执行第一条指令(线性地址 0xffff0 EPROM映射)进入初始引导程序(可用空间0x0C0000-0xfffff)

    初始引导程序: 从磁盘读入引导扇区的程序. 由于EPROM的空间发展, 后逐渐增加硬件检测, 设备驱动(Linux未采用)等一起组成BIOS.

  • 有的机器会在读入引导扇区中间引入中间步骤, 成为引导装入程序, 例如LILO.作用就是可以让用户有选择的引导对应的
    操作系统映像.

  • 从主引导记录块(MBR, 硬盘的第一个扇区)读取引导位置(其他逻辑扇区的引导扇区)-- 每个扇区512B (目前有4KB)

  • Linux的引导扇区由bootSec.s定义. 汇编后需要压缩, 大小不超过512B.借助其他辅助程序(setup, compressed)可以将
    内核vmlinux解压装载进内存.
    bootsec被bios装载到0x7c00处之后自己会移动至0x90000, 然后jump到该处执行. (此时是16位实地址模式)

  • 然后, setup程序被装载到0x90200处, 系统映像于0x10000.(利用的是BIOS的中断处理INT 0x13)
    实际上内核映像是压缩而成, 将引导扇区和引导辅助程序的映像拼接到一起. 大小超过(508KB=512KB-4KB)的称为大内核(后缀bzImage), 小者称为小映像(zImage). 内核会解压后都会装载在0x100000(1MB).

  • setup执行过程中将由16位实地址转化为32位保护模式的段寻址方式, setup解压缩 装载 内核映像至0x100000.

  • 开启PE, 装载GDT IDT

  • 跳转到0x100000, 开始内核初始化.

内核初始化

系统初始化-1

  • SMP结构系统中主CPU开始执行start_up, 位于物理地址0x100000处, 这部分地址是位于线性映射区, 那么虚拟地址为(0xC0000000+0x100000 = 0xC0100000).

    但是段描述表中的__KERNEL_DS的值为0, CS被设置成0, IP被ljmp 0x10000 设置成0x10000,所以CPU在进入startup_32之后ahis使用物理地址取指令.(实际上不跳转的话是可以继续执行的)

  • 首先, CPU将除CS外所有段寄存器(%ds, %es, %fs%gs)设置成__KERNEL_DS(0x18 表示GDT中下表为3的描述项目, RPL为0, 段基址为0)

  • 填入2个用户页目录项和766个空白页目录项(BOOT_USR_PGD_PTRS= 768) , 2个内核页目录项目254个页目录项(BOOT_KERNEL_PGD_PTRS=256)总数为1024
    所以总空间为1K1K4KB = 4GB虚拟存储.

    注意到: 用户空间和内核空间都映射了开头的两个页目录项目而且值相同, 表示指向了相同的页面映射表.
    用户区放同样目录项目的原因是为了平稳过度, CPU进入start_up()到开启页式映射前都是使用的物理地址, 在
    这种情况下, 如果映射目录只包含虚存高区映射,那么一旦开启页式映射, 因为此时CPU的取指令IP仍然指向低区, 仍然会以物理地址取指令(书中的描述我认为有问题, 实际上分页后这里IP是没有变化(没有加_PAGE_OFFSET)的所以访问的虚拟地址是指向低区, 那么如果该地址没有映射则无法确认物理页, 这里巧妙的设置了双份目录项就是为了访问低区地址仍然映射到相同的物理区不影响执行). 直至发生绝对地址跳转.

    解决方案:

    1. 先开启页式映射, 但是在虚存空间的低区暂时提供与高区相同的映射, CPU继续可执行.
    2. 开启了页式映射后, 以一个符号地址(有偏移了)为目标执行一条绝对转移指令.强制转换到高地址区.
  • pg0empty_zero_page之间的8K设置成页面映射表.(主CPU负责初始化, 次CPU跳过该步骤)
    页表含2K个表项, 代表8MB大小的存储空间.

    • pg0被指定到起点地址0x2000物理地址为0x00102000
  • 开启页面映射机制: 将页面目录的地址(物理地址)装入控制寄存器cr3 并把 cr0中的最高位置为1.

  • CPU转入系统空间, 清除低区映射(等次CPU完全进入). 如果内核尝试访问用户空间的虚拟地址则会发生页面异常.

  • 进入stack_start设置CPU堆栈区, task_struct数据结构和系统空间堆栈一共占用8KB(两个页面). init_task_union使用 INIT_TASK()初始化数据, 整个进程的tread全部定义为0. 这个进程为"swapper".

  • 初始化内核.bss段 清零从_bss_start_end所有数据.

  • 设置初始状态的中断向量表(IDT) 通过setup_idt, 一共 8B * 256 = 2KB大小

    初始状态的IDT描述项内容全部相同, 都指向ignore_int(). 后续会将IDT地址写入IDTR, 打开中断.

  • 利用引导参数 将LILO的命令行传入参数, setup阶段从BIOS收集的数据通过empty_zero_page传入.

  • 进入check_cputype 确定当前CPU(利用cpuid)

  • 正式启用内核全局段描述表和中断描述表(弃用setup阶段临时的) lgdt, lidt 指令装入Xgt_desc_struct结构数据, GDTR被设置后需要将各个段寄存器的内容重新装入. LDTR设置为0(Linux不使用)

  • SMP架构中率先完成第一阶段初始化的CPU, ready变量被置为1, 表示为主CPU. 其调用start_kernel开启第二阶段. 其他通过initialize_secondary后空转进程. 等到主CPU完成第二阶段初始化.

系统初始化-2

  • start_kernel 首先通过printk()输出内核版本信息, 内核编译环境等

  • setup_arch 处理系统架构设置,这里包括一些架构的特殊设置, 主体部分先确立根设备ROOT_DEV的设备号(empty_zero_page由参数传入)

    补充: BIOS扫描的硬件信息被放入参数块中, 因为setup阶段后linux无法通过BIOS获取内存信息, 所以BIOS会事先生成内存构成图放入参数块中,
    内存构成图又称为e820图(通过 int 0x15 调用时传入参数为:0xe820).

  • setup_memory_region负责将内存构成拷贝出参数块, 生成e820entry数组称为e820map, 数组中每一项都是对一个物理内存区的描述.

    一共分为四种类型:

    • E820_RAM
    • E820_RESERVED
    • E820_ACPI Advanced Configuration and Power Management Interface
    • E820_NVS Non-Volatile Storage: ROM, EPROM, Flash

    早期PC 机器中分配设置

    • (0x00000 - 0x9FFFF) RAM(640KB)
    • (0xA0000 - 0xEFFFF) VGA, EGA
    • (0xF0000 - 0xFFFFF) BIOS
      可见当时设计默认RAM最大640K, 但是之后的发展使得不适用, 但是为了兼容性仍然会保留0xA0000到0xFFFFF的空间
      所以如果RAM在映射时会跳过该区域.
  • 设置数据结构init_mmcode_resource, 接着调用parse_mem_cmdline()处理命令行中对于内存的特殊设置(如PAE,PSE模式的切换).

    init_mm是系统第一个进程swapper的存储空间控制结构, 也是整个内核mm_struct数据结构.

  • 定义start_pfn(pfn:page frame number) 页面号: 内存中内核映像边界(_end)上第一个可以动态分配的页面. 从该点开始以上就是可以动态分配的空间
    了(PFN_UP获取上面的一个页面边界, PFN_DOWN下面 参数是虚存地址返回物理页面号).

  • 计算max_pfn: RAM空间的顶点, 代表系统最高RAM页面.

    在(3G以上的1GB虚存空间是直接映射区)原有的线性映射区可能通过vmalloc, RAMDISK重复建立映射,会导致一个物理页对应多个虚存页面这样
    可直接访问的物理页面必然会减少, 这部分空间被限制在128MB, 所以RAM最大空间容量是1024MB-128MB=896MB(MAXMEM的值), 相应的页号为MAXMEM_PFN.记录到max_low_pfn中(不高于max_pfn).
    128MB被用于vmallocinitrd

    TODO: RAMDISK

    对于超过896MB的RAM, 需启用CONFIG_HIGHMEM 编译内核, 并且HIGH_MEM是指4GB.

  • 通过init_bootmem()为物理内存页面管理做准备, 建立物理内存的页面位图(bitmap)

    该位图用于确定物理内存哪些页面可用于动态分配.(0~max_low_pfn)

    • pg_data_t数据结构contig_page_data进行初始化. UMA中该数据只有一个, NUMA则多个.
      pg_data_t结构代表着一片均匀连续的内存空间, 称为一个节点. 系统中各个节点的pg_data_t通过node_next链接,
      全局变量pgdat_list则指向链头.
      pg_data_t中有个指向bootmem_data_t的指针, bootmem_data_t记录系统引导以后地一个物理内存页面和物理内存顶点.成员node_bootmem_map
      指向保留页面位图(bootmap_size), 每一位为1代表该物理页需要保留, 从而不能用于动态分配.

      保留的内存区域:

      1. HIGH_MEM到(start_pfn+bootmap_size) 内核映像和保留页面位图.
      2. 页面0, 即empty_zero_page.
      3. 对于SMP结构系统, 页面1, 即起始地址为PAGE_SIZE的页面被用作次CPU的跳板.
      4. 用作RAMDISK的.
  • paging_init() 步骤进一步完善内存分页映射机制:

    • 首先通过pagetable_init扩充startup_32()阶段创建的页面映射目录和页面映射表(当时只开辟了8MB), 而且页面映射目录swapper_pg_dir是从__pgd_offset(PAGE_OFFSET)开始,

      即从虚存地址0xC0000000最高10bit0x300开始, 这是系统空间.(因为前面是用户空间, 需要全部置为0.) 结束页面是max_low_pfn*PAGE_SIZE.

      设置好页目录以后, swapper_pg_dir会被存入cr3中.(这次的目的是刷新TLB)

      此时页面映射物理区间为(0~HIGHMEM)

    • 高于HIGHMEM的物理页面映射: 利用pte_t类型的全局指针kmap_ptr, 该指针指向页面映射表中的一个表项, 这个表项将动态地映射到不同的物理页面.
      相当于想要访问一个属于HIGHMEM的页面就要先改变这个表项. 由kmap_init()初始化.

      TODO: 目前内核的解决方案?

    • 存储pg_data_t数据结构的数组为node_zones, 内核通过它将物理页面划分为三个区域并利用free_area_init初始化空闲页面块队列

      • ZONE_DMA 0x00000000 ~ 0x00FFFFFF 16MB
      • ZONE_NORMAL 0x01000000 ~ 0x03FFFFFF 896MB
      • ZONE_HIGHMEM 0x03900000 ~ 0xFFFFFFFF about 3G
  • 对于地址空间本身Linux内核定义了resource结构, 描述的是一片可以通过I/O操作或内存访问的方式可以访问的连续空间(逻辑连续), 或者操作对象在相应空间的位置.

    这样的对象有RAM,ROM以及一些用于外部设备的器件. resource通过树状结构表示, 内核中有两棵这样的树, 分别为iomem_resourceioport_resource, 分别代表了不同性质的地址资源.

    内核设置resource数组rom_resource[], 用来记录系统中每个ROM空间的位置, 如System ROM为0xF0000到0xFFFFF, 图形ROM空间等.通过probe_roms()扫描区间来将主板上的ROM空间归入
    iomem_resource树以后,根据收集在e802数据结构中的信息, 将BIOS探测到的内存区间也纳入iomem_resource树中统一管理.

    系统固有的I/O资源通过数组standard_io_resources[] 链入ioport_resource树中.至此, setup_arch()工作结束.

  • 之后进行命令行打印 printk saved_command_line

  • parse_options(command_line) 全面处理命令行

    利用数组中的kernel_param中对应的函数指针进行操作. 由内核源码中如__setup("root=",root_dev_setup)宏定义.

  • trap_init

    • TODO: details
    • cpu_init() 设置段寄存器,TSC(time stamp counter), 当前task_struct的指针active_mm的设置,TSS设置, NT位置0等.
  • init_IRQ()

  • sched_init()

  • time_init()

  • softirq_init()

  • init_modules()计算符号表大小

  • mem_init() 清零empty_zero_page free_all_bootmem()

    这里free_all_bootmem是根据页面位图将所有可供动态分配的的物理内存释放, 这里有个技巧,对于位图为0的
    将相应的page结构中的使用计数设置为1, 假装已经分配, 然后调用__free_page(), 这样就把page挂入了所属的空闲页面队列

  • kmem_cache_sizes_init() 是对内存中通用slab分配器初始化, 通过kmem_cache_create() 建立起大小分别为32B, 64B,
    …128KB的缓冲区队列. 这些缓冲区是供kmalloc分配的.

  • 接着proc_root_init对特殊的文件系统/proc初始化.

  • 接下来for_init()计算最大进程个数, 然后proc_cache_init(), buffer_init()kiobuf_setup()signals_inits().
    还有bdev_init()inode_init(), 基本上都是为有关的管理机制建立起其专用的slab缓冲区队列.

  • 接着ipc_init() 初始化SystemV 的进程间通信机制, 对于msg_id, sem, shm初始化. 同时建立proc的目录子节点sysvipc

  • smp_init() 对SMP结构初始化, 启动次CPU, 让他们完成各自的初始化然后进入空转. (start_secondary()->cpu_idle()),

  • 主CPU创建地一个内核线程init, 然后unlock_kernel(),设置当前current->need_resched=1 接受调度.这时候应该会接受成为最先执行init的CPU.其他CPU则位于cpu_idle().

系统初始化-3

该阶段位于init中

  • lock_kernel()后调用 do_basic_setup()free_initmem().
    • do_basic_setup()阶段会有一些设备初始化(这里省略), 然后创建内核的第二个进程keventd 是守护线程(daemon)
      入口为context_thread().这个进程的主体是deadloop, 平时在一个队列context_task_wq中睡眠等待.
      在一些设备驱动中, 一些函数需要在上下文中执行, 那么这个keventd就是充当代理人的身份, 需要将函数交割keventd, 使得它在keventd的上下文中执行.
      为此函数准备一个tq_strcut数据, 然后通过schedule_task()把这个数据结构挂入队列tq_context, 并唤醒在队列中睡眠的keventd来执行.
      之后通过do_initcalls()将初始化代码段和初始化调用段, 内核代码中加上__init限定词的都是代表了在系统初始化时调用, 之后便不再使用. 这些函数在编译链接之后就全部集中在初始化代码段中, 之后可以集中回收. 这些函数相互独立, 对调用顺序独立, 所以do_initcalls采取函数指针遍历的方式调用.编程时所有静态的模块都是通过__initcall()说明, 只不过是通过定义module_init()间接使用.

文件系统根设备安装, 一般设备的安装都是通过/dev目录找到设备节点, 但是根设备不同(/dev 目前还未建立) 除非root=指定, 否则通过引导扇区的设备号bdev = bdget(kdev_t_to_nr(ROOT_DEV)); 这之后试读超级块执行安装. 然后set_fs_rootset_fs_pwd, 使得当前进程的工作目录和文件系统根目录确定, 之后所有进程将会继承.

  • free_initmem会逐个页面地回收整个初始化代码段的内存空间, 其代码在arch/i386/mm/init.c中.至此
    内核的初始化已经完成.
  • open /dev/console 作为fd=0 之后哦dup(0) 两次 形成标准输入0 标准输出1 和标准错误2. 然后
    执行execve依次尝试/sbin/init /etc/init /bin/init /bin/sh

至此, 看到终端显示shell表示启动完成.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值