1. 程式人生 > >Linux程序的管理與排程(六) -- Linux下1號程序的前世(kernel_init)今生(init程序)

Linux程序的管理與排程(六) -- Linux下1號程序的前世(kernel_init)今生(init程序)

Linux下有3個特殊的程序,idle程序(PID=0), init程序(PID=1)和kthreadd(PID=2)


* idle程序由系統自動建立, 執行在核心態

idle程序其pid=0,其前身是系統建立的第一個程序,也是唯一一個沒有通過fork或者kernel_thread產生的程序。完成載入系統後,演變為程序排程、交換


* init程序由idle通過kernel_thread建立,在核心空間完成初始化後, 載入init程式, 並終端使用者空間

由0程序建立,完成系統的初始化. 是系統中所有其它使用者程序的祖先程序
Linux中的所有程序都是有init程序建立並執行的。首先Linux核心啟動,然後在使用者空間中啟動init程序,再啟動其他系統程序。在系統啟動完成完成後,init將變為守護程序監視系統其他程序。


* kthreadd程序由idle通過kernel_thread建立,並始終執行在核心空間, 負責所有核心執行緒的排程和管理

它的任務就是管理和排程其他核心執行緒kernel_thread, 會迴圈執行一個kthread的函式,該函式的作用就是執行kthread_create_list全域性連結串列中維護的kthread, 當我們呼叫kernel_thread建立的核心執行緒會被加入到此連結串列中,因此所有的核心執行緒都是直接或者間接的以kthreadd為父程序

我們下面就詳解分析1號程序的前世(kernel_init)今生(init程序)

Linux系統中的init程序(pid=1)是除了idle程序(pid=0,也就是init_task)之外另一個比較特殊的程序,它是Linux核心開始建立起程序概念時第一個通過kernel_thread產生的程序,其開始在核心態執行,然後通過一個系統呼叫,開始執行使用者空間的/sbin/init程式,期間Linux核心也經歷了從核心態到使用者態的特權級轉變,/sbin/init極有可能產生出了shell,然後所有的使用者程序都有該程序派生出來

1號程序

前面我們瞭解到了0號程序是系統所有程序的先祖, 它的程序描述符init_task是核心靜態建立的, 而它在進行初始化的時候, 通過kernel_thread的方式建立了兩個核心執行緒,分別是kernel_init和kthreadd,其中kernel_init程序號為1

start_kernel在其最後一個函式rest_init的呼叫中,會通過kernel_thread來生成一個核心程序,後者則會在新程序環境下調 用kernel_init函式,kernel_init一個讓人感興趣的地方在於它會呼叫run_init_process來執行根檔案系統下的 /sbin/init等程式:

kernel_init

0號程序建立1號程序的方式如下

kernel_thread(kernel_init, NULL, CLONE_FS);

我們發現1號程序的執行函式就是kernel_init, 這個函式被定義init/main.c中,如下所示

kernel_init函式將完成裝置驅動程式的初始化,並呼叫init_post函式啟動使用者空間的init程序。

由0號程序建立1號程序(核心態),1號核心執行緒負責執行核心的部分初始化工作及進行系統配置,並建立若干個用於快取記憶體和虛擬主存管理的核心執行緒。

init程序

隨後,1號程序呼叫do_execve執行可執行程式init,並演變成使用者態1號程序,即init程序。

init程序是linux核心啟動的第一個使用者級程序。init有許多很重要的任務,比如像啟動getty(用於使用者登入)、實現執行級別、以及處理孤立程序。

它按照配置檔案/etc/initab的要求,完成系統啟動工作,建立編號為1號、2號…的若干終端註冊程序getty。

每個getty程序設定其程序組標識號,並監視配置到系統終端的介面線路。當檢測到來自終端的連線訊號時,getty程序將通過函式do_execve()執行註冊程式login,此時使用者就可輸入註冊名和密碼進入登入過程,如果成功,由login程式再通過函式execv()執行shell,該shell程序接收getty程序的pid,取代原來的getty程序。再由shell直接或間接地產生其他程序。

上述過程可描述為:0號程序->1號核心程序->1號使用者程序(init程序)->getty程序->shell程序

