Kernel引导入口

Kernel引导入口
    在arch/alpha/vmlinux.lds的链接脚本控制下,链接程序将vmlinux的入口置于 "arch/alpha/kernel/head.S"中的__start上,因此当Bootloader跳转到0x100000时, __start处的代码开始执行。__start的代码很简单,只需要设置一下全局变量,然后就跳转到start_kernel去了。start_kernel()是"init/main.c"中的asmlinkage函数,至此,启动过程转入体系结构无关的通用C代码中。


对于I386平台
    在i386体系结构中,因为i386本身的问题,在"arch/alpha/kernel/head.S"中需要更多的设置,但最终也是通过call SYMBOL_NAME(start_kernel)转到start_kernel()这个体系结构无关的函数中去执行了。

    所不同的是,在i386系统中,当内核以bzImage的形式压缩,即大内核方式(__BIG_KERNEL__)压缩时就需要预先处理bootsect.S和setup.S,按照大核模式使用$(CPP) 处理生成bbootsect.S和bsetup.S,然后再编译生成相应的.o文件,并使用 "arch/i386/boot/compressed/build.c"生成的build工具,将实际的内核(未压缩的,含 kernel中的head.S代码)与"arch/i386/boot/compressed"下的head.S和misc.c合成到一起,其中的head.S代替了"arch/i386/kernel/head.S"的位置,由Bootloader引导执行(startup_32入口),然后它调用misc.c中定义的decompress_kernel()函数,使用 "lib/inflate.c"中定义的gunzip()将内核解压到0x100000,再转到其上执行 "arch/i386/kernel/head.S"中的startup_32代码。

对于2.4.x版内核
    没有变化。

三.核心数据结构初始化--内核引导第一部分
    start_kernel()中调用了一系列初始化函数,以完成kernel本身的设置。这些动作有的是公共的,有的则是需要配置的才会执行的。

在start_kernel()函数中,

    ·输出Linux版本信息(printk(linux_banner))
    ·设置与体系结构相关的环境(setup_arch())
    ·页表结构初始化(paging_init())
    ·使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init())
    ·使用alpha_mv结构和entry.S入口初始化系统IRQ(init_IRQ())
    ·核心进程调度器初始化(包括初始化几个缺省的Bottom-half,sched_init())
    ·时间、定时器初始化(包括读取CMOS时钟、估测主频、初始化定时器中断等,time_init())
    ·提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_options())
    ·控制台初始化(为输出信息而先于PCI初始化,console_init())
    ·剖析器数据结构初始化(prof_buffer和prof_len变量)
    ·核心Cache初始化(描述Cache信息的Cache,kmem_cache_init())
    ·延迟校准(获得时钟jiffies与CPU主频ticks的延迟,calibrate_delay())
    ·内存初始化(设置内存上下界和页表项初始值,mem_init())
    ·创建和设置内部及通用cache("slab_cache",kmem_cache_sizes_init())
    ·创建uid taskcount SLAB cache("uid_cache",uidcache_init())
    ·创建文件cache("files_cache",filescache_init())
    ·创建目录cache("dentry_cache",dcache_init())
    ·创建与虚存相关的cache("vm_area_struct","mm_struct",vma_init())
    ·块设备读写缓冲区初始化(同时创建"buffer_head"cache用户加速访问,buffer_init())
    ·创建页cache(内存页hash表初始化,page_cache_init())
    ·创建信号队列cache("signal_queue",signals_init())
    ·初始化内存inode表(inode_init())
    ·创建内存文件描述符表("filp_cache",file_table_init())
    ·检查体系结构漏洞(对于alpha,此函数为空,check_bugs())
    ·SMP机器其余CPU(除当前引导CPU)初始化(对于没有配置SMP的内核,此函数为空,smp_init())
    ·启动init过程(创建第一个核心线程,调用init()函数,原执行序列调用cpu_idle() 等待调度,init())

至此start_kernel()结束,基本的核心环境已经建立起来了。

