Chapter 6 : User Space Initialization

tags: gnitnaw twlkh

這章是講系統初始化(請看第5章)以後的事。從現在開始,系統進入所謂的『後初始化』(user space階段)。

6.1 Root File System

Kernel除了有映像檔(zImage,在RPi則為kernel.img)要載入外,如果你的系統很複雜,有些時候可能還要載入一些module(你總不能全部塞到zImage裡頭讓他爆),更多時候你還需要做一些自訂檔案輸入輸出(如程式操作或log file等)。所以檔案系統是很必要的。

根據Filesystem Hierarchy Standard(FHS),我們可以知道這些目錄結構的用途。以下以RPi為例:

pi@raspberrypi:~ $ tree -L 1 /
/
├── bin
├── boot
├── dev
├── etc
├── home
├── lib
├── lost+found
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var
目錄 描述
/ 根目錄
/bin 可供所有使用者執行的檔案。通常 Embedded linux 不可省略此部份。
/boot 放置啟動程式檔案(如zImagedevice tree binary等),在RPi時為獨立分割區塊。
/dev device node集中處。 Embedded linux 不可省略此部份。
/etc 系統設定檔。 通常Embedded linux 不可省略此部份。
/home 使用者的檔案所在目錄。
/lib 系統函式庫,包含C標準函式庫(libc)或動態連結器(ld)等。 Embedded linux 不可省略此部份,在一般迷你係統中其也佔了大部份容量。
/lost+found 沒法被fsck回復的檔案會被複製到此。
/media 為使用者隨身碟或CD之類的可移除裝置的掛載處,一般不需 super user 權限即可自動掛載。
/mnt 臨時掛載的檔案系統都掛在此目錄下。
/opt 沒有被包含在此distribution的第三方軟體可被安裝在此處(不然就是/usr/local)。
/proc 此目錄與其包含的檔案均為虛擬檔案系統(virtual filesystem),不佔任何硬碟空間,其資料(例如系統核心、行程資訊等)都是在記憶體當中。
/root super user的家目錄。
/run 用以放置系統開機後所產生的各項資訊。早期是放在/var/run。新版FHS則改到/run並可使用虛擬檔案系統以增進效能。
/sbin 必要的系統執行檔放置處,唯有super user才能使用這些執行檔去『改變』系統設定,一般使用者最多可用來『查詢』系統設定。
/srv 放置系統服務的資料或設定(一般不能給人看的資料還是放/var/lib)。
/sys /proc/dev有點像,使用虛擬檔案系統,記錄核心與系統硬體資訊相關資訊。我記得以前使用 device tree 去讀sensor的時候,也是讀取掛在這的虛擬檔案去取值。
/tmp 所有使用者暫時放置檔案的地方,所有使用者都有權限讀取。
/usr 放置可分享但不可變動的資料,如函式庫檔案、標頭檔或應用程式等。
/var 常態性變動的檔案,包括系統中其內容不斷變化的檔案,如記錄檔等。

:::info 本章關於busybox的部份會在Ch11詳細敘述。 :::

6.1.4 - 6.1.6 檔案系統的挑戰

要把能讓系統能完整運行的相關程式檔案全部塞進有限大小的flash memory其實沒想像容易。想像我們寫C程式去讀一個I2C介面的sensor好了,寫程式就要用到相關的標準函式庫,大部份標準函式庫又是靠系統呼叫去實作,系統呼叫也要建立在作業系統上,這些全都要塞到flash memory才能讓整個系統正常運作。不要以為現在Micro SD很便宜所以你隨便就有幾G可以用,現在毛利愈壓愈低的情況下你能省多少是多少,搞不好你能運用的儲存容量不到10MB。而現在Linux kernel愈做愈複雜,要能為了系統量身打造出必要的檔案系統也要花很多功夫。

一個很常用的方法是trial-and-Error (真的假的阿?) 。使用rpm或dpkg等套件管理亦可用於瞭解軟體或函式庫等之間的依賴關係(不過還是很麻煩),不過仍有其侷限性(很難有效地去除不必要的package)。最近比較流行的有用方法是使用buildroot或bitbake等工具去做對硬體的客製化檔案系統。

:::info 本章關於buildroot或bitbake的部份會在Ch16詳細敘述。 :::

6.2 Kernel's Last Boot Steps

延續Ch5最後,./init/main.ckernel_init最後的部份:

(...)
    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }    

