下面给出内核映像完整的启动过程:
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粘合剂把各种体系结构的代码“粘合”到一个统一的入口处。
整个内核启动过程如下图:
图1 Linux内核启动过程