Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1204742
  • 博文数量: 232
  • 博客积分: 7563
  • 博客等级: 少将
  • 技术积分: 1930
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-21 11:17
文章分类

全部博文(232)

文章存档

2011年(17)

2010年(90)

2009年(66)

2008年(59)

分类: LINUX

2010-11-20 09:52:49

第二部分:initrd文件探秘
kernel被GRUB加载经内存并执行后会进一步加载initrd文件,它是按照initrd文件中提供的init脚本一步步执行的,所以要掌握initrd文件的执行过程就必须要搞清楚init文件的内容和作用。下面我会重点介绍init文件。

1、首先什么是 initial ram disk (缩写 initrd)   
     
     它是由 bootloader 初始化的内存盘。在 linux 启动之前,bootloader  会将它(通常是 initrd.img-xxx...xxx 文件)加载到内存中。内核启动的时候会将这个文件解开,并作为根文件系 统使用。而启动阶段的驱动模块(如jbd)放在这些文件系统上,内核是无法读取文件系统的,从而只能通过Linux initrd启动的虚拟文件系统来装载这些模块。这里有些人会问: 既然内核此时不能读取文件系统,那内核的文件是怎么装入内存中的呢?答案很简单,Grub是file-system sensitive的,能够识别常见的文件系统。

2、设 计 initrd 的主要目的
目的是让系统的启动分为两个阶段。首先,带有最少但是必要的驱动(这些驱动是在配置内核时选择嵌入方式)的内核启动。然后,其它需要的模块将 从 initrd 中根据实际需要加载(使用udev机制,最重要的根文件系统所在硬盘的控制器接口module)。这样就可以不必将所有的驱动都编译进内核,而根据实际情况有选择地加载。对于启动较慢的设备 如 usb 设备等,如果将驱动编译进内核,当内核访问其上的文件系统时,通常设备还没有准备好,就会造成访问失败。所以,通常 在 initrd 中加载 usb 驱动,然后休眠几秒钟,带设备初始化完成后,再挂载其中的文件系统。  

3、系统上安装的相关软件包
geekard@geekard-laptop:~$ dpkg -l \*ini\* |grep ii
ii  busybox-initramfs          1:1.13.3-1ubuntu11             Standalone shell setup for initramfs
ii  initramfs-tools                0.92bubuntu78                    tools for generating an initramfs
ii  initramfs-tools-bin        0.92bubuntu78               binaries used by initramfs-tools
geekard@geekard-laptop:~$

BusyBox combines tiny versions of many common UNIX utilities into a single   small executable. It provides minimalist replacements for the most common
utilities you would usually find on your desktop system (i.e., ls, cp, mv,  mount, tar, etc.). The utilities in BusyBox generally have fewer options than
their full-featured GNU cousins; however, the options that are included  provide the expected functionality and behave very much like their GNU
counterparts.
initramfs-tools-bin软件包提供initrd文件制作工具命令,但更好的方法是手动制作(下面我会详细介绍)。

4、initrd 的具体形式
    目前有两种形式:cpio-initrd 和 image-initrd。  
    image- initrd 的制作相对麻烦,处理流程相对复杂(内核空间->用户空间->内核空间 与初始化越来越多的在用户空间进 行的趋势不符),主要是2.4及以前的kernle使用,本文不对其进行介绍。

    cpio- initrd 的处理流程(内核空间->用户空间):  
        1. boot loader 把内核以 及 initrd 文件加载到内存的特定位置。  
        2. 内核判断 initrd 的文件格式,如果 是 cpio 格式。  
        3. 将 initrd 的内容释放 到 rootfs 中。  
        4. 执行 initrd 中 的 /init 文件,执行到这一点,内核的工作全部结束,完全交给 /init 文件处 理。