对于I386平台
    i386平台上的内核启动过程与此基本相同,所不同的主要是实现方式。

对于2.4.x版内核
    2.4.x中变化比较大,但基本过程没变,变动的是各个数据结构的具体实现,比如Cache。

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

    ·总线初始化(比如pci_init())
    ·网络初始化(初始化网络数据结构,包括sk_init()、skb_init()和proto_init()三部分,在proto_init()中,将调用protocols结构中包含的所有协议的初始化过程,sock_init())
    ·创建bdflush核心线程(bdflush()过程常驻核心空间,由核心唤醒来清理被写过的内存缓冲区,当bdflush()由kernel_thread()启动后,它将自己命名为kflushd)
    ·创建kupdate核心线程(kupdate()过程常驻核心空间,由核心按时调度执行,将内存缓冲区中的信息更新到磁盘中,更新的内容包括超级块和inode表)
    ·设置并启动核心调页线程kswapd(为了防止kswapd启动时将版本信息输出到其他信息中间,核心线调用kswapd_setup()设置kswapd运行所要求的环境,然后再创建 kswapd核心线程)
    ·创建事件管理核心线程(start_context_thread()函数启动context_thread()过程,并重命名为keventd)
    ·设备初始化(包括并口parport_init()、字符设备chr_dev_init()、块设备 blk_dev_init()、SCSI设备scsi_dev_init()、网络设备net_dev_init()、磁盘初始化及分区检查等等,device_setup())
    ·执行文件格式设置(binfmt_setup())
    ·启动任何使用__initcall标识的函数(方便核心开发者添加启动函数,do_initcalls())
    ·文件系统初始化(filesystem_setup())
    ·安装root文件系统(mount_root())

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


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

    ·start_kernel()本身所在的执行体,这其实是一个"手工"创建的线程,它在创建了init()线程以后就进入cpu_idle()循环了,它不会在进程(线程)列表中出现
    ·init线程,由start_kernel()创建,当前处于用户态,加载了init程序
    ·kflushd核心线程,由init线程创建,在核心态运行bdflush()函数
    ·kupdate核心线程,由init线程创建,在核心态运行kupdate()函数
    ·kswapd核心线程,由init线程创建,在核心态运行kswapd()函数
    ·keventd核心线程,由init线程创建,在核心态运行context_thread()函数

4.2.4  init_post函数(http://book.51cto.com/art/201007/213609.htm)

到init_post函数为止,内核的初始化已经进入尾声,第一个用户空间进程init将姗姗来迟。

代码清单4.4  init_post函数

774 static int noinline init_post(void)  775 {  776     free_initmem();  777     unlock_kernel();  778     mark_rodata_ro();  779     system_state = SYSTEM_RUNNING;  780     numa_default_policy();  781   782     if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)  783         printk(KERN_WARNING "Warning: unable to open an initial console.\n");  784   785     (void) sys_dup(0);  786     (void) sys_dup(0);  787   788     if (ramdisk_execute_command) {  789         run_init_process(ramdisk_execute_command);  790         printk(KERN_WARNING "Failed to execute %s\n",  791                 ramdisk_execute_command);  792     }  793   794     /*  795      * We try each of these until one succeeds.  796      *  797      * The Bourne shell can be used instead of init if we are  798      * trying to recover a really broken machine.  799      */  800     if (execute_command) {  801         run_init_process(execute_command);  802         printk(KERN_WARNING "Failed to execute %s.  Attempting "  803                     "defaults...\n", execute_command);  804     }  805     run_init_process("/sbin/init");  806     run_init_process("/etc/init");  807     run_init_process("/bin/init");  808     run_init_process("/bin/sh");  809   810     panic("No init found.  Try passing init= option to kernel.");  811 } 第776行,到此,内核初始化已经接近尾声了,所有的初始化函数都已经被调用,因此free_initmem函数可以舍弃内存的__init_begin至__init_end(包括.init.setup、.initcall.init等节)之间的数据。

