Linux Kernel启动过程浅析

PC上Linux的启动过程通常是:BIOS初始化并加载MBR,跳转到MBR所处位置执行bootloader(例如grub)的代码,bootloader启动后加载内核镜像文件并跳转到镜像文件处执行,此时才真正进入到linux kernel的启动过程。本文主要分析Linux kernel的启动过程,代码均基于linux-3.4.4。

Linux Kernel镜像文件的组成

Linux Kernel镜像文件的组成可以理解为512字节的boot sector区、kernel setup代码区及vmlinux组成。512字节的boot sector区包含一些参数,其中一个很重要的参数指出了kernel setup代码区大小;kernel setup代码区(setup.bin)主要完成系统检测及运行环境初始化的工作;vmlinux包含解压缩代码区以及被压缩的内核。

Linux Kernel启动过程

Bootloader将boot sector区、kernel setup代码区加载到0x001000后的某个"low memory"处,并将 vmlinux加载到内存0x100000,最后将控制权交给kernel setup。

kernel setup的入口在arch/x86/boot/header.S文件第256行的_start处,此处跳转到该文件的第389行start_of_setup准备C代码运行环境(包括堆栈设置、setup标签检查、BSS段清零等),然后在该文件的449行调用arch/x86/boot/main.c文件的main函数。

arch/x86/boot/main.c文件的main函数中,拷贝boot_params,初始化硬件(console的初始化、设置BIOS模式、内存布局检测、video模式设置、GDT和IDT的初始化等),最后调用go_to_protected_mode函数以进入保护模式。

go_to_protected_mode设置好IDT、GDT后,调用arch/x86/boot/pmjump.S文件中第26行的protected_mode_jump,调用参数分别是vmlinux的入口地址以及启动参数所处位置的地址。

protected_mode_jump中设置cr0进入保护模式,并跳转到51行in_pm32处。in_pm32设置好寄存器后,跳转到vmlinux的入口处。

vmlinux的入口地址在arch/x86/boot/compressed/head_32.S文件第34行startup_32处。startup_32将被压缩的内核解压到某处,最后第214行跳转到解压后的内核的入口处。

解压后的内核的入口处arch/x86/kernel/head_32.S文件第87行startup_32处。startup_32将寄存器设置适当的值,并在第202行开始建立初始页表,如下所示:

    202 page_pde_offset = (__PAGE_OFFSET >> 20);
    203 
    204         movl $pa(__brk_base), %edi           // 首个页表的首地址为pa(__brk_base)
    205         movl $pa(initial_page_table), %edx   // 将PGD的首地址保存在edx
    206         movl $PTE_IDENT_ATTR, %eax           // eax初始化为物理地址0x0 #define PTE_IDENT_ATTR 0x003 /* PRESENT+RW */
                                                     // #define PDE_IDENT_ATTR 0x067 /* PRESENT+RW+USER+DIRTY+ACCESSED */ 
    207 10:
    208         leal PDE_IDENT_ATTR(%edi),%ecx          /* Create PDE entry */ 
    209         movl %ecx,(%edx)                        /* Store identity PDE entry */ // 从PGD的第  0项开始保存新建的页表
    210         movl %ecx,page_pde_offset(%edx)         /* Store kernel PDE entry */   // 从PGD的第768项开始保存新建的页表 
    211         addl $4,%edx                       // edx指向PGD的下一项
    212         movl $1024, %ecx                   // 设置ecx为1024
    213 11:
    214         stosl            // 将eax的值保存到edi指向的地址中,并使edi自增4(即初始化新建页表的每一项,完成1024次后edi将指向下一个页表的首地址)
    215         addl $0x1000,%eax        // eax + 4K, 每页映射4KB内存
    216         loop 11b
    217         /*
    218          * End condition: we must map up to the end + MAPPING_BEYOND_END.
    219          */
                // MAPPING_BEYOND_END: 映射内核空间所需的所有页表的大小(若内核空间是1G,其大小为 256 * 4kB,256个页表,每个页表4KB)
                // 此处映射的总内存大小为:从物理地址0x0 处开始,到物理地址 “pa(_end) + 映射内核空间所需的所有页表的大小”结束
    220         movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp  
    221         cmpl %ebp,%eax
    222         jb 10b
    223         addl $__PAGE_OFFSET, %edi
    224         movl %edi, pa(_brk_end)
    225         shrl $12, %eax
    226         movl %eax, pa(max_pfn_mapped)
    227 
    228         /* Do early initialization of the fixmap area */
    229         movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
    230         movl %eax,pa(initial_page_table+0xffc)