注意,上述過程描述中提到:1號核心程序呼叫執行init函式並演變成1號使用者態程序(init程序),這裡前者是init是函式,後者是程序。兩者容易混淆,區別如下:

  1. kernel_init函式在核心態執行,是核心程式碼

  2. init程序是核心啟動並執行的第一個使用者程序,執行在使用者態下。

  3. 一號核心程序呼叫execve()從檔案/etc/inittab中載入可執行程式init並執行,這個過程並沒有使用呼叫do_fork(),因此兩個程序都是1號程序。

當核心啟動了自己之後(已被裝入記憶體、已經開始執行、已經初始化了所有的裝置驅動程式和資料結構等等),通過啟動使用者級程式init來完成引導程序的核心部分。因此,init總是第一個程序(它的程序號總是1)。

當init開始執行,它通過執行一些管理任務來結束引導程序,例如檢查檔案系統、清理/tmp、啟動各種服務以及為每個終端和虛擬控制檯啟動getty,在這些地方使用者將登入系統。

在系統完全起來之後,init為每個使用者已退出的終端重啟getty(這樣下一個使用者就可以登入)。init同樣也收集孤立的程序:當一個程序啟動了一個子程序並且在子程序之前終止了,這個子程序立刻成為init的子程序。對於各種技術方面的原因來說這是很重要的,知道這些也是有好處的,因為這便於理解程序列表和程序樹圖。init的變種很少。絕大多數Linux發行版本使用sysinit(由Miguel van Smoorenburg著),它是基於System V的init設計。UNIX的BSD版本有一個不同的init。最主要的不同在於執行級別:System V有而BSD沒有(至少是傳統上說)。這種區別並不是主要的。在此我們僅討論sysvinit。 配置init以啟動getty:/etc/inittab檔案

關於init程式

1號程序通過execve執行init程式來進入使用者空間,成為init程序,那麼這個init在哪裡呢

核心在幾個位置上來查尋init,這幾個位置以前常用來放置init,但是init的最適當的位置(在Linux系統上)是/sbin/init。如果核心沒有找到init,它就會試著執行/bin/sh,如果還是失敗了,那麼系統的啟動就宣告失敗了。

因此init程式是一個可以又使用者編寫的程序, 如果希望看init程式原始碼的朋友,可以參見

init包 說明 學習連結
sysvinit 早期一些版本使用的初始化程序工具, 目前在逐漸淡出linux歷史舞臺, sysvinit 就是 system V 風格的 init 系統,顧名思義,它源於 System V 系列 UNIX。它提供了比 BSD 風格 init 系統更高的靈活性。是已經風行了幾十年的 UNIX init 系統,一直被各類 Linux 發行版所採用。
systemd Systemd 是 Linux 系統中最新的初始化系統(init),它主要的設計目標是克服 sysvinit 固有的缺點,提高系統的啟動速度

Ubuntu等使用deb包的系統可以通過dpkg -S檢視程式所在的包

這裡寫圖片描述

CentOS等使用rpm包的系統可以通過rpm -qf檢視系統程式所在的包

舊版CentOS

新版CentOS

附錄

kernel_init_freeable流程分析

static noinline void __init kernel_init_freeable(void)
{
    /*
     * Wait until kthreadd is all set-up.
     */
    wait_for_completion(&kthreadd_done);

    /* Now the scheduler is fully set up and can do blocking allocations */
    gfp_allowed_mask = __GFP_BITS_MASK;

    /*
     * init can allocate pages on any node
     */
    set_mems_allowed(node_states[N_MEMORY]);
    /*
     * init can run on any cpu.
     */
    set_cpus_allowed_ptr(current, cpu_all_mask);

    cad_pid = task_pid(current);

    smp_prepare_cpus(setup_max_cpus);

    do_pre_smp_initcalls();
    lockup_detector_init();

    smp_init();
    sched_init_smp();

    page_alloc_init_late();

    do_basic_setup();

    /* Open the /dev/console on the rootfs, this should never fail */
    if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
            pr_err("Warning: unable to open an initial console.\n");

    (void) sys_dup(0);
    (void) sys_dup(0);
    /*
     * check if there is an early userspace init.  If yes, let it do all
     * the work
     */

    if (!ramdisk_execute_command)
            ramdisk_execute_command = "/init";

    if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
            ramdisk_execute_command = NULL;
            prepare_namespace();
    }

    /*
     * Ok, we have completed the initial bootup, and
     * we're essentially up and running. Get rid of the
     * initmem segments and start the user-mode stuff..
     *
     * rootfs is available now, try loading the public keys
     * and default modules
     */

    integrity_load_keys();
    load_default_modules();
 }
