天天看點

Ubuntu探秘之四:kernel和initrd

這個筆記是我以前發表在學校BBS(電子科大清水河畔)上的,同學們反應還不錯,特拿到這裡與大家分享。

這一次探秘我們主要研究一下kernel和initrd,重點是後者。

kernel是什麼以及有哪些功能相信大家都很清楚,我就不多說了。這裡主要介紹ubuntu10.04上安裝的相關軟體包、檔案、目錄以及kernel的配置方法。

至于initrd檔案是幹什麼用的呢?

大家隻要做一個簡單的實驗就可以明白了:在開機的GRUB指令行上去掉initrd這一行,然後按CTRL+x啟動,過會你會發現螢幕出現“kernel panic”的提示。

為什麼會這樣呢?這是因為為了減小kernle檔案(vmlinuz)的大小和增強靈活性,現在的發行版如ubuntu預設将硬碟接口卡如IDE,STAT,SCSI的驅動程式以子產品(modules)的形式

放在initrd檔案中,GRUB中去掉initrd這一行意味着initrd中的modules沒有被加載kernel就不能識别該硬碟更别提挂載上面的根檔案系統了。

initrd檔案的功能主要有兩個:

1、提供開機必需的但kernel檔案(即vmlinuz)沒有提供的驅動子產品(modules)

2、負責加載硬碟上的根檔案系統并執行其中的/sbin/init程式進而将開機過程持續下去

GRUB 将kernle加載到記憶體并執行,kernel在運作的後期會讀取并執行initrd檔案中的init腳本檔案并按照其中指令逐行執行,是以要掌握 initrd的作用和硬碟上的根檔案系統加載過程,全面解析init檔案是很重要的。在本文的第二部分,我會将/boot/initrd檔案打開,逐行解 讀init檔案.

一、kernel相關軟體包和配置檔案

1、下面我簡要介紹以下ubuntu 10.04上安裝的與kernle相關的軟體包及檔案:

[email protected]:~$ dpkg -l linux*

Desired=Unknown/Install/Remove/Purge/Hold

| Status=Not/Inst/Cfg-files/Unpacked/Failed-cfg/Half-inst/trig-aWait/Trig-pend

|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)

||/ Name           Version        Description

+++-==============-==============-============================================

ii  linux-firmware 1.34.1         Firmware for Linux kernel drivers

ii  linux-headers- 2.6.32-22.36   Header files related to Linux kernel version

ii  linux-headers- 2.6.32-22.36   Linux kernel headers for version 2.6.32 on x

ii  linux-image-2. 2.6.32-22.36   Linux kernel image for version 2.6.32 on x86

ii  linux-libc-dev 2.6.32-23.37   Linux Kernel Headers for development

。。。。。

可以看出系統安裝的與kernel相關的軟體包挺多的,但主要有三種類型:

linux-firmware      kernel提供的固件程式,位于/lib/firmware目錄下。作業系統用驅動程式驅動硬體,而驅動程式就是和硬體上的固件(如PCI裝置上的PCI BIOS)進行互動以控制對應硬體的,是以固件是驅動程式與硬體的執行機構間的橋梁。

linux-headers     與 特定linux kernel版本相關的頭檔案,位于/usr/src/linux-headers-2.6.32-22/include和/usr/include目錄 下,在編譯一些modules和軟體如virtualbox additional tools 時使用。

linux-image       linux kernel的二進制壓縮檔案即/boot/vmlinu檔案,此檔案就是大名鼎鼎的linux核心檔案!

linux-generi c   主要提供相應版本linux kernel的metadata即更新檔案

2、讓我們重點看一下系統安裝的linux-image軟體包的功能和内容:

g[email protected]:~$ dpkg -s  linux-image-2.6.32-22-generic

Package: linux-image-2.6.32-22-generic

Status: install ok installed

Priority: optional

Section: admin

Installed-Size: 94652

Maintainer: Ubuntu Kernel Team <[email protected] >

Architecture: i386

Source: linux

Version: 2.6.32-22.36

Provides: fuse-module, ivtv-modules, kvm-api-4, linux-image, linux-image-2.6, ndiswrapper-modules-1.9, redhat-cluster-modules