在这之后跳转到291行default_entry处。从348行开始准备启用分页机制:

    345 /*
    346  * Enable paging
    347  */
    348         movl $pa(initial_page_table), %eax
    349         movl %eax,%cr3          /* set the page table pointer.. */
    350         movl %cr0,%eax
    351         orl  $X86_CR0_PG,%eax
    352         movl %eax,%cr0          /* ..and set paging (PG) bit */   // 启用分页
    353         ljmp $__BOOT_CS,$1f     /* Clear prefetch and normalize %eip */
    354 1:
    355         /* Shift the stack pointer to a virtual address */
    356         addl $__PAGE_OFFSET, %esp   //校正esp, 旧的esp保存的是物理地址,现在转成虚拟地址 
    357 
    358 /*
    359  * Initialize eflags. Some BIOS's leave bits like NT set. This would
    360  * confuse the debugger if this code is traced.
    361  * XXX - best to initialize before switching to protected mode.
    362  */
    363         pushl $0
    364         popfl
    365 
    366 #ifdef CONFIG_SMP
    367         cmpb $0, ready
    368         jnz checkCPUtype
    369 #endif /* CONFIG_SMP */
    370 
    371 /*
    372  * start system 32-bit setup. We need to re-do some of the things done
    373  * in 16-bit mode for the "real" operations.
    374  */
    375          call setup_idt   //再次初始化IDT
    376 

    377 checkCPUtype:
    378 
    379          movl $-1,X86_CPUID              # -1 for no CPUID initially
    380 
                               ... ...

    468          movl $(__KERNEL_STACK_CANARY),%eax
    469          movl %eax,%gs
    470 
    471          xorl %eax,%eax        # Clear LDT
    472          lldt %ax
    473 
    474          cld                   # gcc2 wants the direction flag cleared at all times
    475          pushl $0              # fake return address for unwinder
    476          movb $1, ready
    477          jmp *(initial_code)     //跳转到 i386_start_kernel
    478                              
                             ... ...
  
    617          __REFDATA
    618 .align 4
    619 ENTRY(initial_code)
    620          .long i386_start_kernel
    621 


最后跳转到 arch/x86/kernel/head32.c文件的i386_start_kernel。i386_start_kernel将内核所在的内存置为reserved,最后调用 init/main.c文件的start_kernel。

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

在start_kernel()函数中,

  • tich_init()
  • boot_cpu_init()
  • 输出Linux版本信息(printk(linux_banner))
  • 设置与体系结构相关的环境(setup_arch(),其中包含paging_init())
  • 提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_args())
  • pid哈希表的初始化(pidhash_init)
  • 使用"arch/x86/kernel/entry_32.S"中的入口点设置系统自陷入口(trap_init())
  • 建立内核内存分配器(设置内存上下界和页表项初始值,核心Cache初始化,vmalloc,mm_init())
  • 进程调度器的初始化(sched_init())
  • 初始化系统IRQ(init_IRQ())
  • 定时器初始化(init_timers())
  • 高精度定时器的初始化(hrtimers_init())
  • 软中断初始化(softirq_init())
  • 剖析器数据结构初始化(prof_buffer和prof_len变量, profile_init())
  • 控制台初始化(为输出信息而先于PCI初始化,console_init())
  • 延迟校准(获得时钟jiffies与CPU主频ticks的延迟,calibrate_delay())
  • pidmap_init()
  • anon_vma_init()
  • fork_init()
  • 块设备读写缓冲区初始化(同时创建"buffer_head"cache用户加速访问,buffer_init())
  • 创建虚拟文件系统cache(vfs_caches_init(), dcache_init(),inode_init(),files_init(),mnt_init())

  • 创建信号队列cache("signal_queue",signals_init())
  • cgroup_init()
  • 检查体系结构漏洞(对于alpha,此函数为空,check_bugs())

start_kernel最后调用rest_init。rest_init 创建第一个核心线程来执行kernel_init(),原执行序列调用cpu_idle()等待调度。

kernel_init调用smp_init()来初始化SMP机器其余CPU(除当前引导CPU),最后调用init_post()来启动init进程。

至此,内核启动完成。


启动过程的调用关系

_start    (arch/x86/boot/header.S)

->start_of_setup(arch/x86/boot/header.S)

->main (arch/x86/boot/main.c)

->go_to_protected_mode (arch/x86/boot/pm.c)

->protected_mode_jump (arch/x86/boot/pmjump.S)

->in_pm32(arch/x86/boot/pmjump.S)

->startup_32 (arch/x86/boot/compressed/head_32.S)

->startup_32 (arch/x86/kernel/head_32.S)

->i386_start_kernel (arch/x86/kernel/head32.c)

->start_kernel (init/main.c)

->rest_init

->kernel_init init进程


参考文章:

1. Linux启动过程综述


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值