執行流程 說明
wait_for_completion 例項在kernel/sched/completion.c中, 等待Kernel Thread kthreadd (PID=2)建立完畢
gfp_allowed_mask __GFP_BITS_MASK;設定bitmask, 使得init程序可以使用PM並且允許I/O阻塞操作
set_mems_allowed(node_states[N_MEMORY]); init程序可以分配物理頁面
set_cpus_allowed_ptr 通過設定cpu_bit_mask, 可以限定task只能在特定的處理器上執行, 而initcurrent程序此時必然是init程序,設定其cpu_all_mask即使得init程序可以在任意的cpu上執行
task_pid 設定到目前執行程序init的pid號給cad_pid(cad_pid是用來接收ctrl-alt-del reboot signal的程序, 如果設定C_A_D=1就表示可以處理來自ctl-alt-del的動作), 最後會呼叫 ctrl_alt_del(void)並確認C_A_D是否為1,確認完成後將執行cad_work=deferred_cad,執行kernel_restart
smp_prepare_cpus 體系結構相關的函式,例項在arch/arm/kernel/smp.c中,呼叫smp_prepare_cpus時,會以全域性變數setup_max_cpus為函式引數max_cpus,以表示在編譯核心時,設定支援的最大CPU數量
do_pre_smp_initcalls 例項在init/main.c中, 會透過函式do_one_initcall,執行Symbol中 __initcall_start與__early_initcall_end之間的函式
smp_init 例項在kernel/smp.c中, 函式主要是由Bootstrap處理器,進行Active多核心架構下其它的處理器. 如果發生Online的處理器個數(from num_online_cpus)超過在核心編譯時,所設定的最大處理器個數 setup_max_cpus (from NR_CPUS),就會終止流程.如果該處理器目前屬於Present (也就是存在系統中),但尚未是Online的狀態,就會呼叫函式cpu_up(in kernel/cpu.c)來啟動該處理器.
sched_init_smp 例項在kernel/sched.c中, (1), 呼叫get_online_cpus,如果目前CPU Hotplug Active Write行程是自己,就直接返回.反之就把 cpu_hotplug.refcount加1 (表示多一個Reader)
(2),取得Mutex Lock “sched_domains_mutex”
(3),呼叫arch_init_sched_domains,設定scheduler domains與groups,參考Linux Documentation/scheduler/sched-domains.txt檔案,一個Scheduling Domain會包含一個或多個CPU Groups,排程的Load-Balance就會根據Domain中的Groups來做調整.
(4),釋放Mutex Lock “sched_domains_mutex”
(5),呼叫put_online_cpus,如果目前CPU Hotplug Active Writer行程是自己,就直接返回.反之就把 cpu_hotplug.refcount減1,如果 cpu_hotplug.refcount減到為0,表示沒有其他Reader,此時如果有CPU Hotplug Active Writer行程在等待,就會透過wake_up_process喚醒該行程,以便讓等待中的Writer可以被執行下去.(也可以參考_cpu_up中對於函式cpu_hotplug_begin的說明).
(6)註冊CPU Notifier cpuset_cpu_active/cpuset_cpu_inactive/update_runtime
(7),呼叫set_cpus_allowed_ptr,透過這函式可以設定CPU bitmask,限定Task只能在特定的處理器上運作.在這會用引數”non_isolated_cpus”,也就是會把init指定給non-isolated CPU. Linux Kernel可以在啟動時,透過Boot Parameters “isolcpus=“指定CPU編號或是範圍,讓這些處理器不被包含在Linux Kernel SMP balancing/scheduling演算法內,可以在啟動後指派給特定的Task運作.而不在 “isolcpus=“ 指定範圍內的處理器就算是non-isolated CPU.
(8),呼叫sched_init_granularity,透過函式update_sysctl,讓sysctl_sched_min_granularity=normalized_sysctl_sched_min_granularity,sysctl_sched_latency=normalized_sysctl_sched_latency,sysctl_sched_wakeup_granularity=normalized_sysctl_sched_wakeup_granularit
do_basic_setup 例項在init/main.c中,
1,diaousermodehelper_init (in kernel/kmod.c),產生khelper workqueue.
2,呼叫init_tmpfs (in mm/shmem.c),對VFS註冊Temp FileSystem.
3,呼叫driver_init (in drivers/base/init.c),初始化Linux Kernel Driver System Model.
4,呼叫init_irq_proc(in kernel/irq/proc.c),初始化 “/proc/irq”與其下的File Nodes.
5,呼叫do_ctors (in init/main.c),執行位於Symbol __ctors_start 到 __ctors_end間屬於Section “.ctors” 的Constructor函式.
6,透過函式do_initcalls,執行介於Symbol __early_initcall_end與__initcall_end之間的函式呼叫,
sys_open 例項在fs/fcntl.c中,”SYSCALL_DEFINE1(dup, unsigned int, fildes)”,在這會連續執行兩次sys_dup,複製兩個sys_open開啟/dev/console所產生的檔案描述0 (也就是會多生出兩個1與2),只是都對應到”/dev/console”,我們在System V streams下的Standard Stream一般而言會有如下的對應
0:Standard input (stdin)
1:Standard output (stdout)
2:Standard error (stderr)
(為方便大家參考,附上Wiki URL http://en.wikipedia.org/wiki/Standard_streams )
ramdisk_execute_command與prepare_namespace 1,如果ramdisk_execute_command為0,就設定ramdisk_execute_command = “/init”
2,如果sys_access確認檔案ramdisk_execute_command 失敗,就把ramdisk_execute_command 設定為0,然後呼叫prepare_namespace去mount root FileSystem.
integrity_load_keys 至此我們初始化工作完成, 檔案系統也已經準備好了,那麼接下來載入 load integrity keys hook
load_default_modules 載入基本的模組

kernel_init分析

static int __ref kernel_init(void *unused)
{
    int ret;

    kernel_init_freeable();
    /* need to finish all async __init code before freeing the memory */
    async_synchronize_full();
    free_initmem();
    mark_rodata_ro();
    system_state = SYSTEM_RUNNING;
    numa_default_policy();

    flush_delayed_fput();

    rcu_end_inkernel_boot();

    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);
    }

    /*
     * We try each of these until one succeeds.
     *
     * The Bourne shell can be used instead of init if we are
     * trying to recover a really broken machine.
     */
    if (execute_command) {
            ret = run_init_process(execute_command);
            if (!ret)
                    return 0;
            panic("Requested init %s failed (error %d).",
                  execute_command, ret);
    }
    if (!try_to_run_init_process("/sbin/init") ||
        !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.");
}
執行流程 說明
kernel_init_freeable 呼叫kernel_init_freeable完成初始化工作,準備檔案系統,準備模組資訊
async_synchronize_full 用以同步所有非同步函式呼叫的執行,在這函式中會等待List async_running與async_pending都清空後,才會返回. Asynchronously called functions主要設計用來加速Linux Kernel開機的效率,避免在開機流程中等待硬體反應延遲,影響到開機完成的時間
free_initmem free_initmem(in arch/arm/mm/init.c),釋放Linux Kernel介於__init_begin到 __init_end屬於init Section的函式的所有記憶體.並會把Page個數加到變數totalram_pages中,作為後續Linux Kernel在配置記憶體時可以使用的Pages. (在這也可把TCM範圍(__tcm_start到__tcm_end)釋放加入到總Page中,但TCM比外部記憶體有效率,適合多媒體,中斷,…etc等對效能要求高的執行需求,放到總Page中,成為可供一般目的配置的儲存範圍
system_state 設定執行狀態SYSTEM_RUNNING
載入init程序,進入使用者空間 a,如果ramdisk_execute_command不為0,就執行該命令成為init User Process.
b,如果execute_command不為0,就執行該命令成為init User Process.
c,如果上述都不成立,就依序執行如下指令
run_init_process(“/sbin/init”);
run_init_process(“/etc/init”);
run_init_process(“/bin/init”);
run_init_process(“/bin/sh”);
也就是說會按照順序從/sbin/init, /etc/init, /bin/init 與 /bin/sh依序執行第一個 init User Process.
如果都找不到可以執行的 init Process,就會進入Kernel Panic.如下所示panic(“No init found. Try passing init= option to kernel. ”“See Linux Documentation/init.txt for guidance.”);