2021-05-12 14:32:11
Linux 的初始化與啟動過程
我們執行程式只需要點選應用程式的圖示就可以了,但在這之前,我們必須啟動我們的系統。在一切之前,我們必須有某些程式去引導我們系統的核心,這些程式就是核心載入程式了,例如LILO、GRUB、U-Boot、RedBoot。而這些載入程式同樣需要被其他程式載入和執行,這樣說下去,茫茫人生何處才是盡頭啊?想必大家可以想到的----硬體!這麼長的過程複雜、崎嶇!正所謂萬事開頭難,但不怕,我們來一起走過去吧!
X86的引導過程如圖:
cpu自身的初始化:這是引導的第一步,如果在多處理器系統上,那麼每個cpu都要自身初始化。cpu初始化後,cpu從某個固定的位置(應該是0Xfffffff0)取指,這條指令是跳轉指令,目的地是BIOS的首部程式碼,但是cpu並不在乎BIOS是否存在,它僅僅只是執行這個地址的指令而已!
BIOS:BIOS是唯讀記憶體(ROM),被固化於主機板上。其工作主要有兩個,就是上圖的加電自檢即是POST(post on self test)與載入核心載入程式。
那麼他們是具體完成什麼工作的呢?
1) 加電自檢:完成系統的硬體檢測,其中包括記憶體檢測、系統匯流排檢測等工作。
2) 載入核心載入程式:在POST完成後,就要載入核心載入程式了,那它儲存在哪裡呢?磁碟裡!哈哈,BIOS會讀取0磁頭,0磁軌,一磁區的512個位元組,這個磁區有叫做MBR(主開機記錄),MBR中儲存了核心載入程式的開始部分,BIOS將其裝入記憶體執行。512個位元組的MBR有些什麼呢?這裡有必要說說MBR!MBR分割區表以80為起始,以55AA為結束,共64個位元組。具體的MBR知識自己百度!
MBR:1) 446個位元組的載入程式程式碼
2) 64個位元組的分割區表,有多少個分割區呢。。?這還真不知道!分為4個分割區表,一個可啟動分割區和三個不可啟動分割區。
3) 2個位元組的0XAA55,用於檢查MBR是否有效。
需要注意的是,核心載入程式被載入完後,POST部分的程式碼會被從記憶體中清理,只留部分在記憶體中留給目標作業系統使用。
核心載入程式:核心載入程式分兩部分:主、次載入程式。主載入程式的主要工作就是收索,尋找活動的分割區,將活動的分割區引導記錄中的次載入程式載入到記憶體中並且執行。而這個次載入程式就是負責載入核心的並且將控制權交給核心。上面提過核心載入程式有LILO、GRUB、U-Boot、RedBoot。其中前面兩個為pc中的,而後面兩個是嵌入式的。
核心:核心以壓縮的形式存在,不是一個可執行的核心!所以核心階段首先要做的是自解壓核心映像。這裡說說編譯核心後形成的核心壓縮的映像vmlinuz。編譯生成vmlinux後,一般會對其進行壓縮為vmlinuz,使其成為zImage--小於512KB的小核心,或者成為bzImage--大於512KB的大核心。
vmlinuz結構如圖:
做了這麼多工作終於把linux的核心給引匯出來了。!!下面我們來初始化這個千呼萬喚始出來的linux核心!
核心初始化:核心會呼叫一系列的初始化函數去對所有的核心元件進行初始化,由start_kernel()---.....---> rest_init() ----..----> kernel_init() ----....--> init_post() ------到---> 第一個使用者init進程結束。
start_kernel():其完成大部分核心初始化的工作。相關的程式碼去查閱核心的原始碼吧!www.kernel.org
rest_init():start_kernel() 呼叫rest_init() 進行後面的初始化工作。
kernel_init():此函數主要完成裝置驅動程式的初始化,並且呼叫 init_post() 啟動使用者空間的init進程。
init_post():初始化的尾聲,第一個使用者空間的init 橫空出世!其PID始終為1。
init: 核心會在過去曾使用過init的幾個地方查詢它,它的正確位置(對Linux系統來說)是/sbin/init。如果核心找不到init,它就會試著執行/bin/sh,如果執行失敗,系統的啟動也會失敗。找到/sbin/init 後init會根據/etc/inittab (網上的資料都這樣說的,我在Ubuntu3.8的核心裡找不到,但在Fedora中可以找到!是發行版本不同吧?(求指教)這裡附上圖片!)檔案完成其他一些工作,例如:getty進程接受使用者的登入,設定網路等。這裡詳細說說吧。
fedora19的etc有inittab檔案:
系統中所有的進程形成樹型結構,而這棵樹的根就是在核心態形成的,系統自動構造的0號進程,它是所有的進程的祖先。大致是在vmlinux的入口 startup_32(head.S)中為pid號為0的原始進程設定了執行環境,然後原是進程開始執行start_kernel()完成Linux核心的初始化工作。包括初始化頁表,初始化中斷向量表,初始化系統時間等。繼而呼叫 fork(),建立第核心init進程:
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); // 引數CLONE_FS | CLONE_SIGHAND表示0號執行緒和1號執行緒分別共用檔案系統(CLONE_FS)、開啟的檔案(CLONE_FILES)和信號處理程式 (CLONE_SIGHAND)。
這個進程就是著名的pid為1的init進程(核心態的),它會繼續完成剩下的初始化工作比且建立若干個用於快取記憶體和虛擬主記憶體管理的核心執行緒,如kswapd和bdflush等,然後execve(/sbin/init)(生成使用者態的init進程pid=1,因為沒有呼叫fork(),所以pid還是1!), 成為系統中的其他所有進程的祖先。回過頭來看pid=0的進程,在建立了init進程後(核心態的),pid=0的進程呼叫 cpu_idle()在主cpu中演變成了idle進程。而核心態pid=1的init進程同樣會在各個從cpu上生成idel進程。 init在演變成/sbin/init之前,會執行一部分初始化工作,其中一個就是smp_prepare_cpus(),初始化SMP處理器,在這過程中會在處理每個從處理器時呼叫
task =copy_process(CLONE_VM, 0, idle_regs(®s), 0, NULL, NULL, 0); init_idle(task, cpu);
即從init中複製出一個進程,並把它初始化為idle進程(pid仍然為0)。從處理器上的idle進程會進行一Activate工作,然後執行cpu_idle()。
執行/sbin/init,這樣從核心太過度到使用者態,按照組態檔/etc/inittab 要求完成啟動的工作,並且建立若干個不編號為1,2,3...號的終端註冊進程getty,其作用就是設定其行程群組的標識號,監視設定到系統終端的介面電路,當有信號來到的時候,getty會執行execve()生成註冊進程login,使用者可以註冊登入了,如果登入成功,則login會演化為shell進程,若login不成功則關閉開啟的終端線路,使用者1號進程會建立新的getty。到這裡init的流程基本完成了。奉上大圖一張!
init的過程:
init 的流程講完了,這裡粗略說說init還做了什麼事。先來認識下執行級別。
執行級別: linux可以在不同的場合啟動不同的開機啟動程式,這就叫做執行級別。根據不同的執行級別啟動不同的程式。例如在用作伺服器的時候要開啟Apache,而桌面就不需要。
linux預先設定了7種執行級別(0--6)。ubuntu有8種(0--6、S),這裡主要以ubuntu來說。0:關閉系統,1:系統進入單使用者模式,S:單使用者恢復模式,文字登入介面,只執行少數的系統服務。2:多使用者模式(系統預設的級別),圖形登入介面,執行所有預定的系統服務。3--5:多使用者模式,圖形登入介面,執行所有預定的系統服務(對於系統客製化而言,執行級別2-5的作用等同),6:重新啟動系統。
對於每個執行級別,在/etc/都有對應的子錄目---/etc/rcN.d 用來指定要載入的程式。
細心看的話可以發現,除README外其他的檔案都是“S開頭+兩位數位+程式名”的形式。這代表什麼呢?S代表Start啟動。如果是K的話則代表關閉kill,如果從其他的執行級別切換過來的話則要關閉程式。之後的數位為處理的順序,越小則越早執行,如果數位相同,按字母的順序啟動。
上圖可以看出這裡的檔案都是連結檔案,為什麼呢?上面說過各種執行級別有各自的一個錄目用來存放各自的開機程式,如果有多個執行級別要啟動同一個程式,那麼這個程式的指令碼會被拷貝到每一個錄目裡,這樣做的話,如果要修改啟動指令碼就要修改每一個錄目,這樣不科學啊!!!!所以這些檔案都是連結檔案指向/etc/init.d。啟動時就是執行這些指令碼的。
子系統的初始化:
核心選項:linux 允許使用者傳遞核心設定選項給核心,核心在初始化的過程中呼叫parse_args()函數對這些選項進行解析,之後呼叫相應的函數進行處理。對於parse_args()函數,其能夠解析形如“變數名=值”的字串,在模組載入的時候也會被呼叫來解析模組的引數。
子系統的初始化:在完成核心選項的解析後,就進入初始化的函數呼叫。在kernel_init()函數中呼叫do_basic_setup()函數再去呼叫do_initcalls()函數來完成。各個函數具體實現請查閱原始碼!
登入:登入有三種方式。
1) 命令列登入
init建立getty,等使用者輸入使用者名稱和密碼,輸入完成後呼叫login程式進行核對密碼,如果正確就讀取/etc/passwd檔案,讀取這個使用者的指定的shell並啟動它。
2) ssh登入
系統呼叫sshd程式,取代getty和login,之後啟動shell。
3) 圖形介面登入
init進程呼叫顯示管理器,Gnome圖形介面對應為gdm,然後輸入使用者名稱、密碼,如果密碼正確就啟動使用者對談。
到這裡系統就啟動起來了!說了這麼多,現在我們來玩點好玩的!!!!!!簡單熟悉一下linux的啟動!!!!!!!!!!!首先要準備一個ubuntu系統最好13.04或者12.04都得,可以是虛擬機器(最好是在虛擬機器上操作!因為我因為這個小實驗而重灌了一遍真機系統!當時不懂啊!慘。。。。。。。。。)。
1:進入系統,在主資料夾(方便)新建一個c語言檔案並且命名為init.c,輸入---->最簡單的c語言程式helloworld,不這是最偉大的c語言程式!
main(){
printf("helloworld!n");
}
之後不用說就是編譯啦。開啟終端(ctrl + alt + t)執行這條指令:gcc --static -o init init.c 這樣init檔案就準備好了!猜到我想做什???了嗎。。?哈哈我們繼續!!
2:將上文提到過的/sbin/init 檔案備份執行這條指令:sudo cp /sbin/init /sbin/init.bak 備份成init.bak 檔案
將原來的init 檔案刪除,執行指令:sudo rm /sbin/init
好了,下一步就將我們的helloworld init複製到/sbin/錄目下!執行指令: sudo mv init /sbin/ 這條指令之前要注意你終端當前的路徑與init的路徑是不是相同,要不不能成功的!!
3:這樣就做好了!!!我們果斷重新啟動!這樣在啟動中我們看到了我們的 helloworld!唉。。它停哪裡了!!那是肯定的,上文介紹了init的作用,你換了,不能啟動是正常的啦。。思考下,到這裡核心處於什麼狀態。?這裡其實核心基本已經初始化完成了!就剩下進程的生成與子系統的初始化了。在init_post()函數的最尾會查詢init的路徑,如果找不到就崩潰。而且會試圖建立一個互動式的shell(/bin/sh)來代替找不到的init,讓使用者可以修復這種錯誤、重新啟動。
4:現在我們來恢復我們的系統!剛才在 2 中的步驟不會讓你白做的!重新啟動系統,並在一開始就按著左Shift (我以前的系統是不用的,不知道以前對grub.cfg做了什麼壞事!!),系統會自動檢測到這個信號的,這樣會出現下面圖中的介面。(建議去看看grub2的特性!):
按下e,進行編輯,按鍵盤右下角 向下的按鍵,在linux 這一行的最尾(quiet)前面加入----> init=/sbin/init.bak ,按下 ctrl + x 啟動系統。如圖:
如果一切沒有錯,那麼你肯定能成功地重新啟動系統!如果看到登入介面,那麼恭喜你,你已經在漫漫人生路上走了一趟了!
好了,到這裡本文要講的知識已經講完了。下面我們一起來思考一下我在這其中遇到的沒有解決的問題,請大神們指教!
問題: 首先我的實驗是在虛擬機器上做的,原本我以為init是跟核心的壓縮檔案有關,所以用3.8.0-19 核心進去系統將init刪了,這樣3.8.0-19核心肯定啟動不了核心。我用3.8.0-27核心進去系統,同樣不能進去。。還是出現偉大的helloworld!這樣看 init跟vmlinux等 在boot裡的檔案無關了!那麼init關機後會被儲存在哪裡!?還有一個最重要的!我重新裝了一遍系統(虛擬機器的),這是沒有3.8.0-27核心的,我將我真機的3.8.0-27的核心檔案(/boot 裡的vmlinuz檔案等)放到虛擬機器的/boot/資料夾裡,重新啟動系統,發現解析度變了,游標不靈活(可能是解析度的關係!),螢幕大小感覺是原來的兩倍。這是為什麼呢?
ps:本文挺長!如果你有疑問請提出來,交流交流,學習學習!如果你知道上面的問題的原因,那麼請大神留幾句話共小弟學習學習!
本文永久更新連結地址:http://www.linuxidc.com/Linux/2015-06/118516.htm
相關文章