(...)
if (execute_command) {
        ret = run_init_process(execute_command);
        if (!ret)
            return 0;
        panic("Requested init %s failed (error %d).",        // 大家都不想看到的kernel panic
            execute_command, ret);
    }
    if (!try_to_run_init_process("/sbin/init") ||            // 在RPi,/sbin/init被link到/lib/systemd/systemd
        !try_to_run_init_process("/etc/init") ||
        !try_to_run_init_process("/bin/init") ||
        !try_to_run_init_process("/bin/sh"))
        return 0;

    panic("No working init found.  Try passing init= option to kernel. "
          "See Linux Documentation/init.txt for guidance.");

前面說過,try_to_run_init_processrun_init_process只是個封裝函式,用意是把引入的檔案名稱用execve()執行。如果kernel_init在命令列沒有收到關於 ramdisk_execute_command 的設定,然後那四個 try_to_run_init_process 找到一個可以run成功的執行檔,那個檔案就會被執行並取代原本這個kernel_init process(PID不變)。如果這四個地方都找不到那隻好接panic了(因為./init/main.c內的函式只能被執行一次,失敗了不能重來,除非重開機)。

當然,為了要執行被try_to_run_init_process引入的可執行檔,執行kernel_init前要先把檔案系統掛載上去才行,不然kernel根本找不到/sbin/init或是此執行檔依賴的函式庫。問題就出在這了,有時候由於受限於硬體驅動程式(例如SATA硬碟,其驅動程式一般放在/lib/modules而不在Image裡面),在執行kernel_init前根本沒法去掛載儲存裝置,既然還沒有掛載那你上哪找/sbin/init?臨時虛擬檔案系統(Initial RAM Disk/initrd 或 Initial RAM Filesystem/initramfs)就是為瞭解決這個問題:先把必要的檔案載入到記憶體中並虛擬成檔案系統讓系統可以執行,之後再釋放以及掛上真的檔案系統。

6.3 The init Process

這節講的System V runlevel在目前大多數的Linux系統(包括RPi)上已經被systemd取代。在RPi可以看到/sbin/init被link到/lib/systemd/systemdkernel_initexecve去執行/lib/systemd/systemd後,systemd就會取代這個kernel_init並成為第1個user space產生的process(PID=1)。

systemd 取消了以前的 runlevel 概念改用target(還是有點相容啦)。這邊不多說看鳥哥比較快。

6.4 Initial RAM Disk (initrd)

initrd是用一部份記憶體模擬成一塊磁碟(就實際意義上它是一個真實的block device)然後把該『磁碟』掛到root上,等初始化差不多結束可以掛真的root的時候再卸載。不過既然是模擬成真實磁碟,那理所當然要有一個檔案系統(例如ext2),也不能隨便改變大小;然後要使用這塊『磁碟』內的東西時,就要將其中的檔案載入到記憶體中... 明明就已經在記憶體的東西還要這樣幹的話就有點像脫褲子放屁

要使用initrd/initramfs記得把kernel相關選項打開,如圖:

6.4.1 - 6.4.4 initrd的流程

  • 開發者需將initrd的(壓縮過的)映像檔準備好,裡麵包含必要的函式庫與執行檔。
  • bootloader負責將initrd映像檔載入到記憶體中,放在/initrd.image
    • 如果bootloader將kernel與initrd映像檔分別載入,那bootloader必須將initrd在記憶體的位址傳給kernel(可用command line等方式)。
    • 早前有些ARM系統會把kernel與initrd映像檔串起來一起載入,然後使用command line將initrd在記憶體的位址傳給kernel。不過此方式已被initramfs取代。
  • bootloader將自己的工作結束,交接給bootstrap loader
  • bootstrap loader將自己的工作結束,交接給kernel(start_kernel),然後一直執行到rest_init
  • rest_init分出一個kthread執行kernel_init,然後首先呼叫kernel_init_freeable()
  • 由於command line等方式告知kernel要載入initrdkernel_init_freeable便呼叫prepare_namespace()函式,
  • prepare_namespace()呼叫initrd_load(),將/initrd.image解壓縮後的內容塞到/dev/ram0,然後卸載/initrd.image
  • initrd_load()呼叫handle_initrd(),把/dev/ram(0)掛上根目錄/,隨後執行initrd上的/linuxrc,此檔案通常為一script,會把必要的module(例如硬碟相關)的modules先行從/dev/ram(0)載入。
  • /linuxrc執行完畢,/被卸載,/dev/ram0的空間也會被釋放掉。
  • initrd_load()執行完回到prepare_namespace(),mount真正的/
  • 跑完繼續kernel_init_freeable()然後回到kernel_init,根據6.2的步驟可以快快樂樂執行/sbin/init

