Linux内核启动过程分析(5)

下面给出内核映像完整的启动过程:

arch/x86/boot/header.S:   
 --->header第一部分(以前的bootsector.S):  载入bootloader到0x7c00处,设置内核属性
 --->_start()  bzImage映像的入口点(实模式),header的第二部分(以前的setup.S)
  --->code32_start=0x100000  0x100000为解压后的内核的载入地址(1M高端地址)
  --->设置大量的bootloader参数、创建栈空间、检查签名、清空BSS
  --->arch/x86/boot/main.c:main()  实模式内核的主函数
  --->copy_boot_params()  把位于第一个扇区的参数复制到boot_params变量中,boot_params位于setup的数据段
  --->检查内存布局、设置键盘击键重复频率、查询Intel SpeedStep(IST)信息
  --->设置视频控制器模式、解析命令行参数以便传递给decompressor
  --->arch/x86/boot/pm.c:go_to_protected_mode()  进入保护模式
    --->屏蔽PIC中的所有中断、设置GDT和IDT
    --->arch/x86/boot/pmjump.S:protected_mode_jump(boot_params.hdr.code32_start,...)  跳转到保护模式
    --->in_pm32()  跳转到32位保护模式的入口处(即0x100000处)
      --->jmpl *%eax 跳转到arch/i386/boot/compressed/head_32.S:startup_32()处执行
     
arch/i386/boot/compressed/head_32.S:startup_32()  保护模式下的入口函数 
 --->leal boot_stack_end(%ebx), %esp  设置堆栈
 --->拷贝压缩的内核到缓冲区尾部
 --->清空BSS
 --->compressed/misc.c:decompress_kernel()  解压内核
  --->lib/decompress_bunzip2.c:decompress()
  --->lib/decompress_bunzip2.c:bunzip2()
    --->lib/decompress_bunzip2.c:start_bunzip()  解压动作
  --->parse_elf()  将解压后的内核ELF文件(.o文件)解析到内存中
 --->计算vmlinux编译时的运行地址与实际装载地址的距离
 --->jmp *%ebp  跳转到解压后的内核的arch/x86/kernel/head_32.S:startup_32()处运行
 
arch/x86/kernel/head_32.S:startup_32()  32位内核的入口函数,即进程0(也称为清除进程)
 --->拷贝boot_params以及boot_command_line
 --->初始化页表:这会创建PDE和页表集
 --->开启内存分页功能
 --->为可选的浮点单元(FPU)检测CPU类型
 --->head32.c:i386_start_kernel() 
  --->init/main.c:start_kernel()  Linux内核的启动函数,包含创建rootfs,加载内核模块和cpio-initrd
  --->很多初始化操作
  --->setup_command_line()  把内核启动参数复制到boot_command_line数组中
  --->parse_early_param()  体系结构代码会先调用这个函数,做时期的参数检查
    --->parse_early_options()
    --->do_early_param()  检查早期的参数
  --->parse_args()  解析模块的参数
  --->fs/dcache.c:vfs_caches_init()  创建基于内存的rootfs(一个VFS)
    --->fs/namespace.c:mnt_init()
    --->fs/ramfs/inode.c:init_rootfs()
      --->fs/filesystems.c:register_filesystem()  注册rootfs
    --->fs/namespace.c:init_mount_tree()               
      --->fs/super.c:do_kern_mount()  在内核中挂载rootfs
      --->fs/fs_struct.c:set_fs_root() 将rootfs配置为当前内存中的根文件系统
  --->rest_init()
    --->arch/x86/kernel/process.c:kernel_thread(kernel_init,...)  启动一个内核线程来运行kernel_init函数,进行内核初始化
    --->cpu_idle()                            进入空闲循环
    --->调度器周期性的接管控制权,提供多任务处理
   
init/main.c:kernel_init() 内核初始化过程入口函数,加载initramfs或cpio-initrd,或传统的image-initrd,把工作交给它
 --->sys_open("/dev/console",...)  启动控制台设备
 --->do_basic_setup()
  --->do_initcalls()  启动所有静态编译进内核的模块
  --->init/initramfs.c:populate_rootfs()  初始化rootfs
    --->unpack_to_rootfs()  把initramfs或cpio-initrd解压释放到rootfs
    --->如果是image-initrd则拷贝到/initrd.image