cpio- initrd 的制作:  
     首先在一个目录中建立必要的文件及目录。例如:  
        song@ubuntu:/home/linux_src/initrd/debian_etch/initrd$ ls -l  
         总用量 5  
        drwxr-xr-x  2 song song  864 2007-05-01 21:37 bin  
        drwxr-xr-x  3 song song  160 2007-05-01 21:37 conf  
        drwxr-xr-x  4 song song  136 2007-05-01 21:37 etc  
        -rwxr-xr-x  1 song song 3233 2007-05-02 15:16 init  
        drwxr-xr-x  4 song song  416 2007-05-01 21:37 lib  
        drwxr-xr-x  2 song song   48 2007-04-14 15:59 modules  
        drwxr-xr-x  2 song song  208 2007-05-01 21:37 sbin  
        drwxr-xr-x 11 song song  400 2007-05-01 21:37 scripts  
     然后,将这些内容打成 gzip 压缩过的 cpio 包:  
        song@ubuntu:/home/linux_src/initrd/debian_etch/initrd$ find . | cpio -o -H newc | gzip -9 > ../initrd.img.gz  
        20500 blocks  
        song@ubuntu:/home/linux_src/initrd/debian_etch/initrd$ ls -l ../initrd.img.gz  
        -rw-r--r-- 1 song song 4493175 2007-05-02 17:17 ../initrd.img.gz  
    
initrd文件的解包:  
    首先建立一个空目录,然 后进入那个目录,并运行相应的命令。例如,在 /home/linux_src/initrd/debian_etch 目录下存 在 initrd.img-2.6.18-4-686 文件,我们现在要把它解开,过程如下:  
        song@ubuntu:/home/linux_src/initrd/debian_etch$ mkdir tmp  
        song@ubuntu:/home/linux_src/initrd/debian_etch$ cd tmp  
        song@ubuntu:/home/linux_src/initrd/debian_etch/tmp$ gzip -dc ../initrd.img-2.6.18-4-686 | cpio -idm  
        20500 blocks  
        song@ubuntu:/home/linux_src/initrd/debian_etch/tmp$ ls -l  
         总用量 5  
        drwxr-xr-x  2 song song  864 2007-05-02 17:23 bin  
        drwxr-xr-x  3 song song  160 2007-05-02 17:23 conf  
        drwxr-xr-x  4 song song  136 2007-05-02 17:23 etc  
        -rwxr-xr-x  1 song song 3213 2007-03-08 06:30 init  
        drwxr-xr-x  4 song song  416 2007-05-02 17:23 lib  
        drwxr-xr-x  2 song song   48 2007-04-14 15:59 modules  
        drwxr-xr-x  2 song song  208 2007-05-02 17:23 sbin  
        drwxr-xr-x 11 song song  400 2007-05-02 17:23 scripts  

initrd  中 init 脚本的分析    
     由前面 cpio-initrd 的处理流程可以看到,内核在将其解开并放入 rootfs 后,将要执 行 /init 文件,所以我们分析的重点就是这个文件。其它的文件请结合具体的源码与本文的内容进行理 解。  

#!/bin/sh
该行说明该init文件是一个由sh解释并执行的脚本文件,内核通过文件头来确定应该怎样执行(即是直接执行还是调用哪个程序执行该文件)。

[ -d /dev ] || mkdir -m 0755 /dev
[ -d /root ] || mkdir -m 0700 /root   #这是硬盘上的根分区预先挂载到的目录
[ -d /sys ] || mkdir /sys
[ -d /proc ] || mkdir /proc
[ -d /tmp ] || mkdir /tmp
mkdir -p /var/lock
mount -t sysfs -o nodev,noexec,nosuid none /sys   #udev会参考的vfs,udev会根据其中的信息加载modules和创建设备文件
mount -t proc -o nodev,noexec,nosuid none /proc    

这一部分很简单,建立了相关目录和挂载点,并将kernel运行过程中产生的信息挂载到/sys和/proc目录下。注意/sys目录是udev会参考的 vfs,udev会根据其中的信息加载modules和创建设备文件,当不使用udev机制(我后面会讲)时/sys目录可以不建立。/proc目录和相 应的proc文件系统必须建立和挂载,因为脚本会参考其中的/proc/cmdline文件获得kernel命令行上的参数。

grep -q '\' /proc/cmdline || echo "Loading, please wait..."

如果在GRUB的kernel行上有quiet关键字,则在kernel启动和initrd中init脚本执行的过程中不会在屏幕上显示相关信息而是一个闪烁的下划线,否则将显示"Loading, please wait..."

# Note that this only becomes /dev on the real filesystem if udev's scripts
# are used; which they will be, but it's worth pointing out
if ! mount -t devtmpfs -o mode=0755 none /dev; then  #其实then的代码一般不会执行
    mount -t tmpfs -o mode=0755 none /dev
    mknod -m 0600 /dev/console c 5 1
    mknod /dev/null c 1 3