Depends: initramfs-tools (>= 0.36ubuntu6), coreutils | fileutils (>= 4.0), module-init-tools (>= 3.3-pre11-4ubuntu3), wireless-crda

Pre-Depends: dpkg (>= 1.10.24)                    #注意這些依賴的軟體包的作用是和kernel啟動時需要的檔案密切相關的。

Recommends: grub-pc | grub | lilo (>= 19.1)

Suggests: fdutils, linux-doc-2.6.32 | linux-source-2.6.32, linux-tools

Breaks: lvm2 (<< 2.02.54-1ubuntu3)

Conflicts: hotplug (<< 0.0.20040105-1)

Description: Linux kernel image for version 2.6.32 on x86/x86_64

This package contains the Linux kernel image for version 2.6.32 on

x86/x86_64.

.

Also includes the corresponding System.map file, the modules built by the  

packager, and scripts that try to ensure that the system is not left in an

unbootable state after an update.                  

.

Supports Generic processors.

.

Geared toward desktop systems.

.

You likely do not want to install this package directly. Instead, install

the linux-generic meta-package, which will ensure that upgrades work

correctly, and that supporting packages are also installed.

[email protected]:~$

g[email protected]:~$ dpkg -L  linux-image-2.6.32-22-generic

/.

/boot                             #這就是系統啟動目錄,其中多是與kernel相關的檔案

/boot/vmlinuz-2.6.32-22-generic

/boot/config-2.6.32-22-generic

/boot/abi-2.6.32-22-generic

/boot/System.map-2.6.32-22-generic

/boot/vmcoreinfo-2.6.32-22-generic

/lib

/lib/modules                      #這就是kernel提供的module所在的目錄,在開機過程中和系統運作過程中經常使用。注意module和特定版本的kernel是對應的。

/lib/modules/2.6.32-22-generic

/lib/modules/2.6.32-22-generic/kernel

/lib/modules/2.6.32-22-generic/kernel/arch

/lib/modules/2.6.32-22-generic/kernel/arch/x86/kvm/kvm.ko

…..

/lib/firmware /2.6.32-22-generic/mts_gsm.fw   #kernel提供的固件程式

/lib/firmware/2.6.32-22-generic/yam/9600.bin

/usr

/usr/share

/usr/share/doc

/usr/share/doc/linux-image-2.6.32-22-generic

/usr/share/doc/linux-image-2.6.32-22-generic/changelog.Debian.old.gz

/usr/share/doc/linux-image-2.6.32-22-generic/copyright

/usr/share/doc/linux-image-2.6.32-22-generic/changelog.Debian.gz

3、好了,我們現在知道了/boot目錄下除grub子目錄外的所有檔案都是由linux-image軟體包提供的,那麼這些檔案的作用是什麼呢 ?

[email protected]:~$ ls /boot                #kernel相關檔案

abi-2.6.32-22-generic         memtest86+.bin

config-2.6.32-22-generic      System.map-2.6.32-22-generic

grub                          vmcoreinfo-2.6.32-22-generic

initrd.img-2.6.32-22-generic  vmlinuz-2.6.32-22-generic

[email protected]:~$

各檔案功能如下:

config-2.6.32-22-generic       核心參數配置記錄檔案,這個檔案在重新編譯配置kernel 時非常有用。

System.map-2.6.32-22-generic   核心對象清單檔案,核心對象是核心提供的常量及函數的符号連結

vmcoreinfo-2.6.32-22-generic   核心核心參數設定記錄檔案

initrd.img-2.6.32-22-generic   intrd檔案,注意這個和系統的kernel版本是對應的,一般開機時必須在GRUB中指定

vmlinuz-2.6.32-22-generic       核心檔案,這個和上個檔案即為在GRUB指令行上指定的檔案

[email protected]:~$ uname -a  #檢視系統kernel資訊指令

Linux geekard-laptop 2.6.32-22-generic #36-Ubuntu SMP Thu Jun 3 22:02:19 UTC 2010 i686 GNU/Linux

[email protected]:~$

顯示的含義 為:主機名為geekard-laptop、硬體平台為i686的筆記本電腦安裝了GNU/Linux 類型的作業系統,其kernel是在SMP Thu Jun 3 22:02:19 UTC 2010第36次編譯的,版本為版本為2.6.32-22-generic