:::info 命令列(command line)設定中,若是加入root=/dev/path的設定,會使得以上步驟有如下改變:

  • 執行./linuxrc的步驟會被省略。
  • kernel不會卸載這個本來應該是臨時的根目錄,他會一直維持在那邊。當然之後的prepare_namespace()kernel_init_freeable()kernel_init會繼續跑下去。

RPi就是使用這樣的設定(在/boot/cmdline.txt中有root=/dev/mmcblk0p2),System.map中也有bcm2835_mmc_driver的資訊,猜測在kernel_init階段(do_basic_setup())kernel已經搞定了bcm2835_mmc_driver所以可以直接掛載/dev/mmcblk0p2不需特別準備initrd(也就是說一開始本來只是要暫時的假檔案系統但是很乾脆直接給真的)。 :::

:::info 根據jserv的文章:『kernel 永遠保留 PID=1 作為 init process 識別,而 /linuxrc執行的 PID 必非為 1』,關於如何執行/linuxrc的函式在init/do_mounts_initrd.c,這邊說明一下:

  • 在kernel 3.6(包含之前),執行/linuxrc是使用kernel_thread去新增一個thread去執行(使用kernel_execve)的 => 以此方式分出的process,其PPID為呼叫kernel_thread產生此process的PID,以本例來說就是PPID=1。
  • 從kernel 3.7開始,改用work queue(kworker)去執行 => 以此方式分出的process,其PPID為kthreadd(kernel_init第1個分出的process為init,第2個則為kthreadd)的PID,以本例來說就是PPID=2。
  • 不管是哪個版本,執行/linuxrc的process的PID都不會是1。 :::

6.5 initramfs

initramfs做的事跟initrd很像,但他們有本質上的不同: |項目|initrd|initramfs| |---|--------|-----------| |載入/設定的時機|prepare_namespace()|do_basic_setup()(較initrd更為提前)| |Image格式|壓縮過的(gzipped)檔案系統(如ext2)|cpio+gzip,使用上較方便,亦可顧及檔案權限(限root)| |Image製作|跟kernel Image分開(也可串在一起)|被包在kernel image裡面| |作法|ram disk(block device)|tmpfs(將cache掛載像個檔案系統,並把initramfs放在cache,需要的話直接從cache抓出來執行)| |先執行的程式|/linuxrc|/init(若是ramdisk_execute_command沒有發現有另外指定檔案)|

要編譯initramfs的話是在./usr這個目錄裡面去做:

~/git/raspberry_kernel/linux/usr$ ls -l
total 60
-rw-rw-r-- 1 chen chen   988 Nov 18 20:27 built-in.o
-rwxrwxr-x 1 chen chen 19328 Nov 18 20:27 gen_init_cpio
-rw-rw-r-- 1 chen chen 13029 Sep 19 15:10 gen_init_cpio.c
-rw-rw-r-- 1 chen chen   134 Nov 18 20:27 initramfs_data.cpio.gz
-rw-rw-r-- 1 chen chen   916 Nov 18 20:27 initramfs_data.o
-rw-rw-r-- 1 chen chen  1307 Sep 19 15:10 initramfs_data.S
-rw-rw-r-- 1 chen chen  2978 Sep 19 15:10 Kconfig
-rw-rw-r-- 1 chen chen  2358 Sep 19 15:10 Makefile

編譯腳本則是放在./scripts/gen_initramfs_list.sh,預設的腳本為:

default_initramfs() {
        cat <<-EOF >> ${output}
                # This is a very simple, default initramfs

                dir /dev 0755 0 0    # 新增/dev 目錄,其權限為755,used-id/group-id 為0 (root)
                nod /dev/console 0600 0 0 c 5 1 #新增一node /dev/console,權限為0600,為character device,major=5,minor = 1
                dir /root 0700 0 0
                # file /kinit usr/kinit/kinit 0755 0 0
                # slink /init kinit 0755 0 0
        EOF
}

若是使用initramfs,一般來說第一個執行的程式會是/init。但若是在命令列設定rdinit=相關指令去設定程式,就會改為執行該指定程式。

6.6 Shutdown

雖然關機程序一般來說不是那麼重要,針對shutdown、halt跟reboot還是要有不同的策略。 通常shutdown時要先發一個signal給所有process告知即將shutdown,讓他們有時間結束手上的工作,然後他們會發回SIGKILL表示已經終止。然後shutdown可以卸載磁碟然後進行指定的halt或reboot程序。

有一點要注意的是,若是使用ext2檔案系統,如果遇到不正常關機,那重新開機後系統可能會花上幾小時在fsck上。

參考資料