fi

这一部分在/dev目录下建立devtmpfs文件系统,devtmpfs是一个虚拟的文件系统被挂载后会自动在/dev目录生成很多常见的设备节点文 件,当挂载devtmpfs失败时会手动建立/dev/console和/dev/null设备节点文件。/dev/console 总是代表当前终端,用于输出kernel启动时的输出内容,在最后通 过 exec 命令用指定程序替换当前 shell 时使用。/dev/null 也是很常用的,凡是重定向到它的数据都将消失得无影无踪。

mkdir /dev/pts #主要用于nfs等启动时使用,对于本地/dev/pts不使用,故下面一段代码可忽略
mount -t devpts -o noexec,nosuid,gid=5,mode=0620 none /dev/pts || true
> /dev/.initramfs-tools
mkdir /dev/.initramfs
usplash 会使用/dev/.initramfs 目录。usplash 会在机器启动的时候提供类似 windows的启动画面,ubuntu linux 的启动画面就是通过 usplash 实现的。由于 在 /sbin 目录当中没有任何 usplash 相关的文件,所以我们可以忽略这个目录的存 在。

# Export the dpkg architecture
export DPKG_ARCH=
. /conf/arch.conf
DPKG_ARCH  表明了当前运行linux的计算机的类型,对一般的pc是大多 i386,也可能是别的比如 powerpc 一类的。用export是为了让这个变量不仅在此 shell环境中有效,而且在它的子 shell环境中仍然有效。而且在第27行 export DPKG_ARCH 变量的时候,让 DPKG_ARCH 变量等于空。这样,当前运行的计算机 的类型就完全由 /conf/arch.conf 决定了
# Set modprobe env
export MODPROBE_OPTIONS="-qb"
设置modprobe默认的选项,-b 表示用use-blacklist(主要是系统的硬件有多个modules支持,选择使用哪一个,其他的加入到黑名单中以防冲突) ,-q表示quiet

# Export relevant variables
export ROOT=       #从kernel中提取的realfs所在的设备
export ROOTDELAY=  
export ROOTFLAGS=
export ROOTFSTYPE=
export IPOPTS=
export HWADDR=
export break=
export init=/sbin/init    #realfs中的第一个执行的程序位置
export quiet=n
export readonly=y
export rootmnt=/root        #realfs在rootfs中的临时挂载点
export debug=             #将本脚本的输出定向到一个文件,以便启动系统后分析
export panic=
export blacklist=          #设置modules的黑名单
export resume_offset=

这一部分主要是输出一些变量到环境中
ROOT       保存GRUB的kernel命令行上的root参数即硬盘上根分区所对应的设备节点文件
ROOTDELAY  指定将要进入的系统的根目录所在的分区必 须在多少秒之内准备好
ROOTFLAGS  指定将要进入的系统的根目录所在的分区挂 载到 ${rootmnt} 目录时的参数
ROOTFSTYPE  指定根分区所在的文件系统类型
IPOPTS      当kernel为nfs时指定的服务器IP
HWADDR      当kernel为nfs时指定的服务器MAC
break    由 maybe_break 函数使用。若 break  的值同 maybe_break 的第一个参数相同,则 maybe_break 函数调 用 panic 函数(注意 panic 函数和 panic 变量是不同 的)。 若 panic 变量为"0"(此 处是字符串,其内容是"0",不是整数), 则 panic 函数将重新启动机器。其他情况下(包括 panic 变量为空的情况)都将以交互的方式调出 shell,此shell的输入输出使用已经创建好的节点 /dev/console。
init  指定硬盘realfs中的第一个执行的程序位置, 此变量指定在这个脚本最后要执行的进程。 此处 /sbin/init 是系统上所有进程的父进程,负责开启其它进程。当然,你也可以把它换成其他的程序,甚至是 ls,不一定非要是 /sbin/init,虽然这样你的系统启动之后什么都不能做。
quiet=n  指定为非"y",会显示一些启动的状态信息;若指定为"y"则不显示这些信息。  
readonly=y  如果 readonly 等于字符串"y",则以只读方式挂 载最终要进入的系统的根目录所在分区到 ${rootmnt} 目录,其他情况(包括 readonly 为 空)以读写方式挂载。  
rootmnt=/root        指定硬盘上的realfs在rootfs中的临时挂载点
debug            #将本脚本的输出定向到一个文件,以便启动系统后分析
panic           描述见 break参数的说明。
blacklist        #设置modules的黑名单
resume_offset    一般用不到