####################################### 传统的image-initrd情形 ###########################################
 --->rootfs中没有/init文件
 --->do_mounts.c:prepare_namespace() 加载image-initrd,并运行它的/linuxrc文件,以挂载实际的文件系统
  --->do_mounts_initrd.c:initrd_load()  把image-initrd数据加载到默认设备/dev/ram0中
  --->do_mounts_rd.c:rd_load_image()  加载image-initrd映像
    --->identify_ramdisk_image() 识别initrd,确定是romfs、squashfs、minix,还是ext2
    --->crd_load()  解压并为ramdisk分配空间,计算循环冗余校验码
    --->lib/inflate.c:gunzip()  对gzip格式的ramdisk进行解压
  --->do_mounts_initrd.c:handle_initrd() 指定的根设备不是/dev/ram0,由initrd来挂载真正的根文件系统
    --->mount_block_root("/dev/root.old",...)  将initrd挂载到rootfs的/root下
    --->arch/x86/kernel/process.c:kernel_thread(do_linuxrc, "/linuxrc",...)  启动一个内核线程来运行do_linuxrc函数
    --->do_mounts_initrd.c:do_linuxrc()
      --->arch/x86/kernel/sys_i386_32.c:kernel_execve() 运行image-initrd中的/linuxrc
    --->将initrd移动到rootfs的/old下
    --->若在linuxrc中根设备重新设成Root_RAM0,则返回,说明image-initrd直接作为最终的根文件系统
    --->do_mounts.c:mount_root() 否则将真正的根文件系统挂载到rootfs的/root下,并切换到这个目录下
    --->mount_block_root()
      --->do_mount_root()
      --->fs/namespace.c:sys_mount()  挂载到"/root"
    --->卸载initrd,并释放它的内存
  --->do_mounts.c:mount_root() 没有指定另外的根设备,则initrd直接作为真正的根文件系统而被挂载
  --->fs/namespace.c:sys_mount(".", "/",...)  根文件挂载成功,移动到根目录"/"
########################################################################################################
 --->init/main.c:init_post()  启动用户空间的init进程
  --->run_init_process(ramdisk_execute_command)   若加载了initramfs或cpio-initrd,则运行它的/init
  --->run_init_process("/sbin/init")  否则直接运行用户空间的/sbin/init
  --->arch/x86/kernel/sys_i386_32.c:kernel_execve()  运行用户空间的/sbin/init程序,并分配pid为1
  --->run_init_process("/bin/sh")  当运行init没成功时,可用此Shell来代替,以便恢复机器
 