另外在系統運作的過程中可以檢視和編輯/proc/sys/kernel/下的相應檔案 ,實時擷取和設定 kernel的參數

[email protected]:~$ ls /etc/kernel   #更新核心過程中用到的配置腳本

header_postinst.d  postinst.d  prerm.d

[email protected]:~$

4、核心的安裝方式:

    (1)、從配置并編譯好的rpm或deb軟體包安裝。

    (2)、從源碼包安裝,需手動配置。建議進階使用者使用,配置不當會造成系統不能正常啟動。

從源代碼安裝的步驟:(以下内容摘自kernel源代碼的README檔案)

1、INSTALLING the kernel source:

- If you install the full sources, put the kernel tarball in a

   directory where you have permissions (eg. your home directory) and

   unpack it:

        gzip -cd linux-2.6.XX.tar.gz | tar xvf -

   or

        bzip2 -dc linux-2.6.XX.tar.bz2 | tar xvf -

   Replace "XX" with the version number of the latest kernel.

2、Make sure you have no stale .o files and dependencies lying around:

        cd linux

        make mrproper

   You should now have the sources correctly installed.

3、CONFIGURING the kernel:

        "make config"      Plain text interface.

    "make menuconfig"  Text based color menus, radiolists & dialogs.

    "make xconfig"     X windows (Qt) based configuration tool.

    "make gconfig"     X windows (Gtk) based configuration tool.

    "make oldconfig"   Default all questions based on the contents of

               your existing ./.config file and asking about

               new config symbols.

在配置kernel時最重要的是确定kernel對晶片組、總線、檔案系統的支援。