# Bring in the main config
. /conf/initramfs.conf    #最重要的是BOOT参数,定义了是本地启动还是nfs启动
for conf in conf/conf.d/*; do  #对于不支持kernel命令行选项的bootloader很有用
    [ -f ${conf} ] && . ${conf}
done
. /scripts/functions    #这个脚本文件很重要,在其中定义了很多以后要用到的工具函数

在当前shell中引入主配置文件 /conf/initramfs.conf。 这个配置文件实际上是 mkinitramfs(8)  的配置文件,其中定义了一些变量,并赋予了适当的值,如 BOOT=local 则默认从本地磁盘启动(可以是可移动磁盘)。 BOOT 变量的值实际上是 /scripts 目录下的一个文件,可以是 local 或 是 nfs。在此 init 脚本挂载将要进入的系统的根目录所在分区的时候,会先读取并运 行 /scripts/${BOOT} 文件。 在这个文件中定义了 mountroot 函数,对于 local 启动和 nfs 启动 此函数的实现不同。这样通过对不同情况引入不同的文件,来达到同样名称的函数行为不同的目的。这就导致了具体挂载的行为和启动方式相关。  

引入 /conf/conf.d 下的所有文件,注意在引入的时候用了 -f 参数判断,这样只有普通的文件才 会被引入,链接(硬链接除外)、目录之类的非普通文件不会被引入,当使用不支持命令行参数的开机引导
程序时,可以在该目录下建立各种参数设置文件。(Uubunt使用的GRUB支持kernel命令行参数,所以这个目录下就上面提到的两个文件initramfs.conf和arch.conf)

# Parse command line options
for x in $(cat /proc/cmdline); do   #为export的变量赋值,最重要的是root
    case $x in
    init=*)                 #指定切换到磁盘上的rootfs上时执行的第一个程序,默认为/sbin/init
        init=${x#init=}
        ;;
    root=*)
        ROOT=${x#root=} #形如:root=/dev/sda3的选项,下面的case代码不会执行
        case $ROOT in
        LABEL=*)        #获取磁盘上rootfs所在的分区设备文件,支持用LABEL,UUID的形式
            ROOT="${ROOT#LABEL=}"  

            # support / in LABEL= paths (escape to \x2f)
            case "${ROOT}" in      #下面是对诸如root=LABEL=/的设备标签转换为root=LABEL=\x2f的形式
            *[/]*)
            if [ -x "$(command -v sed)" ]; then
                ROOT="$(echo ${ROOT} | sed 's,/,\\x2f,g')"
            else
                if [ "${ROOT}" != "${ROOT#/}" ]; then
                    ROOT="\x2f${ROOT#/}"
                fi
                if [ "${ROOT}" != "${ROOT%/}" ]; then
                    ROOT="${ROOT%/}\x2f"
                fi
                IFS='/'
                newroot=
                for s in $ROOT; do
                    if [ -z "${newroot}" ]; then
                        newroot="${s}"
                    else
                        newroot="${newroot}\\x2f${s}"
                    fi
                done
                unset IFS
                ROOT="${newroot}"
            fi
            esac
            ROOT="/dev/disk/by-label/${ROOT}"
            ;;
        UUID=*)
            ROOT="/dev/disk/by-uuid/${ROOT#UUID=}"
            ;;
        /dev/nfs)
            [ -z "${BOOT}" ] && BOOT=nfs
            ;;
        esac
        ;;
    rootflags=*)            #获取kernel命令行上的rootflags参数
        ROOTFLAGS="-o ${x#rootflags=}"
        ;;
    rootfstype=*)
        ROOTFSTYPE="${x#rootfstype=}"
        ;;
    rootdelay=*)
        ROOTDELAY="${x#rootdelay=}"
        case ${ROOTDELAY} in
        *[![:digit:].]*)
            ROOTDELAY=
            ;;
        esac
        ;;
    resumedelay=*)
        RESUMEDELAY="${x#resumedelay=}"
        ;;
    loop=*)
        LOOP="${x#loop=}"
        ;;
    loopflags=*)
        LOOPFLAGS="-o ${x#loopflags=}"
        ;;
    loopfstype=*)
        LOOPFSTYPE="${x#loopfstype=}"
        ;;
    cryptopts=*)
        cryptopts="${x#cryptopts=}"
        ;;
    nfsroot=*)
        NFSROOT="${x#nfsroot=}"
        ;;
    netboot=*)
        NETBOOT="${x#netboot=}"
        ;;
    ip=*)
        IPOPTS="${x#ip=}"
        ;;
    hwaddr=*)
        HWADDR="${x#hwaddr=}"
        ;;
    boot=*)               #指定本地还是网络启动
        BOOT=${x#boot=}
        ;;
    resume=*)
        RESUME="${x#resume=}"
        ;;
    resume_offset=*)
        resume_offset="${x#resume_offset=}"
        ;;
    noresume)
        noresume=y
        ;;
    panic=*)              #获取panic参数值
        panic="${x#panic=}"
        case ${panic} in
        *[![:digit:].]*)
            panic=
            ;;
        esac
        ;;
    quiet)
        quiet=y
        ;;
    ro)
        readonly=y
        ;;
    rw)
        readonly=n
        ;;
    debug)
        debug=y
        quiet=n
        exec >/dev/.initramfs/initramfs.debug 2>&1
        set -x
        ;;
    debug=*)
        debug=y
        quiet=n
        set -x    #开启shell的调试模式
        ;;
    break=*)
        break=${x#break=}
        ;;
    break)
        break=premount
        ;;
    blacklist=*)
        blacklist=${x#blacklist=}
        ;;
    netconsole=*)
        netconsole=${x#netconsole=}
        ;;
    esac
done

以上的位于for循环中的脚本的主要功能是获取kernel的命令行选项(/proc/cmdline )然后赋给相应的变量。其中最重要的是root,其它的可有可无。

if [ -z "${noresume}" ]; then
    export resume=${RESUME}
else
    export noresume
fi

若 NORESUME 变量不为空,则将 RESUME 变量的值赋给 resume 变 量,并把 resume 变量 export 出去,使得在子 shell 环境中也可以 使用 resume 变量。resume 变量将在 /scripts/local-premount/resume 脚本中使用。

[ -n "${netconsole}" ] && modprobe netconsole netconsole=${netconsole}
基于网络的终端控制,一般用不到。

maybe_break top    

参见上文中对break变量的解释,kernel中指定的break参数包含top时,maybe_break会查看panic变量是否为0,若否则以交互的方式调出 shell,此shell的输入输出使用已经创建好的节点 /dev/console。

# export BOOT variable value for compcache,
# so we know if we run from casper
export BOOT

# Don't do log messages here to avoid confusing usplash
run_scripts /scripts/init-top    

按照/scripts/init-top/ORDER文件的配置依次执行其下的脚本文件,这里最重要的是开启了udev daemon
udev 以 daemon 的方式启动 udevd,接着执行 udevtrigger 触发在机器启动前已 经接入系统的设备的 uevent,然后调用 udevsettle 等待,直到当 前 events 都被处理完毕。之后,如果 ROOTDELAY 变量不为空,就 sleep ROOTDELAY 秒以等待 usb/firewire disks 准备 好。  

maybe_break modules    
log_begin_msg "Loading essential drivers..."
load_modules
log_end_msg

load_modules按照/conf/modules文件的配置加载重要的module,由于使用了udev机制这一步其实是多余的,实际上/conf/modules文件并不存在。
maybe_break premount
[ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-premount"
run_scripts /scripts/init-premount
[ "$quiet" != "y" ] && log_end_msg
这段代码从字面上理解是为接下来挂载将要使用的系统的根目录所在的分区作准备,/scripts/init-premount实际并不存在,所以这一步其实啥都没干。

maybe_break mount                #这一步主要是执行/scripts/local中的mountroot函数,将realfs挂载到${rootmnt},在mountroot函数中最重要的是检测realfs的fytype
log_begin_msg "Mounting root file system..."
. /scripts/${BOOT}
parse_numeric ${ROOT}          #这一步主要用于解析lilo启动管理程序传递的参数,对于GRUB用不到。
mountroot
log_end_msg
这一部分的功能主要是把硬盘上的realfs挂载到${rootmnt}特别重要,大部分的系统启动问题都是这一部分存在问题引起的。其中很大一部分的原因是硬盘控制器的module
没有被udev或上面的load_modules加载,导致kernel不能读取硬盘中的数据。

下面我们来看一下 /scripts/local 中定义的 mountroot 函数是如何工作 的。  
    首先,它通 过 run_scripts 函数执行 /scripts/local-top 目录下所有具有可执行权限的文件。在这个目录下有3个文件:lvm,mdrun  和 udev_helper。  
    lvm  是逻辑卷管理方面的脚本,我没有过(估计一般pc很少有人会用),而且其中调用的具有可执行权限的文件在此 initrd.img  中也不存在。因为这个脚本在运行的时候会先检查需要的文件是否存在,若不存在则退出,所以这个脚本相当于什么也没做。略过。  
    mdrun  是 raid 方面的脚本。它要求 udev_helper 先被执行(见第136行代码的说明)其中用到的具有可执行权限的文件在 此 initrd.img 中不存在。这等效于这个脚本不起作用。  
    udev_helper  脚本 mdrun 的先决条件,根据实际情况 ide-generic 模块可能会被加 载。  
    在这三个脚本执行过之 后,mountroot 函数会查看 ROOT 设备节点是否已经存在,如果不存在将等 ${ROOTDELAY} 秒。若在这段时间内 ROOT 设备节点没有出现则调 用 panic 函数重启机器或是生一 个交互 shell。  
     若 ROOT 设备节点已经存在,则查看 ROOTFSTYPE 变量是否为空。若不空, 则 FSTYPE 变量的值就是 ${ROOTFSTYPE};否则通过 eval 用 fstype 命令得到 ROOT 的分区格式。其中,fstype 命令会输 出 FSTYPE=blabla 类型的字符串,它跟在 eval 后面就相当于作了  FSTYPE=blabla 这样的赋值操作。如果经过这一步之后 ROOTFSTYPE 的值是 "unknown"(包括通过在 kernel 后添 加 rootfstype=unknown 参数和 fstype 输出的  FSTYPE=unknown),则 mountroot 函数调用 /lib/udev/vol_id 得到 分区的格式。此时,FSTYPE 的值仍有可能是 "unknown"。 如果是这样的话,在最后的 mount 操作就会失败。或许你会觉得这里要判断分区格式是不是很麻烦。是的,确实如此。但是要知道这 里的 mount 不会自己判断分区格式,所以要在参数中指定。  
     在得到了 FSTYPE 之后,mountroot 函数调用 run_scripts 函数运 行 /scripts/local-premount 下面具有可执行权限的文件。  
     在 /scripts/local-premount 目录中只有一个具有可执行权限的脚本 resume。此脚本负责在 计算机休眠后恢复休眠前的状态。若 resume 变量为空或者这个变量所指的设备不存在,则直接退出;否则,运 行 /bin/resume 恢复状态。
     在这之后,mountroot 函数根据变量 readonly 确定是以只读还是读写的方式挂载,根 据 FSTYPE 变量加载适当得内核模块。在得到了所有必要的参数之后,通过 mount 命令将将要进入的 系统的根目录所在的分区挂载到 ${rootmnt} 目录下。  
     最后,mountroot 函数通过 run_scripts 函数执行 /scripts/local- bottom 下具有可执行权限的文件。由于在此目录下没有文件,所以这一步什么都没有做。  
      parse_numeric 函数 ( /scripts/functions 中定义)从它的注释中可以看出,这个是为了和 lilo 兼容而存 在的。由于现在一般用 grub 作为 bootloader,我们平常写的 root=/dev /hdxx,root=LABEL=xx...xx 或 root=UUID=x...x-...-xxx 的形式都会造 成此函数的直接返回,相当于什么都没有做。由于我没有用过 lilo,所以对于下面 lilo 的处理,我也不好说什 么。  
maybe_break bottom                 #/scripts/init-bottom中的主要文件是udev,这一步要停止udev服务并执行mount -n -o move /dev ${rootmnt}/dev
[ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-bottom"
run_scripts /scripts/init-bottom
[ "$quiet" != "y" ] && log_end_msg

这一部分的主要功能是按照/scripts/init-buttom/ORDER文件的配置依次执行其下的脚本文件,这里最重要的是关闭了udev daemon


# Move virtual filesystems over to the real filesystem
mount -n -o move /sys ${rootmnt}/sys
mount -n -o move /proc ${rootmnt}/proc

将开机过程中kernel产生的信息转移到硬盘上rootfs对应的目录中。

# Check init bootarg
if [ -n "${init}" ] && [ ! -x "${rootmnt}${init}" ]; then
    echo "Target filesystem doesn't have ${init}."
    init=
fi

查看init变量中指定的文件能否执行,一般是/sbin/init

# Search for valid init
if [ -z "${init}" ] ; then
    for init in /sbin/init /etc/init /bin/init /bin/sh; do
        if [ ! -x "${rootmnt}${init}" ]; then
            continue
        fi
        break
    done
fi

当init变量为空时(一般都是这样),查找可以找到并使用的init程序

# No init on rootmount
if [ ! -x "${rootmnt}${init}" ]; then
    panic "No init found. Try passing init= bootarg."
fi

找不到可以使用的init程序时,kernel panic。

# Confuses /etc/init.d/rc
if [ -n ${debug} ]; then
    unset debug
fi

因为在最终要使用的系统的 /etc/init.d/rc 中通过 debug 变量来显示要执行的一些命令, 其中 debug=echo 那一行是注释掉的。所以这里要 unset debug 变量,否则;/etc/init.d/rc 的执行会出问题。

# Chain to real filesystem
maybe_break init
exec run-init ${rootmnt} ${init} "$@" <${rootmnt}/dev/console >${rootmnt}/dev/console 2>&1
panic "Could not execute run-init."

这一段代码是这个 init 脚本的最后部分,把系统的启动交给了将要进入的系统的 ${init} (上面初始化为 "/sbin/init"),并用 /dev/console 作为输入与输出的设备。  
     那么这个 run-init (/bin/run-init) 究竟作了些什么。我们得到 klibc- utils 源码包并解开之后,run-init 的源码在 klibc-1.4.34/usr/kinit/run-init 目录下。这个程序要完成的功能的核 心在 run-init.c 的第88行,run_init(realroot, console, init, initargs)  (runinitlib.c 中定义)函数的调用。坐在这个函数中首先通过 chdir 调用将目录切换到 了 realroot。因为此时还没有改变根目录,所以 / 和 . 应该不是同一个目录。然后确 认 / 和 . 不在同一个文件系统上(注意,同样的分区格式,不同的分区,也是不同的文件系统)。接下来确定 存在 /init 文件,并且当前的根目录所在的文件系统类型是 ramfs 或 tmpfs。在这 一切都确定之后,通过 nuke_dir("/")  (runinitlib.c 中定义)调用删除当前根目录下除挂载点以外的内容,挂载点下有真正的rootfs,以释放它们所占用的内存。紧接着把当前目录,也就 是 realroot ,通过 mount 移动到根目录,并通过 chroot 函数 将根目录设为当前目录,这样硬盘上的根文件系统就挂载完毕了,再通过一个 chdir("/")  调用改变当前工作目录为根目录。现在,我们剩下的只是让 /sbin/init 跑起来。但在开始之前要得到 0, 1, 2 三个文件描述符,用来做我们 的 stdin, stdout 和 stderr。在得到这些之后就通 过 execv(init, initargs) 调用让我们的 /sbin/init 跑起来 了。  

小 结  
    好了,上面我已经说了这么 多。那么,init 脚本究竟都作了什么呢?  
     首先,建立一些必要的文件夹作为程序工作的时候需要的目录或者必要的挂载点,以及必需的设备节点。  
     然后,根据提供的参数建立适当的设备节点并加载适当的内核模块,启动适当的进程(udevd)帮助我们完成这一步骤。当没有使用udev机制时应在/conf/modules中指明要加载的驱动。同时要自己建好相关的设备节点。
     最后,在做完了这些乱七八糟的为挂载根目录及运行 /sbin/init 进程作准备的事情之后,调用 run- init 来运行 /sbin/init 从而启动我们的系统。  
     由以上可以看出Ubunt为了兼容各种不同的硬件配置将该init脚本写的非常繁琐,在实际应用中我们可以根据自己主机的情况定制该文件。
阅读(2204) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~