Linux開機流程
一.簡介
計算機啟動分為核心載入前、載入時和載入後3個大階段,這3個大階段又可以分為很多小階段,本文將非常細化分析每一個重要的小階段。
核心載入前的階段和作業系統無關,Linux或Windows在這部分的順序是一樣的。由於使用anaconda安裝Linux時,預設的圖形介面是不支援GPT分割槽的,即使是目前最新的CentOS 7.3也仍然不支援,所以在本文中主要介紹傳統BIOS平臺(MBR方式)的啟動方式(其實是本人愚笨,看不懂uefi啟動方式)。
在核心載入時和載入後階段,由於CentOS 7採用的是systemd,和CentOS 5或CentOS 6的sysV風格的init大不相同,所以本文也只介紹sysV風格的init。
二.按下電源和bios階段
按下電源,計算機開始通電,最重要的是要接通cpu的電路,然後通過cpu的針腳讓cpu執行起來,只有cpu執行起來才能執行相關程式碼跳到bios。
bios是按下開機鍵後第一個執行的程式,它會讀取CMOS中的資訊,以瞭解部分硬體的資訊,比如硬體自檢(post)、硬體上的時間、硬碟大小和型號等。其實,手動進入bios介面看到的資訊,都是在這一階段獲取到的,如下圖。對本文來說,最重要的還是獲取到了啟動裝置以及它們的啟動順序(順序從上到下)資訊。
當硬體檢測和資訊獲取完畢,開始初始化硬體,最後從排在第一位的啟動裝置中讀取MBR,如果第一個啟動裝置中沒有找到合理的MBR,則繼續從第二個啟動裝置中查詢,直到找到正確的MBR。
三.MBR和各種bootloader階段
這小節將介紹各種BR(boot record)和各種boot loader,但只是簡單介紹其基本作用。
MBR是主引導記錄,位於磁碟的第一個扇區,和分割槽無關,和作業系統無關,bios一定會讀取MBR中的記錄。
在MBR中儲存了bootloader/分割槽表/BRID。bootloader佔用446個位元組,用於引導載入;分割槽表佔用64個位元組,每個主分割槽或擴充套件分割槽佔用16個位元組,如果16個位元組中的第一個位元組為0x80,則表示該分割槽為啟用的分割槽(活動分割槽),且只允許有一個啟用的分割槽;最後2個位元組是BRID(boot record ID),它固定為0x55AA,用於標識該儲存裝置的MBR是否是合理有效的MBR,如果bios讀取MBR發現最後兩個位元組不是0x55AA,就會讀取下一個啟動裝置。
boot loader
MBR中的bootloader只佔用446位元組,所以可儲存的程式碼有限,能載入引導的東西也有限,所以在磁碟的不同位置上設計了多種boot loader。下面將說明各種情況。
在建立檔案系統時,是否還記得有些分割槽的第一個block是boot sector?這個啟動扇區中也放了boot loader,大小也很有限。如果是主分割槽上的boot sector,則該段boot loader所在扇區稱為VBR(volumn boot record),如果是邏輯分割槽上的boot sector,則該段boot loader所在扇區稱為EBR(Extended boot sector)。但很不幸,這兩種方式的boot loader都很少被使用上了,因為它們很不方便,加上後面出現了啟動管理器(LILO和GRUB),它們就被遺忘了。但即使如此,在分割槽中還是存在boot sector。
分割槽表
硬碟分割槽的好處之一就是可以在不同的分割槽中安裝不同的作業系統,但boot loader必須要知道每個作業系統具體是在哪個分割槽。
分割槽表的長度只有64個位元組,裡面又分成四項,每項16個位元組。所以,一個硬碟最多隻能分四個主分割槽。
每個主分割槽表項的16個位元組,都由6個部分組成:
(1).第1個位元組:只能為0或者0x80。0x80表示該主分割槽是啟用分割槽,0表示非啟用分割槽。單磁碟只能有一個主分割槽是啟用的。
(2).第2-4個位元組:主分割槽第一個扇區的物理位置(柱面、磁頭、扇區號等等)。
(3).第5個位元組:主分割槽型別。
(4).第6-8個位元組:主分割槽最後一個扇區的物理位置。
(5).第9-12位元組:該主分割槽第一個扇區的邏輯地址。
(6).第13-16位元組:主分割槽的扇區總數。
最後的四個位元組"主分割槽的扇區總數",決定了這個主分割槽的長度。也就是說,一個主分割槽的扇區總數最多不超過2的32次方。如果每個扇區為512個位元組,就意味著單個分割槽最大不超過2TB。
採用VBR/EBR方式引導作業系統
暫且先不討論grub如何管理啟動作業系統的,以VBR和EBR引導作業系統為例。
當bios讀取到MBR中的boot loader後,會繼續讀取分割槽表。分兩種情況:
(1)如果查詢分割槽表時發現某個主分割槽表的第一個位元組是0x80,也就是啟用的分割槽,那麼說明作業系統裝在了該主分割槽,然後執行已載入的MBR中的boot loader程式碼,載入該啟用主分割槽的VBR中的boot loader,至此,控制權就交給了VBR的boot loader了;
(2)如果作業系統不是裝在主分割槽,那麼肯定是裝在邏輯分割槽中,所以查詢完主分割槽表後會繼續查詢擴充套件分割槽表,直到找到EBR所在的分割槽,然後MBR中的boot loader將控制權交給該EBR的boot loader。
也就是說,如果一塊硬碟上裝了多個作業系統,那麼boot loader會分佈在多個地方,可能是VBR,也可能是EBR,但MBR是一定有的,這是被bios給"繫結"了的。在裝LINUX作業系統時,其中有一個步驟就是詢問你MBR裝在哪裡的,但這個MBR並非一定真的是MBR,可能是MBR,也可能是VBR,還可能是EBR,並且想要單磁碟多系統共存,則MBR一定不能被覆蓋(此處不考慮grub)。
如下圖,是我測試單磁碟裝3個作業系統時的分割槽結構。其中/dev/sda{1,2,3}是第一個CentOS 6系統,/dev/sda{5,6,7}是第二個CentOS 7系統,/dev/sda{8,9,10}是第三個CentOS 6系統,每一個作業系統的分割槽序號從前向後都是/boot分割槽、根分割槽、swap分割槽。
再看下圖,是裝第三個作業系統時的詢問boot loader安裝位置的步驟。
裝第一個作業系統時,boot loader可以裝在/dev/sda上,也可以選擇裝在/dev/sda1上,這時裝的是MBR和VBR,任選一個都會將另一個也裝上,從第二個作業系統開始,裝的是EBR而非MBR,且應該指定boot loader位置(如/dev/sda5和/dev/sda8),否則預設選項是裝在/dev/sda上,但這會覆蓋原有的MBR。
另外,在指定boot loader安裝路徑的下方,還有一個方框是作業系統列表,這就是作業系統選單,其中可以指定預設的作業系統,這裡的預設指的是MBR預設跳轉到哪個VBR或EBR上。
所以,MBR/VBR和EBR之間的跳轉關係如下圖。
使用這種方式的選單管理作業系統啟動,無需什麼stage1,stage1.5和stage2的概念,只要跳轉到了分割槽上的VBR或EBR,那麼直接就可以載入引導該分割槽上的作業系統。
但是,這種管理作業系統啟動的選單已經沒有意義了,現在都是使用grub來管理,所以裝第二個作業系統或第n個作業系統時不手動指定boot loader安裝位置,覆蓋掉MBR也無所謂,想要實現單磁碟多系統共存所需要做的,僅僅只是修改grub的配置檔案而已。
使用grub管理引導選單時,VBR/EBR就毫無用處了,具體的見下文。
四.grub階段
使用grub管理啟動,則MBR中的boot loader是由grub程式安裝的,此外還會安裝其他的boot loader。CentOS 6使用的是傳統的grub,而CentOS 7使用的是grub2。
如果使用的是傳統的grub,則安裝的boot loader為stage1、stage1_5和stage2,如果使用的是grub2,則安裝的是boot.img和core.img。傳統grub和grub2的區別還是挺大的,所以下面分開解釋,如果對於grub有不理解之處,見我的另一篇文章grub2詳解。
使用grub2時的啟動過程
grub2程式安裝grub後,會在/boot/grub2/i386-pc/目錄下生成boot.img和core.img檔案,另外還有一些模組檔案,其中包括檔案系統類的模組。
[root@xuexi ~]# find /boot/grub2/i386-pc/ -name '*.img' -o -name "*fs.mod" -o -name "*ext[0-9].mod"
/boot/grub2/i386-pc/affs.mod
/boot/grub2/i386-pc/afs.mod
/boot/grub2/i386-pc/bfs.mod
/boot/grub2/i386-pc/btrfs.mod
/boot/grub2/i386-pc/cbfs.mod
/boot/grub2/i386-pc/ext2.mod # ext2、ext3和ext4都使用該模組
/boot/grub2/i386-pc/hfs.mod
/boot/grub2/i386-pc/jfs.mod
/boot/grub2/i386-pc/ntfs.mod
/boot/grub2/i386-pc/procfs.mod
/boot/grub2/i386-pc/reiserfs.mod
/boot/grub2/i386-pc/romfs.mod
/boot/grub2/i386-pc/sfs.mod
/boot/grub2/i386-pc/xfs.mod
/boot/grub2/i386-pc/zfs.mod
/boot/grub2/i386-pc/core.img
/boot/grub2/i386-pc/boot.img
其中boot.img就是安裝在MBR中的boot loader。當然,它們的內容是不一樣的,安裝boot loader時,grub2-install會將boot.img轉換為合適的程式碼寫入MBR中的boot loader部分。
core.img是第二段Boot loader段,grub2-install會將core.img轉換為合適的程式碼寫入到緊跟在MBR後面的空間,這段空間是MBR之後、第一個分割槽之前的空閒空間,被稱為MBR gap,這段空間最小31KB,但一般都會是1MB左右。
實際上,core.img是多個img檔案的結合體。它們的關係如下圖:
這張圖解釋了開機過程中grub2階段的所有過程,boot.img段的boot loader只有一個作用,就是跳轉到core.img對應的boot loader的第一個扇區,對於從硬碟啟動的系統來說,該扇區是diskboot.img的內容,diskboot.img的作用是載入core.img中剩餘的內容。
由於diskboot.img所在的位置是以硬編碼的方式寫入到boot.img中的,所以boot.img總能找到core.img中diskboot.img的位置並跳轉到它身上,隨後控制權交給diskboot.img。隨後diskboot.img載入壓縮後的kernel.img(注意,是grub的kernel不是作業系統的kernel)以初始化grub執行時的各種環境,控制權交給kernel.img。
但直到目前為止,core.img都還不識別/boot所在分割槽的檔案系統,所以kernel.img初始化grub環境的過程就包括了載入模組,嚴格地說不是載入,因為在安裝grub時,檔案系統類的模組已經嵌入到了core.img中,例如ext類的檔案系統模組ext2.mod。
載入了模組後,kernel.img就能識別/boot分割槽的檔案系統,也就能找到grub的配置檔案/boot/grub2/grub.cfg,有了grub.cfg就能顯示啟動選單,我們就能自由的選擇要啟動的作業系統。
當選擇某個選單項後,kernel.img會根據grub.cfg中的配置載入對應的作業系統核心(/boot目錄下vmlinuz開頭的檔案),並向作業系統核心傳遞啟動時引數,包括根檔案系統所在的分割槽,init ramdisk(即initrd或initramfs)的路徑。例如下面是某個選單項的配置:
menuentry 'CentOS 6' --unrestricted {
search --no-floppy --fs-uuid --set=root f5d8939c-4a04-4f47-a1bc-1b8cbabc4d32
linux16 /vmlinuz-2.6.32-504.el6.x86_64 root=UUID=edb1bf15-9590-4195-aa11-6dac45c7f6f3 ro quiet
initrd16 /initramfs-2.6.32-504.el6.x86_64.img
}
載入完作業系統核心後grub2就將控制權交給作業系統核心。
總結下,從MBR開始後的過程是這樣的:
1.執行MBR中的boot loader(即boot.img)跳轉到diskboot.img。
2.執行diskboot.img,載入core.img剩餘的部分,並跳轉到kernel.img。
3.kernel.img讀取/boot/grub2/grub2.cfg,並顯示啟動管理選單。
4.選中某選單後,kernel.img載入該選單項配置的作業系統核心/boot/vmlinux-XXX,並傳遞核心啟動引數,包括根檔案系統所在分割槽和init ramdisk的路徑。
5.控制權交給作業系統核心。
使用傳統grub時的啟動過程
傳統grub對應的boot loader是stage1和stage2,從stage1跳轉到stage2大多數情況下還會用到stage1_5對應的boot loader。
與grub2相比,stage1和boot.img的作用是類似的,都在MBR中。當該段boot loader執行後,它的目的是跳轉到stage1_5的第一個扇區上,然後由該扇區的程式碼載入剩餘的內容,並跳轉到stage2的第一個扇區上。
stage1_5存在的理由是因為stage2功能較多,導致其檔案體積較大(一般至少都有100多K),所以並沒有像core.img一樣嵌入到磁碟上,而是簡單地將其放在了boot分割槽上,但stage1並不識別boot分割槽的檔案系統型別,所以藉助中間的輔助boot loader即stage1_5來跳轉。
stage1_5的目的之一是識別檔案系統,但檔案系統的型別有很多,所以對應的stage1_5也有很多種。
[root@xuexi ~]# ls -C /boot/grub/*stage1_5*
/boot/grub/e2fs_stage1_5 /boot/grub/jfs_stage1_5 /boot/grub/vstafs_stage1_5
/boot/grub/fat_stage1_5 /boot/grub/minix_stage1_5 /boot/grub/xfs_stage1_5
/boot/grub/ffs_stage1_5 /boot/grub/reiserfs_stage1_5
/boot/grub/iso9660_stage1_5 /boot/grub/ufs2_stage1_5
雖然有很多種stage1_5,但每個boot分割槽也只能對應一種stage1_5。這個stage1_5對應的boot loader一般會被嵌入到MBR後、第一個分割槽前的中間那段空間(即MBR gap)。
當執行了stage1_5對應的boot loader後,stage1_5就能識別出boot所在的分割槽,並找到stage2檔案的第一個扇區,然後跳轉過去。
當控制權交給了stage2,stage2就能載入grub的配置檔案/boot/grub/grub.conf並顯示選單並初始化grub的執行時環境,當選中作業系統後,stage2將和kernel.img一樣載入作業系統核心,傳遞核心啟動引數,並將控制權交給作業系統核心。
所以,stage1、stage1_5和stage2之間的關係如下圖:
雖然絕大多數都提供了stage1_5,但它不是必須的,它的作用僅僅只是識別boot分割槽的檔案系統型別,對於一個會程式設計的人來說,可以將固定boot分割槽的檔案系統識別程式碼嵌入到stage1中,這樣stage1自身就能識別boot分割槽,就不需要stage1_5了。
看看安裝grub時,grub到底做了些什麼工作。
grub> setup (hd0)
Checking if "/boot/grub/stage1" exists... yes
Checking if "/boot/grub/stage2" exists... yes
Checking if "/boot/grub/e2fs_stage1_5" exists... yes
Running "embed /boot/grub/e2fs_stage1_5 (hd0)"... 15 sectors are embedded.
succeeded
Running "install /boot/grub/stage1 (hd0) (hd0)1+15 p (hd0,0)/boot/grub/stage2 /boot/grub/menu.lst"... succeeded
Done.
首先檢測各stage檔案是否存在於/boot/grub目錄下,隨後嵌入stage1_5到磁碟上,該檔案系統型別的stage1_5佔用了15個扇區,最後安裝stage1,並告知stage1 stage1_5的位置是第1到第15個扇區,之所以先嵌入stage1_5再嵌入stage1就是為了讓stage1知道stage1_5的位置,最後還告知了stage1 stage2和配置檔案menu.lst(它是grub.conf的軟連結)的路徑。
五.核心載入和核心初始化階段
提前說明,下文所述均為sysV init系統啟動風格,systemd的啟動管理方式大不相同,所以不要將systemd管理的啟動方式與此做比較。
到目前為止,核心已經被載入到記憶體掌握了控制權,且收到了boot loader最後傳遞的核心啟動引數以及init ramdisk的路徑。
所有的核心都是以bzImage方式壓縮過的,壓縮後CentOS 6的核心大小大約為4M,CentOS 7的核心大小大約為5M。核心要能正常運作下去,它需要進行解壓釋放。
解壓釋放之後,將建立pid為0的idle程序,該程序非常重要,後續核心所有的程序都是通過fork它建立的,且很多cpu降溫工具就是強制執行idle程序來實現的。
然後建立pid=1和pid=2的核心程序。pid=1的程序也就是init程序,pid=2的程序是kthread核心執行緒,它的作用是在真正呼叫init程式之前完成核心環境初始化和設定工作,例如根據grub傳遞的核心啟動引數找到init ramdisk並載入。
所謂的救援模式就是剛載入完核心,init程序接收到控制權的那一階段,因為沒有進行任何作業系統初始化過程,所以可以修復和作業系統相關的很多問題。另外,安裝映象中也有核心,可以通過安裝映象進入救援模式,這種進入救援模式的方式幾乎可修復任何作業系統啟動相關的問題,即使是/boot目錄下核心映象缺失都可以重灌。(還有一種單使用者模式,它是執行級別為1的環境,所以已經初始化完執行級別,見後文)
載入init ramdisk
在前面,已經建立了pid=1的init程序和pid=2的kthread程序,但注意,它們都是核心執行緒,全稱是kernel_init和kernel_kthread,而真正能被ps捕獲到的pid=1的init程序是由kernel_init呼叫init程式後形成的。
要載入/sbin/init程式,首先要找到根分割槽,根分割槽是有檔案系統的,所以核心需要先識別檔案系統並載入檔案系統的驅動,但檔案系統的驅動又是放在根分割槽的,這就出現了先有雞還是先有蛋的矛盾。
解決的方法之一是像grub2識別boot分割槽的檔案系統一樣,將根檔案系統驅動模組嵌入到核心中,但檔案系統的種類太多,而且會升級,這樣就導致核心不斷的嵌入新的檔案系統驅動模組,核心不斷增大,這顯然是不合適的。
解決方法之二則像傳統grub藉助中間過渡引導段stage1_5一樣,將根檔案系統的驅動模組放入一箇中間過渡檔案,在載入根檔案系統之前先載入這個過渡檔案,再由過渡檔案跳轉到根檔案系統。
方法二正是現在採用的,其採用的中間過渡檔案稱為init ramdisk,它是在安裝完作業系統時生成的,這樣它會收集到當前作業系統的根檔案系統是什麼型別的檔案系統,也就能只嵌入一個對應的檔案系統驅動模組使其變得足夠小。如下圖,它是安裝作業系統時安裝完所有軟體包後執行的一個收集過程。
在CentOS 5上採用的init ramdisk稱為initrd,而CentOS 6和CentOS 7採用的則是initramfs,它們的目的是一樣的,但在實現上卻大有不同。但它們都存放在/boot目錄下。
[root@xuexi ~]# ll -h /boot/init*
-rw-------. 1 root root 19M Feb 25 11:53 /boot/initramfs-2.6.32-504.el6.x86_64.img
可以看到,它們的大小有十多兆,由此也可知道init ramdisk的作用肯定不僅僅只是找到根檔案系統,它還會做其他工作。具體還做什麼工作,請繼續閱讀下文。
initrd
initrd其實是一個映象檔案系統,是在記憶體中劃分一片區域模擬磁碟分割槽,在該檔案中包含了找到根檔案系統的指令碼和驅動。
既然是檔案系統,那麼核心也必須要帶有對應檔案系統的驅動,另外檔案系統要使用就必須有根"/",這個根是記憶體中的"虛根"。由於核心載入到這裡已經初始化一些執行環境了,所以核心的執行狀態等引數也要儲存下來,儲存的位置就是記憶體中虛根下的/proc和/sys,此外還有收集到的硬體裝置資訊以及裝置的執行環境也要儲存下來,儲存的位置是/dev。到此為止,pid=2的核心執行緒kernel_kthread就完成了基本工作,開始轉到kernel_init程序上了。
再之後就是kernel_init掛載真正的根檔案系統並從虛根切換到實根,最後kernel_init將呼叫init程式,也就是真正的能被我們看見的pid=1的init程序,然後將控制權交給init,所以從現在開始,將切換到使用者空間,後續剩餘的事情都將由使用者空間的程式完成。
以下是CentOS 5.8中initrd檔案的解壓過程和解包後的目錄結構。
[root@localhost ~]# cp /boot/initrd-2.6.18-308.el5.img /tmp/initrd.gz
[root@localhost tmp]# gunzip initrd.gz
[root@localhost tmp]# cpio -id < initrd
[root@localhost tmp]# ls
bin dev etc init initrd lib proc sbin sys sysroot
initramfs
initramfs比initrd先進了一些,initrd必須是一個檔案系統,是在記憶體中模擬出磁碟分割槽的,所以核心必須要帶有它的檔案系統驅動,而initramfs則僅僅只是一個映象壓縮檔案而非檔案系統,所以它不需要帶檔案系統驅動,在載入時,核心會將其解壓的內容裝入到一個tmpfs 中。
initramfs和initrd最大的區別在於init程序的區別對待。initramfs為了儘早進入使用者空間,它將init程式整合到了initramfs映象檔案中,這樣就可以在initramfs裝入tmpfs時直接執行init程序,而不用去找根檔案系統下的/sbin/init,由此掛載根檔案系統的工作將由init來完成,而不再是核心執行緒kernel_init完成。最後從虛根切換到實根。
那根分割槽下的/sbin/init是幹嘛的呢?可以認為是init ramdisk中init的一個備份,如果ramdisk中找不到init就會去找/sbin/init。另外,在正常執行的作業系統環境下,/sbin/init還經常用來完成其他工作,如傳送訊號。
其實initramfs完成了很多工作,解開它的映象檔案就能發現它的目錄結構和真實環境下的目錄結構類似。以下是CentOS 7上initramfs-3.10.0-327.el7.x86_64解包過程和解包後的目錄結構。
[root@xuexi ~]# cp /boot/initramfs-3.10.0-327.el7.x86_64.img /tmp/initramfs.gz
[root@xuexi ~]# cd /tmp; gunzip /tmp/initramfs.gz
[root@xuexi tmp]# cpio -id < initramfs
[root@xuexi tmp]# ls -l
total 8
lrwxrwxrwx 1 root root 7 Jun 29 23:28 bin -> usr/bin
drwxr-xr-x 2 root root 42 Jun 29 23:28 dev
drwxr-xr-x 11 root root 4096 Jun 29 23:28 etc
lrwxrwxrwx 1 root root 23 Jun 29 23:28 init -> usr/lib/systemd/systemd
lrwxrwxrwx 1 root root 7 Jun 29 23:28 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Jun 29 23:28 lib64 -> usr/lib64
drwxr-xr-x 2 root root 6 Jun 29 23:28 proc
drwxr-xr-x 2 root root 6 Jun 29 23:28 root
drwxr-xr-x 2 root root 6 Jun 29 23:28 run
lrwxrwxrwx 1 root root 8 Jun 29 23:28 sbin -> usr/sbin
-rwxr-xr-x 1 root root 3041 Jun 29 23:28 shutdown
drwxr-xr-x 2 root root 6 Jun 29 23:28 sys
drwxr-xr-x 2 root root 6 Jun 29 23:28 sysroot
drwxr-xr-x 2 root root 6 Jun 29 23:28 tmp
drwxr-xr-x 7 root root 61 Jun 29 23:28 usr
drwxr-xr-x 2 root root 27 Jun 29 23:28 var
另外,還可以在其sbin目錄下發現init程式。
[root@xuexi tmp]# ll sbin/init
lrwxrwxrwx 1 root root 22 Jun 29 23:28 sbin/init -> ../lib/systemd/systemd
六. 作業系統初始化
下文解釋的是sysV風格的系統環境,與systemd初始化大不相同。
當init程序掌握控制權後,意味著已經進入了使用者空間,後續的事情也將以使用者空間為主導來完成。
init的名稱是initialize的縮寫,是初始化的意思,所以它的作用也就是初始化的作用。在核心載入階段,也有初始化動作,初始化的環境是核心的環境,是由kernel_init、kernel_thread等核心執行緒完成的。而init掌握控制權後,已經可以和使用者空間互動,意味著真正的開始進入作業系統,所以它初始化的是作業系統的環境。
作業系統初始化涉及了不少過程,大致如下:讀取執行級別;初始化系統類的環境;根據執行級別初始化使用者類的環境;執行rc.local檔案完成使用者自定義開機要執行的命令;載入終端;
執行級別
在sysV風格的系統下,使用了執行級別的概念,不同執行級別初始化不同的系統類環境,你可以認為windows的安全模式也是使用執行級別的一種產物。
在Linux系統中定義了7個執行級別,使用0-6的數字表示。
0:halt,即關機
1:單使用者模式
2:不帶NFS的多使用者模式
3:完整多使用者模式
4:保留未使用的級別
5:X11,即圖形介面模式
6:reboot,即重啟
實際上,執行關機或重啟命令的本質就是向init程序傳遞0或6這兩個執行級別。
sysV的init程式讀取/etc/inittab檔案來獲取預設的執行級別,並根據此檔案所指定的配置執行預設執行級別對應的操作。注意,systemd管理的系統是沒有/etc/inittab檔案的,即使有也僅僅只是出於提醒的目的,因為systemd沒有了執行級別的概念,說實話,systemd管的真的太多了。
CentOS 6.6上該檔案內容如下:
[root@xuexi ~]# cat /etc/inittab
# inittab is only used by upstart for the default runlevel.
#
# ADDING OTHER CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM.
#
# System initialization is started by /etc/init/rcS.conf
#
# Individual runlevels are started by /etc/init/rc.conf
#
# Ctrl-Alt-Delete is handled by /etc/init/control-alt-delete.conf
#
# Terminal gettys are handled by /etc/init/tty.conf and /etc/init/serial.conf,
# with configuration in /etc/sysconfig/init.
#
# For information on how to write upstart event handlers, or how
# upstart works, see init(5), init(8), and initctl(8).
#
# Default runlevel. The runlevels used are:
# 0 - halt (Do NOT set initdefault to this)
# 1 - Single user mode
# 2 - Multiuser, without NFS (The same as 3, if you do not have networking)
# 3 - Full multiuser mode
# 4 - unused
# 5 - X11
# 6 - reboot (Do NOT set initdefault to this)
#
id:3:initdefault:
該檔案告訴我們,系統初始化過程由/etc/init/rcS.conf完成,執行級別類的初始化過程由/etc/init.conf來完成,按下CTRL+ALT+DEL鍵要執行的過程由/etc/init/control-alt-delete.conf來完成,終端載入的過程由/etc/init/tty.conf和/etc/init/serial.conf讀取配置檔案/etc/sysconfig/init來完成。再檔案最後,還有一行"id:3:initdefault",表示預設的執行級別為3,即完整的多使用者模式。
確認了要進入的執行級別後,init將先讀取/etc/init/rcS.conf來完成系統環境類初始化動作,再讀取/etc/init/rc.conf來完成執行級別類動作。
系統環境初始化
先看看/etc/init/rcS.conf檔案的內容。
[root@xuexi ~]# cat /etc/init/rcS.conf
# rcS - runlevel compatibility
#
# This task runs the old sysv-rc startup scripts.
#
# Do not edit this file directly. If you want to change the behaviour,
# please create a file rcS.override and put your changes there.
start on startup
stop on runlevel
task
# Note: there can be no previous runlevel here, if we have one it's bad
# information (we enter rc1 not rcS for maintenance). Run /etc/rc.d/rc
# without information so that it defaults to previous=N runlevel=S.
console output
pre-start script
for t in $(cat /proc/cmdline); do
case $t in
emergency)
start rcS-emergency
break
;;
esac
done
end script
exec /etc/rc.d/rc.sysinit
post-stop script
if [ "$UPSTART_EVENTS" = "startup" ]; then
[ -f /etc/inittab ] && runlevel=$(/bin/awk -F ':' '$3 == "initdefault" && $1 !~ "^#" { print $2 }' /etc/inittab)
[ -z "$runlevel" ] && runlevel="3"
for t in $(cat /proc/cmdline); do
case $t in
-s|single|S|s) runlevel="S" ;;
[1-9]) runlevel="$t" ;;
esac
done
exec telinit $runlevel
fi
end script
其中"exec /etc/rc.d/rc.sysinit"這一行就表示要執行/etc/rc.d/rc.sysinit檔案,該檔案定義了系統初始化(system initialization)的內容,包括:
(1).確認主機名。
(2).掛載/proc和/sys等特殊檔案系統,使得核心引數和狀態可與人進行互動。是否還記得在核心載入階段時的/proc和/sys?
(3).啟動udev,也就是啟動類似windows中的裝置管理器。
(4)初始化硬體引數,如載入某些驅動,設定時鐘等。
(5).設定主機名。
(6).執行fsck檢測磁碟是否健康。
(7).掛載/etc/fstab中除/proc和NFS的檔案系統。
(8).啟用swap。
(9).將所有執行的操作寫入到/var/log/dmesg檔案中。
執行級別環境初始化
執行完系統初始化後,接下來就是執行執行級別的初始化。先看看/etc/init/rc.conf的內容。
[root@xuexi ~]# cat /etc/init/rc.conf
# rc - System V runlevel compatibility
#
# This task runs the old sysv-rc runlevel scripts. It
# is usually started by the telinit compatibility wrapper.
#
# Do not edit this file directly. If you want to change the behaviour,
# please create a file rc.override and put your changes there.
start on runlevel [0123456]
stop on runlevel [!$RUNLEVEL]
task
export RUNLEVEL
console output
exec /etc/rc.d/rc $RUNLEVEL
最後一行"exec /etc/rc.d/rc $RUNLEVEL"說明呼叫/etc/rc.d/rc這個指令碼來初始化指定執行級別的環境。Linux採用了將各執行級別初始化內容分開管理的方式,將0-6這7個執行級別要執行的初始化指令碼分別放入rc[0-6].d這7個目錄中。
[root@xuexi ~]# ls -l /etc/rc.d/
total 60
drwxr-xr-x. 2 root root 4096 Jun 11 02:42 init.d
-rwxr-xr-x. 1 root root 2617 Oct 16 2014 rc
drwxr-xr-x. 2 root root 4096 Jun 11 02:42 rc0.d
drwxr-xr-x. 2 root root 4096 Jun 11 02:42 rc1.d
drwxr-xr-x. 2 root root 4096 Jun 11 02:42 rc2.d
drwxr-xr-x. 2 root root 4096 Jun 11 02:42 rc3.d
drwxr-xr-x. 2 root root 4096 Jun 11 02:42 rc4.d
drwxr-xr-x. 2 root root 4096 Jun 11 02:42 rc5.d
drwxr-xr-x. 2 root root 4096 Jun 11 02:42 rc6.d
-rwxr-xr-x. 1 root root 220 Oct 16 2014 rc.local
-rwxr-xr-x. 1 root root 19914 Oct 16 2014 rc.sysinit
實際上/etc/init.d/下的指令碼才是真正的指令碼,放入rcN.d目錄中的檔案只不過是/etc/init.d/目錄下指令碼的軟連結。注意,/etc/init.d是Linux耍的一個小把戲,它是/etc/rc.d/init.d的一個符號連結,在有些類unix系統中是沒有/etc/init.d的,都是直接使用/etc/rc.d/init.d。
以/etc/rc.d/rc3.d為例。
[root@xuexi ~]# ll /etc/rc.d/rc3.d/ | head
total 0
lrwxrwxrwx. 1 root root 16 Feb 25 11:52 K01smartd -> ../init.d/smartd
lrwxrwxrwx. 1 root root 16 Feb 25 11:52 K10psacct -> ../init.d/psacct
lrwxrwxrwx. 1 root root 19 Feb 25 11:51 K10saslauthd -> ../init.d/saslauthd
lrwxrwxrwx 1 root root 22 Jun 10 08:59 K15htcacheclean -> ../init.d/htcacheclean
lrwxrwxrwx 1 root root 15 Jun 10 08:59 K15httpd -> ../init.d/httpd
lrwxrwxrwx 1 root root 15 Jun 11 02:42 K15nginx -> ../init.d/nginx
lrwxrwxrwx. 1 root root 18 Feb 25 11:52 K15svnserve -> ../init.d/svnserve
lrwxrwxrwx. 1 root root 20 Feb 25 11:51 K50netconsole -> ../init.d/netconsole
lrwxrwxrwx 1 root root 17 Jun 10 00:50 K73winbind -> ../init.d/winbind
可見,rcN.d中的檔案都以K或S加一個數字開頭,其後才是指令碼名稱,且它們都是/etc/rc.d/init.d中檔案的連結。S開頭表示進入該執行級別時要執行的程式,S字母后的數值表示啟動順序,數字越大,啟動的越晚;K開頭的表示退出該執行級別時要殺掉的程式,數值表示關閉的順序。
所有這些檔案都是由/etc/rc.d/rc這個程式呼叫的,K開頭的則傳給rc一個stop引數,S開頭的則傳給rc一個start引數。
開啟rc0.d和rc6.d這兩個目錄,你會發現在這兩個目錄中除了"S00killall"和"S01reboot",其餘都是K開頭的檔案。
而在rc[2-5].d這幾個目錄中,都有一個S99local檔案,且它們都是指向/etc/rc.d/rc.local的軟連結。S99表示最後啟動的一個程式,所以rc.local中的程式是2345這4個執行級別初始化過程中最後執行的一個指令碼。這是Linux提供給我們定義自己想要在開機時(嚴格地說是進入執行級別)就執行的命令的檔案。
當初始化完執行級別環境後,將要準備登入系統了。
所謂的單使用者模式(runlevel=1),就是初始化完執行級別1對應的環境。因為已經初始化了作業系統和執行級別,所以單使用者模式所處的層次要比救援模式高的多,能修復的問題也就只有它後面還未初始化的過程:終端初始化和使用者登入問題。
七.終端初始化和登入系統
Linux是多工多使用者的作業系統,它允許多人同時線上工作。但每個人都必須要輸入使用者名稱和密碼才能驗證身份並最終登入。但登陸時是以圖形介面的方式給使用者使用,還是以純命令列模式給使用者使用呢?這是終端決定的,也就是說在登入前需要先載入終端。至於什麼是終端,見我的另一篇文章Linux終端型別。
終端初始化
在Linux上,每次開機都必然會開啟所有支援的虛擬終端,如下圖。
這些虛擬終端是由getty命令(get tty)來完成的,getty命令有很多變種,有mingetty、agetty、rungettty等,在CentOS 5和CentOS 6都使用mingetty,在CentOS 7上使用agetty。getty命令的作用之一是呼叫登入程式/bin/login。
例如,在CentOS 6下,捕獲tty終端情況。
[root@xuexi ~]# ps -elf | grep tt[y]
4 S root 1412 1 0 80 0 - 1016 n_tty_ Jun21 tty2 00:00:00 /sbin/mingetty /dev/tty2
4 S root 1414 1 0 80 0 - 1016 n_tty_ Jun21 tty3 00:00:00 /sbin/mingetty /dev/tty3
4 S root 1417 1 0 80 0 - 1016 n_tty_ Jun21 tty4 00:00:00 /sbin/mingetty /dev/tty4
4 S root 1419 1 0 80 0 - 1016 n_tty_ Jun21 tty5 00:00:00 /sbin/mingetty /dev/tty5
4 S root 1421 1 0 80 0 - 1016 n_tty_ Jun21 tty6 00:00:00 /sbin/mingetty /dev/tty6
4 S root 1492 1410 0 80 0 - 27118 n_tty_ Jun21 tty1 00:00:00 -bash
在CentOS 7下,捕獲tty終端情況。
[root@xuexi tmp]# ps -elf | grep tt[y]
4 S root 8258 1 0 80 0 - 27507 n_tty_ 04:17 tty2 00:00:00 /sbin/agetty --noclear tty2 linux
4 S root 8259 1 0 80 0 - 27507 n_tty_ 04:17 tty3 00:00:00 /sbin/agetty --noclear tty3 linux
4 S root 8260 1 0 80 0 - 27507 n_tty_ 04:17 tty4 00:00:00 /sbin/agetty --noclear tty4 linux
4 S root 8262 915 0 80 0 - 29109 n_tty_ 04:17 tty1 00:00:00 -bash
4 S root 8307 8305 0 80 0 - 29109 n_tty_ 04:17 tty5 00:00:00 -bash
4 S root 8348 8346 0 80 0 - 29136 n_tty_ 04:17 tty6 00:00:00 -bash
細心一點會發現,有的tty終端仍然以/sbin/mingetty程序或/sbin/agetty程序顯示,有些卻以bash程序顯示。這是因為getty程序在呼叫/bin/login後,如果輸入使用者名稱和密碼成功登入了某個虛擬終端,那麼gettty程式會融合到bash(假設bash是預設的shell)程序,這樣getty程序就不會再顯示了。
雖然getty不顯示了,但並不代表它消失了,它仍以特殊的方式存在著。是否還記得/etc/inittab檔案?此檔案中提示了終端載入的過程由/etc/init/tty.conf讀取配置檔案/etc/sysconfig/init來完成。
[root@xuexi ~]# grep tty -A 1 /etc/inittab
# Terminal gettys are handled by /etc/init/tty.conf and /etc/init/serial.conf,
# with configuration in /etc/sysconfig/init.
那麼就看看/etc/init/tty.conf檔案。
[root@xuexi ~]# cat /etc/init/tty.conf
# tty - getty
#
# This service maintains a getty on the specified device.
#
# Do not edit this file directly. If you want to change the behaviour,
# please create a file tty.override and put your changes there.
stop on runlevel [S016]
respawn
instance $TTY
exec /sbin/mingetty $TTY
usage 'tty TTY=/dev/ttyX - where X is console id'
此檔案中的respawn表示程序由init程序監視,一發現被殺掉了init會立即重啟它。所以,只要getty程序一結束,init會立即監視到而重啟該程序。因此,使用者登入成功後getty只是融合到了bash程序中,並非退出,否則init會立即重啟它,而它會呼叫login程式讓你再次輸入使用者和密碼。
再看看/etc/sysconfig/init檔案。
[root@xuexi ~]# cat /etc/sysconfig/init
# color => new RH6.0 bootup
# verbose => old-style bootup
# anything else => new style bootup without ANSI colors or positioning
BOOTUP=color
# column to start "[ OK ]" label in
RES_COL=60
# terminal sequence to move to that column. You could change this
# to something like "tput hpa ${RES_COL}" if your terminal supports it
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
# terminal sequence to set color to a 'success' color (currently: green)
SETCOLOR_SUCCESS="echo -en \\033[0;32m"
# terminal sequence to set color to a 'failure' color (currently: red)
SETCOLOR_FAILURE="echo -en \\033[0;31m"
# terminal sequence to set color to a 'warning' color (currently: yellow)
SETCOLOR_WARNING="echo -en \\033[0;33m"
# terminal sequence to reset to the default color.
SETCOLOR_NORMAL="echo -en \\033[0;39m"
# Set to anything other than 'no' to allow hotkey interactive startup...
PROMPT=yes
# Set to 'yes' to allow probing for devices with swap signatures
AUTOSWAP=no
# What ttys should gettys be started on?
ACTIVE_CONSOLES=/dev/tty[1-6]
# Set to '/sbin/sulogin' to prompt for password on single-user mode
# Set to '/sbin/sushell' otherwise
SINGLE=/sbin/sushell
其中ACTIVE_CONSOLES指令決定了要開啟哪些虛擬終端。SINGLE決定了在單使用者模式下要呼叫哪個login程式和哪個shell。
登入過程
如果不在虛擬終端登入,而是通過為ssh分配的偽終端登入,那麼到建立完getty程序那一步其實開機流程已經完成了。但不管在哪種終端下登入,登入過程也可以算作開機流程的一部分,所以也簡單說明下。
getty程序啟用虛擬終端後將呼叫login程序提示使用者輸入使用者名稱或密碼(或偽終端的連線程式如ssh提示輸入使用者名稱和密碼),當用戶輸入完成後,將驗證輸入的使用者名稱是否合法,密碼是否正確,使用者名稱是否是明確被禁止登陸的,PAM模組對此使用者的限制是如何的等等,還要將登入過程記錄到各個日誌檔案中。如果登入成功,將載入該使用者的bash,載入bash過程需要讀取各種配置檔案,初始化各種環境等等。但不管怎麼說,只要登入成功就表示開機流程全部完成了。
本文版權歸作者所有,歡迎轉載,請務必新增原文連結。