/init  cpio-initrd(或initramfs)中的初始化脚本,挂载真正的根文件系统,启动用户空间的init进程
 --->export PATH=/sbin:/bin:/usr/sbin:/usr/bin  设置cpio-initrd的环境变量$PATH
 --->挂载procfs、sysfs
 --->解析命令行参数
 --->udevd --daemon --resolve-names=never  启动udev
 --->/initqueue/*.sh  执行/initqueue下的脚本完成对应初始化工作(现在该目录下为空)
 --->/initqueue-settled/*.sh  执行/initqueue-settled下的脚本(现在该目录下为空)
 --->/mount/*.sh  挂载真正的根文件系统
  --->/mount/99mount-root.sh  根据/etc/fstab中的选项挂载根文件系统
  --->/lib/dracut-lib.sh  一系列通用函数
  --->把根文件系统挂载到$NEWROOT下
 --->寻找真正的根文件系统中的init程序并存放在$INIT中 /sbin/init, /etc/init, /bin/init, 或/bin/sh
 --->从/proc/cmdline中获取启动init的参数并存放在$initargs中
 --->switch_root "$NEWROOT" "$INIT" $initargs  切换到根分区,并启动其中的init进程

注意kernel_evecve调用的是与具体体系平台相关的实现,但它是一个通用的系统调用,在linux/syscalls.h中声明,这个头文件中声明了与体系结构无关的所有系统调用接口。只不过kernel_evecve在实现时是与体系结构相关的,每种体系结构都要提供它的实现。
    从以上分析可以看出,如果使用新的cpio-initrd(或initramfs),kernel_init只负责内核初始化(包括加载内核模块、创建基于内存的rootfs以及加载cpio-initrd)。后续根文件系统的挂载、init进程的启动工作都交给cpio-initrd来完成。cpio-initrd相对于image-initrd承担了更多的初始化责任,这种变化也可以看作是内核代码的用户层化的一种体现,实际上精简内核代码,将部分功能移植到用户层必然是linux内核发展的一个趋势。如果是使用传统的image-initrd的话,根文件系统的挂载也会放在kernel_init()中,其中prepare_namespace完成挂载根文件系统,init_post()完成运行/sbin/init,显然这样内核的代码不够精简。   
    5、init进程
    init是第一个调用的使用标准C库编译的程序。在此之前,还没有执行任何标准的C应用程序。在桌面Linux系统上,第一个启动的程序通常是/sbin/init,它的进程号为1。init进程是所有进程的发起者和控制者,它有两个作用:
    (1)扮演终结父进程的角色:所有的孤儿进程都会被init进程接管。
    (2)系统初始化工作:如设置键盘、字体,装载模块,设置网络等。
    在完成系统初始化工作之后,init进程将在控制台上运行getty(登录程序)等任务,我们熟悉的登录界面就出现了!
    init程序的运行流程需要分专门的一节来讨论,因为它有不同的实现方式。传统的实现是基于UNIX System V init进程的,程序包为sysvinit(以前的RedHat/Fedora用的就是这个)。目前已经有多种sysvinit的替代产品了,这其中包括initng,它已经可以用于Debian了,并且在Ubuntu上也能工作。在同一位置上,Solaris使用SMF(Service Management Facility),而Mac OS则使用 launchd。现在广泛使用的是upstart init初始化进程,目前在Ubuntu和Fedora,还有其他系统中已经取代了sysvinit。
    传统的Sysvinit daemon是一个基于运行级别的初始化程序,它使用了运行级别(如单用户、多用户等)并通过从/etc/rcX.d目录到/etc/init.d目录的初始化脚本的链接来启动与终止系统服务。Sysvinit无法很好地处理现代硬件,如热插拔设备、USB硬盘、网络文件系统等。upstart系统则是事件驱动的,事件可能被硬件改动触发,也可被启动或关机或任务所触发,或者也可能被系统上的任何其他进程所触发。事件用于触发任务或服务,统称为作业。比如连接到一个USB驱动器可能导致udev服务发送一个block-device-added事件,这可能引起一个预定任务检查/etc/fstab和挂载驱动器(如果需要的话)。再如,一个Apache web服务器可能只有当网络和所需的文件系统都可用时才能启动。
    Upstart作业在/etc/init目录及其子目录下被定义。upstart系统兼容sysvinit,它也会处理/etc/inittab和System V init脚本(如果有的话)。在诸如近来的Fedora版本的系统上,/etc/inittab可能只含有initdefault操作的id项。目前Ubuntu系统默认没有/etc/inittab,如果您想要指定一个默认运行级别的话,您可以创建一个。Upstart也使用initctl命令来支持与upstart init守护进程的交互。这时您可以启动或终止作业、列表作业、以及获取作业的状态、发出事件、重启init进程,等等。
    总的来说,x86架构的Linux内核启动过程分为6大步,分别为:
    (1)实模式的入口函数_start():在header.S中,这里会进入众所周知的main函数,它拷贝bootloader的各个参数,执行基本硬件设置,解析命令行参数。
    (2)保护模式的入口函数startup_32():在compressed/header_32.S中,这里会解压bzImage内核映像,加载vmlinux内核文件。
    (3)内核入口函数startup_32():在kernel/header_32.S中,这就是所谓的进程0,它会进入体系结构无关的start_kernel()函数,即众所周知的Linux内核启动函数。start_kernel()会做大量的内核初始化操作,解析内核启动的命令行参数,并启动一个内核线程来完成内核模块初始化的过程,然后进入空闲循环。
    (4)内核模块初始化的入口函数kernel_init():在init/main.c中,这里会启动内核模块、创建基于内存的rootfs、加载initramfs文件或cpio-initrd,并启动一个内核线程来运行其中的/init脚本,完成真正根文件系统的挂载。
    (5)根文件系统挂载脚本/init:这里会挂载根文件系统、运行/sbin/init,从而启动众所周知的进程1。
    (6)init进程的系统初始化过程:执行相关脚本,以完成系统初始化,如设置键盘、字体,装载模块,设置网络等,最后运行登录程序,出现登录界面。


    如果从体系结构无关的视角来看,start_kernel()可以看作时体系结构无关的Linux main函数,它是体系结构无关的代码的统一入口函数,这也是为什么文件会命名为init/main.c的原因。这个main.c粘合剂把各种体系结构的代码“粘合”到一个统一的入口处。

整个内核启动过程如下图:

Linux内核启动过程分析

图1 Linux内核启动过程

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/17407.html