所有使用__init标记过的函数和使用__initdata标记过的数据,在free_initmem函数执行后,都不能使用,它们曾经获得的内存现在可以重新用于其他目的。

第782行,如果可能,打开控制台设备,这样init进程就拥有一个控制台,并可以从中读取输入信息,也可以向其中写入信息。

实际上init进程除了打印错误信息以外,并不使用控制台,但是如果调用的是shell或者其他需要交互的进程,而不是init,那么就需要一个可以交互的输入源。如果成功执行open,/dev/console即成为init的标准输入源(文件描述符0)。

第785~786行,调用dup打开/dev/console文件描述符两次。这样,该控制台设备就也可以供标准输出和标准错误使用(文件描述符1和2)。假设第782行的open成功执行(正常情况),init进程现在就拥有3个文件描述符--标准输入、标准输出以及标准错误。

第788~804行,如果内核命令行中给出了到init进程的直接路径(或者别的可替代的程序),这里就试图执行init。

因为当kernel_execve函数成功执行目标程序时并不返回,只有失败时,才能执行相关的表达式。接下来的几行会在几个地方查找init,按照可能性由高到低的顺序依次是: /sbin/init,这是init标准的位置;/etc/init和/bin/init,两个可能的位置。

第805~807行,这些是init可能出现的所有地方。如果在这3个地方都没有发现init,也就无法找到它的同名者了,系统可能就此崩溃。因此,第808行会试图建立一个交互的shell(/bin/sh)来代替,希望root用户可以修复这种错误并重新启动机器。

第810行,由于某些原因,init甚至不能创建shell。当前面的所有情况都失败时,调用panic。这样内核就会试图同步磁盘,确保其状态一致。如果超过了内核选项中定义的时间,它也可能会重新启动机器。

InitTab文件详解 (2007-06-02 20:51) http://blog.chinaunix.net/space.php?uid=20164485&do=blog&cuid=313074
InitTab文件详解
作者
Init 由 Miquel van Smoorenburg ([email protected]) 所写. 本手册页由 Sebastian Lederer ([email protected]) 所写, 由 Michael Haardt ([email protected]) 修改.

  
NAME
inittab - 与 sysv 兼容的 init 进程使用的初始化文件格式
描述
inittab 文件描述在系统引导及通常的操作期间, 都启动哪些进程 (比如 /etc/init.d/boot, /etc/init.d/rc, getty 等等). Init(8) 讨论有关 runlevels (运行级) 的概念, 每一个运行级都有它自己启动进程的集合. 有效的运行级为 0-6 加上用于 ondemand 条目的 A, B 和 C. inittab 文件中的每一个条目有如下的格式:
    id:runlevels:action:process
     
以 `#' 开头的行被忽略.
id
    inittab 文件中条目的唯一标识, 限于 1-4 个字符 (如果是用版本号小于 5.2.18 或 a.out 的库编译生成的 sysvinit 程序, 则仅限于 2 个字符).
    注意: 对于 getty 或其它的注册进程, id 必须是响应的终端线路的 tty 后缀, 如 1 响应 tty1, 否则, 注册过程不能正常的工作.
runlevels
    列出发生指定动作的运行级. 
action
    描述要发生的动作. 
process
    要执行的进程. 如果 process 域以一个 `+' 开头, init 不会在 utmp 和 wtmp 文件中为此进程记帐. 这是由于 getty 自己主持 utmp/wtmp 记帐的需要, 同时这也是一个历史遗留的漏洞. 
runlevels 域可以包含表示不同运行级的多个字符, 例如 123 表示本进程在运行级为 1, 2 和 3 时都要启动. 用于 ondemand 条目的 runlevels 域可以包含 A, B, 或 C. 用于 sysinit, boot, 和 bootwait 条目的 runlevels 域被忽略.
当改变运行级时, 在新运行级中没有给出的那些正在运行的进程被杀死, 先使用 SIGTERM 信号, 然后是 SIGKILL.
action 域可以使用的动作有:
respawn
    该进程只要终止就立即重新启动 (如 getty). 