4、 Do a "make" to create a compressed kernel image. It is also  possible to do "make install" if you have lilo installed to suit the

   kernel makefiles, but you may want to check your particular lilo setup first.

   COMPILING the kernel: If you configured any of the parts of the kernel as `modules', you  will also have to do "make modules_install".

5、配置grub.cfg檔案,在其中添加新配置的kernel及initrd檔案作為啟動項

6、Reboot with the new kernel and enjoy.

第二部分: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、系統上安裝的相關軟體包

[email protected]:~$ 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

[email protected]:~$

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 的制作:  

     首先在一個目錄中建立必要的檔案及目錄。例如:  

        [email protected]:/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 包:  

        [email protected]:/home/linux_src/initrd/debian_etch/initrd$ find . | cpio -o -H newc | gzip -9 > ../initrd.img.gz  

        20500 blocks  

        [email protected]:/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 檔案,我們現在要把它解開,過程如下:  

        [email protected]:/home/linux_src/initrd/debian_etch$ mkdir tmp  

        [email protected]:/home/linux_src/initrd/debian_etch$ cd tmp  

        [email protected]:/home/linux_src/initrd/debian_etch/tmp$ gzip -dc ../initrd.img-2.6.18-4-686 | cpio -idm  

        20500 blocks  

        [email protected]:/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 '/<quiet/>' /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腳本寫的非常繁瑣,在實際應用中我們可以根據自己主機的情況定制該檔案。

定制initrd檔案

既然我們已經知道了 initrd.img 到底要做什麼,我們現在就來一個精簡的 init 腳 本。  

    把 幾乎所有的過程都放到一 個腳本當中,仍掉了 nfs 啟動的内容,仍掉了從休眠中喚醒的功能,根據需要舍棄了一些檔案和檔案夾的建立,以及一些變量。這樣我 們的腳本隻有本地啟動的内容,結構更加緊湊,操作過程可能會更加清楚。這個也難說,具體和個人有關。不要 udev, 雖然很實用。 因為我們下邊的這個腳本是個原理性的示範。由于沒有了 udev, /sys 目錄就沒有必要了,同時我們還得自己照顧設 備節點。對于我這裡的情況,要手動建立 /dev/hda1, /dev/hda2 和 /dev /hda5 這三個裝置節點,其中 hda1 是主分區,它挂載到根目錄,hda2 是擴充分 區,hda5 是 swap。如果裝置節點建立少了,啟動的時候就會失敗。現在我這裡的情況是比較簡單的,但是如果通過改變啟動參數 使用移動儲存設備啟動呢?是以 udev 是一個很有用的東西,同時對于移動的裝置你不知道确切的 /dev /sdaxx 這樣的形式,UUID 就變得很重要了。

下面是我寫的精簡版本init。

#!/bin/sh

#Modified by : geekard [email protected]

#Update:2010.8.7

#Version:1

#本init腳本未使用udev ,相關modules和裝置檔案需要手動建立

#建立後面将用的目錄結構,用于存放檔案和作為挂載點

[ -d /dev ] || mkdir -m 0755 /dev

[ -d /root ] || mkdir -m 0700 /root

#由于不使用udev機制,故/sys目錄可以省略,但/proc必須要有。

#[ -d /sys ] || mkdir /sys

[ -d /proc ] || mkdir /proc

[ -d /tmp ] || mkdir /tmp

mkdir -p /var/lock

mkdir /dev/.initramfs  #用于debug

mount -t proc -o nodev,noexec,nosuid none /proc   #用于後面的cmdline的參數提取。

echo "Loading, please wait..."

#挂載tmpfs檔案系統并建立将使用的裝置檔案,若使用udev機制,則不需要建立這些裝置檔案,相應的指令為:mount -t devtmpfs none /dev  .

mount -t tmpfs none /dev

mknod -m 0600 /dev/console c 5 1

mknod /dev/null c 1 3

mknod /dev/sda b 8 0

mknod  /dev/sda1 b 8 1  #我的swap的分區節點

mknod  /dev/sda3 b 8 3 #root

mknod  /dev/sda5 b 8 5  #home

# Set modprobe env

export MODPROBE_OPTIONS="-qb"

# Export relevant variables

export ROOT=             #realfs裝置檔案路徑,在kernel指令行中指定,如:root=/dev/sda3,由于本init沒有使用udev,故UUID不能使用,隻能用sd*和hd*的形式。

export break=

export init=/sbin/init   #realfs中的第一個執行的init位置

export rootmnt=/root     #realfs的臨時挂載點,這裡的/指rootfs

export debug=

export BUSYBOX=y

#将一些函數包括進來,主要是一些輸出函數

. /scripts/functions

# Parse command line options

for x in $(cat /proc/cmdline); do

    case $x in

    root=*)

        ROOT=${x#root=}

        ;;

    debug)

        debug=y

        quiet=n

        exec >/dev/.initramfs/initramfs.debug 2>&1

        set -x

        ;;

    break=*)

        break=${x#break=}

        ;;

    break)

        break=premount

        ;;

    esac

done

# export BOOT variable value for compcache,

# so we know if we run from casper

export BOOT=local

#加載/conf/modules中指定的modules,由于沒有啟用udev,kernel找到的硬體的module不會自動加載,是以應在modules檔案中

#指明在mount root fs 前要加載的重要modules如ata,scsi controler的modules(開機後用lspci -v獲得),vfat,fat,ntfs等的modules.

maybe_break modules

log_begin_msg "Loading essential drivers..."

load_modules

log_end_msg

#這一部分很重要,因為沒有啟動udev機制,重要的module需要自己在/conf/modules中指定。實際上隻需要一個module即自己的硬碟接口module,如我用的STAT接口對應的module是ahci

#以下的FSTYPE判别語句可以省略,因為常見的ext2|3|4,核心支援。

maybe_break mount

log_begin_msg "Mounting root file system..."

eval $(fstype < ${ROOT})

modprobe -q ${FSTYPE}

roflag=-w  

mount ${roflag} -t ${FSTYPE} ${ROOT} ${rootmnt}  

log_end_msg  

# Move virtual filesystems over to the real filesystem

mount -n -o move /proc ${rootmnt}/proc

# Confuses /etc/init.d/rc

if [ -n ${debug} ]; then

    unset debug

fi

# Chain to real filesystem

#run-init的作用是:先将/中除 ${rootmnt}的内容删除,然後調用mount -o move  ${init} / 即将realfs的内容移動到/,再

# exec chroot / /sbin/init  即将目前/指向硬碟中的realfs并用exec指令執行realfs中的/sbin/init,這将替換掉目前執行的位于initrd中的init

maybe_break init

exec run-init ${rootmnt} ${init} "$@" <${rootmnt}/dev/console >${rootmnt}/dev/console 2>&1

panic "Could not execute run-init."

PS:轉載請注明本作者和出處!!!

繼續閱讀