wait
    只要进入指定的运行级就启动本进程, 并且 init 等待该进程的结束. 
once
    只要进入指定的运行级就启动一次本进程. 
boot
    在系统引导期间执行本进程. runlevels 域被忽略. 
bootwait
    在系统引导期间执行本进程. 并且 init 等待该进程的结束 (如 /etc/rc). runlevels 域被忽略. 
off
    什么也不做. 
ondemand
    在进入 ondemand 运行级时才会执行标记为 ondemand 的那些进程. 无论怎样, 实际上没有改变运行级 (ondemand 运行级就是 `a', `b', 和 `c'). 
initdefault
    initdefault 条目给出系统引导完成后进入的运行级, 如果不存在这样的条目, init 就会在控制台询问要进入的运行级. process 域被忽略. 
sysinit
    系统引导期间执行此进程. 本进程会在 boot 或 bootwait 条目之前得到执行. runlevels 域被忽略. 
powerwait
    本进程在电源不足时执行. 通常在有进程把 UPS 和计算机相连时通知 init 进程, Init 在继续其它工作之前要等待此进程结束. 
powerfail
    类似 powerwait, 但是init 不等待此进程完成. 
powerokwait
    在 init 收到电源已经恢复的通知后立即执行此进程. 
powerfailnow
    本进程在 init 被告知 UPS 电源快耗尽同时外部电源失败 (无效) 时被执行. (假设 UPS 和监视进程能够发现这样的情况). 
ctrlaltdel
    在 init 收到 SIGINT 信号时执行此进程. 这意味着有人在控制台按下了 CTRL-ALT-DEL 组合键, 典型地, 可能是想执行类似 shutdown 然后进入单用户模式或重新引导机器. 
kbrequest
    本进程在 init 收到一个从控制台键盘产生的特殊组合按键信号时执行.
    对于此功能本文档尚未完成; 可以在 kbd-x.xx 包中找到更多信息 (在写作本文档时最新的是 kbd-0.94). 当然你可能想为某些 "KeyboardSignal" 行为映射组合键, 如为了映射 (Alt-上箭头) 可以在键盘映射文件中 使用如下的方式:
        alt keycode 103 = KeyboardSignal
         
举例
这是一个与老的 Linux inittab 文件类似的例子文件:
    # inittab for linux
    id:1:initdefault:
    rc::bootwait:/etc/rc
    1:1:respawn:/etc/getty 9600 tty1
    2:1:respawn:/etc/getty 9600 tty2
    3:1:respawn:/etc/getty 9600 tty3
    4:1:respawn:/etc/getty 9600 tty4
     
本文件在引导时执行 /etc/rc 并且在 ty1-tty4 上启动 getty 进程.
一个更详尽的 inittab 会有不同的运行级 (参考本身的注释):
    # 进入默认的运行级
    id:2:initdefault:
    # 在进行其它工作之前先完成系统初始化.
    si::sysinit:/etc/rc.d/bcheckrc
    # 运行级 0 挂起系统, 6 重新引导, 1 单用户模式.
    l0:0:wait:/etc/rc.d/rc.halt
    l1:1:wait:/etc/rc.d/rc.single
    l2:2345:wait:/etc/rc.d/rc.multi
    l6:6:wait:/etc/rc.d/rc.reboot
    # "3 个键" 按下时要做的工作.
    ca::ctrlaltdel:/sbin/shutdown -t5 -rf now
    # 运行级2和3: 在控制台生成 getty 进程, 运行级为3时在 modem 上生成 getty.
    1:23:respawn:/sbin/getty tty1 VC linux
    2:23:respawn:/sbin/getty tty2 VC linux
    3:23:respawn:/sbin/getty tty3 VC linux
    4:23:respawn:/sbin/getty tty4 VC linux
    S2:3:respawn:/sbin/uugetty ttyS2 M19200

你可能感兴趣的:(Linux)