Uboot和系統移植(16)------- Linux內核的啓動過程

一,makefile和鏈接腳本的簡單分析

1、Makefile分析

(1)kernel的Makefile寫法和規則等和uboot的Makefile是一樣的,甚至Makefile中的很多內容都是一樣的。
(2)kernel的Makefile比uboot的Makefile要複雜,這裏我們並不會一行一行的詳細分析。
(3)Makefile中只有一些值得關注的我會強調一下,其他不強調的地方暫時可以不管。
(4)Makefile中剛開始定義了kernel的內核版本號。這個版本號挺重要(在模塊化驅動安裝時會需要用到),要注意會查,會改。

在這裏插入圖片描述
(5)在make編譯內核時,也可以通過命令行給內核makefile傳參(跟uboot配置編譯時傳參一樣)。譬如make O=xxx可以指定不在源代碼目錄下編譯,而到另外一個單獨文件夾下編譯。
(6)kernel的頂層Makefile中定義了2個變量很重要,一個是ARCH,一個是CROSS_COMPILEARCH決定當前配置編譯的路徑,譬如ARCH = arm的時候,將來在源碼目錄下去操作的arch/arm目錄。CROSS_COMPILE用來指定交叉編譯工具鏈的路徑和前綴

在這裏插入圖片描述

(7)CROSS_COMPILE = xxx和ARCH = xxx和O=xxx這些都可以在make時通過命令行傳參的方式傳給頂層Makefile
所以有時候你會看到別人編譯內核時:
在這裏插入圖片描述
2、鏈接腳本分析
(1)分析連接腳本的目的就是找到整個程序的入口entry
(2)kernel的連接腳本並不是直接提供的,而是提供了一個彙編文件vmlinux.lds.S,然後在編譯的時候再去編譯這個彙編文件得到真正的鏈接腳本vmlinux.lds。
(3)vmlinux.lds.S在arch/arm/kernel/ 目錄下。
(4)入門在哪裏?從vmlinux.lds中ENTRY(stext)可以知道入口符號是stext,在SI中搜索這個符號,發現arch/arm/kernel/目錄下的head.S和head-nommu.S中都有。
(5)head.S是啓用了MMU情況下的kernel啓動文件,相當於uboot中的start.S。head-nommu.S是未使用mmu情況下的kernel啓動文件。

在這裏插入圖片描述

思考:爲什麼linux kernel不直接提供vmlinux.lds而要提供一個vmlinux.lds.S然後在編譯時纔去動態生成vmlinux.lds呢?
猜測:.lds文件中只能寫死,不能用條件編譯。但是我們在kernel中鏈接腳本確實有條件編譯的需求(但是lds格式又不支持),於是乎kernel工作者找了個投機取巧的方法,就是把vmlinux.lds寫成一個彙編格式,然後彙編器處理的時候順便條件編譯給處理了,得到一個不需要條件編譯的vmlinux.lds。

二,head.S文件分析

1.前段分析

1、內核運行的物理地址與虛擬地址
(1)KERNEL_RAM_VADDR(VADDR就是virtual address),這個宏定義了內核運行時的虛擬地址。值爲0xC0008000
(2)KERNEL_RAM_PADDR(PADDR就是physical address),這個宏定義內核運行時的物理地址。值爲0x30008000
(3)總結:內核運行的物理地址是0x30008000,對應的虛擬地址是0xC0008000。
在這裏插入圖片描述

2、內核的真正入口
(1)內核的真正入口就是ENTRY(stext)處
(2)前面的__HEAD定義了後面的代碼屬於段名爲.head.text的段

在這裏插入圖片描述

3、內核運行的硬件條件
(1)內核的起始部分代碼是被解壓代碼調用的。回憶之前講zImage的時候,uboot啓動內核後實際調用運行的是zImage前面的那段未經壓縮的解壓代碼,解壓代碼運行時先將zImage後段的內核解壓開,然後再去調用運行真正的內核入口。
(2)內核啓動不是無條件的,而是有一定的先決條件,這個條件由啓動內核的bootloader(我們這裏就是uboot)來構建保證。

(3)ARM體系中,函數調用時實際是通過寄存器傳參的(函數調用時傳參有兩種設計:一種是寄存器傳參,另一種是棧內存傳參)。所以uboot中最後theKernel (0, machid, bd->bi_boot_params);執行內核時,運行時實際把0放入r0中,machid放入到了r1中,bd->bi_boot_params放入到了r2中。ARM的這種處理技巧剛好滿足了kernel啓動的條件和要求。
(4)kernel啓動時MMU是關閉的,因此硬件上需要的是物理地址。但是內核是一個整體(zImage)只能被連接到一個地址(不能分散加載),這個連接地址肯定是虛擬地址。因此內核運行時前段head.S中尚未開啓MMU之前的這段代碼必須是位置無關碼,而且其中涉及到操作硬件寄存器等時必須使用物理地址。

2.內核啓動的彙編階段分析

在這裏插入圖片描述

1、__lookup_processor_type
(1)我們從cp15協處理器的c0寄存器中讀取出硬件的CPU ID號,然後調用這個函數來進行合法性檢驗。如果合法則繼續啓動,如果不合法則停止啓動,轉向__error_p啓動失敗。
(2)該函數檢驗cpu id的合法性方法是:內核會維護一個本內核支持的CPU ID號碼的數組,然後該函數所做的就是將從硬件中讀取的cpu id號碼和數組中存儲的各個id號碼依次對比,如果沒有一個相等則不合法,如果有一個相等的則合法。
(3)內核啓動時設計這個校驗,也是爲了內核啓動的安全性着想。

2、__lookup_machine_type
(1)該函數的設計理念和思路和上面校驗cpu id的函數一樣的。不同之處是本函數校驗的是機器碼

3、__vet_atags
(1)該函數的設計理念和思路和上面2個一樣,不同之處是用來校驗uboot給內核的傳參ATAGS格式是否正確。這裏說的傳參指的是uboot通過tag給內核傳的參數(主要是板子的內存分佈memtag、uboot的bootargs)
(2)內核認爲如果uboot給我的傳參格式不正確,那麼我就不啓動。
(3)uboot給內核傳參的部分如果不對,是會導致內核不啓動的。譬如uboot的bootargs設置不正確內核可能就會不啓動。

4、__create_page_tables
(1)顧名思義,這個函數用來建立頁表
(2)linux內核本身被連接在虛擬地址處,因此kernel希望儘快建立頁表並且啓動MMU進入虛擬地址工作狀態。但是kernel本身工作起來後頁表體系是非常複雜的,建立起來也不是那麼容易的。kernel想了一個好辦法。

(3)kernel建立頁表其實分爲2步。第一步,kernel先建立了一個段式頁表(和uboot中之前建立的頁表一樣,頁表以1MB爲單位來區分的),這裏的函數就是建立段式頁表的。段式頁表本身比較好建立(段式頁表1MB一個映射,4GB空間需要4096個頁表項,每個頁表項4字節,因此一共需要16KB內存來做頁表),壞處是比較粗不能精細管理內存;第二步,再去建立一個細頁表(4kb爲單位的細頁表),然後啓用新的細頁表廢除第一步建立的段式映射頁表
(4)內核啓動的早期建立段式頁表,並在內核啓動前期使用;內核啓動後期就會再次建立細頁表並啓用。等內核工作起來之後就只有細頁表了。

5、__switch_data

在這裏插入圖片描述
(1)建立了段式頁表後進入了__switch_data部分,這東西是個函數指針數組。
(2)分析得知下一步要執行__mmap_switched函數。
(3)複製數據段、清除bss段(目的是構建C語言運行環境)。
(4)保存起來cpu id號、機器碼、tag傳參的首地址。
(5)b start_kernel跳轉到C語言運行階段。

總結:彙編階段其實也沒幹啥,主要原因是uboot幹了大部分活。彙編階段主要就是校驗啓動合法性、建立段式映射的頁表並開啓MMU以方便使用內存、跳入C階段。

3.內核啓動的C語言階段分析

3.1內核啓動文件main.c -------> Do necessary setups

在這裏插入圖片描述

1、一些初始化代碼
(1)smp_setup_processor_id。smp就是對稱多處理器(其實就是我們說的多核心CPU),所以是建立smp。
(2)lockdep_init。鎖定依賴,是一個內核調試模塊,處理內核自旋鎖死鎖問題相關的。
(3)debug_objects_early_init。對obj_hash,obj_static_pool這兩個全局變量進行初始化設置。這兩個全局變量在進行調試的時候會使用到。
(4)boot_init_stack_canary。用來防止棧溢出。
(5)cgroup_init_early。control group,內核提供的一種來處理進程組的技術。
(6)local_irq_disable。屏蔽當前CPU上的所有中斷。
(7)early_boot_irqs_off。通過該標記可以讓我們知道是否在early bootup code。
(8)early_init_irq_lock_class。設置所有IRQ描述符的鎖是統一的鎖還是各有各的小鎖。
(9)lock_kernel。獲得大內核鎖,該鎖可以用來鎖定整個內核。
(10)tick_init。初始化tick控制功能,註冊clockevents的框架。
(11)boot_cpu_init。設置第一個CPU核爲活躍CPU核。若系統爲單CPU核系統,則設置僅有的CPU爲活躍CPU核。
(12)page_address_init。函數初始化高端內存頁表池的鏈表。

2、打印內核版本信息
(1)代碼位於:kernel/init/main.c中的572行

在這裏插入圖片描述
(2)printk函數是內核中用來從console打印信息的,類似於應用層編程中的printf。內核編程時不能使用標準庫函數,因此不能使用printf,其實printk就是內核自己實現的一個printf。
(3)printk函數的用法和printf幾乎一樣,不同之處在於可以在參數最前面用一個宏來定義消息輸出的級別。爲什麼要有這種級別?主要原因是linux內核太大了,代碼量太多,裏面的printk打印信息太多了。如果所有的printk都能打印出來而不加任何限制,則最終內核啓動後得到海量的輸出信息。

在這裏插入圖片描述
(4)爲了解決打印信息過多,無效信息會淹沒有效信息這個問題,linux內核的解決方案是給每一個printk添加一個打印級別。級別定義0-7(注意編程的時候要用相應的宏定義,不要直接用數字)分別代表8種輸出的重要性級別,0表示最重要,7表示最不重要。我們在printk的時候自己根據自己的消息的重要性去設置打印級別。
(5)linux的控制檯監測消息的地方也有一個消息過濾顯示機制,控制檯實際只會顯示級別比我的控制檯定義的級別高的消息。譬如說控制檯的消息顯示級別設置爲4,那麼只有printk中消息級別爲0-3(也可能是0-4)的纔可以顯示看見,其餘的被過濾掉了。

3、setup_arch函數

實際上這個函數是用來確定我們當前內核的機器(arch、machine)的。我們的linux內核會支持一種CPU的運行,CPU+開發板就確定了一個硬件平臺,然後我們當前配置的內核就在這個平臺上可以運行。之前說過的機器碼就是給這個硬件平臺一個固定的編碼,以表徵這個平臺。當前內核支持的機器碼以及硬件平臺相關的一些定義都在這個函數中處理。我們當前使用的平臺:CPU:S5PV210開發板:X210

(1)setup_processor函數

在這裏插入圖片描述
setup_processor函數用來查找CPU信息,可以結合串口打印的信息來分析。它的查找方式是通過read_cpuid_id函數,對硬件進行查找,從而確定CPI的ID。

(2)setup_machine函數

在這裏插入圖片描述
setup_machine函數的傳參是機器碼編號,通過在keinel根目錄中搜索grep "mathine_arch_type " * -nR),從而確定machine_arch_type符號在include/generated/mach-types.h32039-32050行定義了。經過分析後確定這個傳參值就是2456。通過這個2456機器碼反查從而找到對應這個機器碼的machine_desc描述符,並且返回這個描述符的指針
其實真正幹活的函數是lookup_machine_type,它通過調用了 ___lookup_machine_type這個函數來工作。

該函數的工作原理內核在建立的時候就把各種CPU架構的信息組織成一個一個的machine_desc結構體實例,然後都給一個段屬性.arch.info.init,鏈接的時候會保證這些描述符會被連接在一起。__lookup_machine_type就去那個那些描述符所在處依次挨個遍歷各個描述符,比對看機器碼哪個相同。

(3)setup_arch函數進行了基本的cmdline處理

在這裏插入圖片描述

  • 這裏說的cmdline就是指的uboot給kernel傳參時傳遞的命令行啓動參數,也就是uboot的bootargs
  • 有幾個相關的變量需要注意:default_command_line:實際是一個全局變量字符數組,這個字符數組可以用來存東西。CONFIG_CMDLINE:在.config文件中定義的(可以在make menuconfig中去更改設置),這個表示內核的一個默認的命令行參數。
  • 內核對cmdline的處理思路是:內核中自己維護了一個默認的cmdline(就是.config中配置的這一個),然後uboot還可以通過tag給kernel再傳遞一個cmdline。如果uboot給內核傳cmdline成功則內核會優先使用uboot傳遞的這一個;如果uboot沒有給內核傳cmdline或者傳參失敗,則內核會使用自己默認的這個cmdline。以上說的這個處理思路就是在setup_arch函數中實現的。

思考:內核爲什麼要這樣設計?
因爲可以通過傳參數就可以改變內核運行的狀態,不用重新編譯燒錄,方便了很多

4、setup_command_line函數
該函數也是在處理和命令行參數cmdline有關的任務。

5、parse_early_param&parse_args函數

在這裏插入圖片描述
(1)解析cmdline傳參和其他傳參
(2)這裏的解析意思是把cmdline的細節設置信息給解析出來。譬如cmdline:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3,則解析出的內容就是就是一個字符串數組,數組中依次存放了一個設置項目信息
console=ttySAC2,115200 一個(串口2,波特率是115200
root=/dev/mmcblk0p2 rw 一個
init=/linuxrc 一個
rootfstype=ext3 一個
(3)這裏只是進行了解析,並沒有去處理。也就是說只是把長字符串解析成了短字符串,最多和內核裏控制這個相應功能的變量掛鉤了,但是並沒有去執行。執行的代碼在各自模塊初始化的代碼部分。

6、還是一些初始化代碼
(1)trap_init。設置異常向量表。
(2)mm_init。內存管理模塊初始化。
(3)sched_init。內核調度系統初始化。
(4)early_irq_init&init_IRQ。中斷初始化。
(5)console_init.控制檯初始化。
……
start_kernel函數中調用了很多的xx_init函數,全都是內核工作需要的模塊的初始化函數。這些初始化之後內核就具有了一個基本的可以工作的條件了。

在rest_init函數之前,內核工作所需的初始化條件已經基本結束了,剩下的一些工作就比較重要了,放在了該函數中。至此也就意味着start_kernel函數基本結束了。

下面對start_kernel函數做的工作進行簡單的總結: start_kernel函數做的主要工作有打印了一些信息、內核工作需要的模塊的初始化被依次調用(譬如內存管理、調度系統、異常處理···)、我們需要重點了解的就是setup_arch中做的2件事情:機器碼架構的查找並且執行架構相關的硬件的初始化、uboot給內核的傳參cmdline。

3.2補充進程知識

1、什麼是內核進程/線程
(1)進程和線程。簡單來理解,一個運行的程序就是一個進程。所以進程就是任務、進程就是一個獨立的程序。獨立的意思就是這個程序和別的程序是分開的,這個程序可以被內核單獨調用執行或者暫停
(2)在linux系統中,線程和進程非常相似,幾乎可以看成是一樣的。實際上我們當前講課用到的進程和線程的概念就是一樣的。
(3)進程/線程就是一個獨立的程序。應用層運行一個程序就構成一個用戶進程/線程,那麼內核中運行一個函數(函數其實就是一個程序)就構成了一個內核進程/線程。
(4)所以我們kernel_thead函數運行一個函數,其實就是把這個函數變成了一個內核線程去運行起來,然後他可以被內核調度系統去調度。說白了就是去調度器註冊了一下,以後人家調度的時候會考慮你。

2、進程0、進程1、進程2
(1)截至目前爲止,我們一共涉及到3個內核進程/線程。
(2)操作系統是用一個數字來表示/記錄一個進程/線程的,這個數字就被稱爲這個進程的進程號。這個號碼是從0開始分配的。因此這裏涉及到的三個進程分別是linux系統的進程0、進程1、進程2.
(3)在linux命令行下,使用ps命令可以查看當前linux系統中運行的進程情況。
(4)我們在ubuntu下ps -aux可以看到當前系統運行的所有進程,可以看出進程號是從1開始的。爲什麼不從0開始,因爲進程0不是一個用戶進程,而屬於內核進程。

在這裏插入圖片描述
(5)三個進程
進程0**:進程0其實就是剛纔講過的idle進程,叫空閒進程,也就是死循環。
進程1:kernel_init函數就是進程1,這個進程被稱爲
init進程**。
進程2:kthreadd函數就是進程2,這個進程是linux內核的守護進程。這個進程是用來保證linux內核自己本身能正常工作的
通過觀察可以發現,進程0屬於內核進程,工作在內核態,而進程2屬於用戶進程,工作在用戶態,那麼兩個狀態之間的切換需要由進程1來完成。

3.3rest_init

在這裏插入圖片描述
1、操作系統最終去哪了
(1)rest_init中調用kernel_thread函數啓動了2個內核線程,分別是:kernel_init和kthreadd
(2)調用schedule函數開啓了內核的調度系統,從此linux系統開始轉起來了。
(3)rest_init最終調用cpu_idle函數結束了整個內核的啓動。也就是說linux內核最終結束了一個函數cpu_idle。這個函數裏面肯定是死循環。
(4)簡單來說,linux內核最終的狀態是:有事幹的時候去執行有意義的工作(執行各個進程任務),實在沒活幹的時候就去死循環(實際上死循環也可以看成是一個任務)。
(5)之前已經啓動了內核調度系統,調度系統會負責考評系統中所有的進程,這些進程裏面只有有哪個需要被運行,調度系統就會終止cpu_idle死循環進程(空閒進程)轉而去執行有意義的幹活的進程。這樣操作系統就轉起來了。

2、init進程原理詳解1

(1)init進程剛開始運行的時候是內核態,它屬於一個內核線程然後他自己運行了一個用戶態下面的程序後把自己強行轉成了用戶態。因爲init進程自身完成了從內核態到用戶態的過度,因此後續的其他進程都可以工作在用戶態下面了

(2)內核態下做了什麼?重點就做了一件事情,就是掛載根文件系統並試圖找到用戶態下的那個init程序。init進程要把自己轉成用戶態就必須運行一個用戶態的應用程序(這個應用程序名字一般也叫init),要運行這個應用程序就必須得找到這個應用程序,要找到它就必須得掛載根文件系統,因爲所有的應用程序都在文件系統中。
內核源代碼中的所有函數都是內核態下面的,執行任何一個都不能脫離內核態。應用程序必須不屬於內核源代碼,這樣才能保證自己是用戶態。也就是說我們這裏執行的這個init程序和內核不在一起,他是另外提供的。提供這個init程序的那就是根文件系統

(3)用戶態下做了什麼?init進程大部分有意義的工作都是在用戶態下進行的。init進程對我們操作系統的意義在於:其他所有的用戶進程都直接或者間接派生自init進程。

(4)如何從內核態跳躍到用戶態?還能回來不?
init進程在內核態下面時,通過一個函數kernel_execve來執行一個用戶空間編譯連接的應用程序就跳躍到用戶態了。注意這個跳躍過程中進程號是沒有改變的,所以一直是進程1.這個跳躍過程是單向的,也就是說一旦執行了init程序轉到了用戶態下整個操作系統就算真正的運轉起來了,以後只能在用戶態下工作了,用戶態下想要進入內核態只有走API這一條路了

  • init進程構建了用戶交互界面
    (1**)init進程是其他用戶進程的老祖宗**。linux系統中一個進程的創建是通過其父進程創建出來的。根據這個理論只要有一個父進程就能生出一堆子孫進程了。
    (2)init啓動了login進程、命令行進程、shell進程
    (3)shell進程啓動了其他用戶進程。命令行和shell一旦工作了,用戶就可以在命令行下通過./xx的方式來執行其他應用程序,每一個應用程序的運行就是一個進程。

3、init進程代碼詳解2

3.1、打開控制檯
(1)linux系統中每個進程都有自己的一個文件描述符表,表中存儲的是本進程打開的文件
(2)linux系統中有一個設計理念:一切皆是文件。所以設備也是以文件的方式來訪問的。我們要訪問一個設備就要去打開這個設備對應的文件描述符。譬如/dev/fb0這個設備文件就代表LCD顯示器設備,/dev/buzzer代表蜂鳴器設備,/dev/console代表控制檯設備。
(3)這裏我們打開了/dev/console文件,並且複製了2次文件描述符,一共得到了3個文件描述符。這三個文件描述符分別是0、1、2.這三個文件描述符就是所謂的:標準輸入、標準輸出、標準錯誤
(4)進程1打開了三個標準輸入輸出錯誤文件,因此後續的進程1衍生出來的所有的進程默認都具有這3個三件描述符

在這裏插入圖片描述

3.2、掛載根文件系統

在這裏插入圖片描述
(1)prepare_namespace函數中掛載根文件系統

(2)根文件系統在哪裏?根文件系統的文件系統類型是什麼? uboot通過傳參來告訴內核這些信息

  • uboot傳參中的root=/dev/mmcblk0p2 rw 這一句就是告訴內核根文件系統在哪裏
  • uboot傳參中的rootfstype=ext3這一句就是告訴內核rootfs的類型

(3)如果內核掛載根文件系統成功,則會打印出:VFS: Mounted root (ext3 filesystem) on device 179:2.
如果掛載根文件系統失敗,則會打印:No filesystem could mount root, tried: yaffs2

(4)如果內核啓動時掛載rootfs失敗,則後面肯定沒法執行了,肯定會死。內核中設置了啓動失敗休息5s自動重啓的機制,因此這裏會自動重啓,所以有時候大家會看到反覆重啓的情況。

(5)如果掛載rootfs失敗,可能的原因有:

  • 最常見的錯誤就是uboot的傳參bootargs設置不對

  • rootfs燒錄失敗(fastboot燒錄不容易出錯,以前是手工燒錄很容易出錯)

  • rootfs本身製作失敗的。(尤其是自己做的rootfs,或者別人給的第一次用)

3.3、執行用戶態下的進程1程序

在這裏插入圖片描述
(1)上面一旦掛載rootfs成功,則進入rootfs中尋找應用程序的init程序這個程序就是用戶空間的進程1.找到後用run_init_process去執行他

(2)我們如果確定init程序是誰?方法是:
先從uboot傳參cmdline中看有沒有指定,如果有指定先執行cmdline中指定的程序。cmdline中的init=/linuxrc這個就是指定rootfs中哪個程序是init程序。這裏的指定方式就表示我們rootfs的根目錄下面有個名字叫linuxrc的程序,這個程序就是init程序

如果uboot傳參cmdline中沒有init=xx或者cmdline中指定的這個xx執行失敗,還有備用方案。
第一備用:/sbin/init,
第二備用:/etc/init,
第三備用:/bin/init,
第四備用:/bin/sh。

三,cmdline常用參數

1、格式簡介
(1)格式就是由很多個項目用空格隔開依次排列每個項目中都是項目名=項目值
(2)整個cmdline會被內核啓動時解析,解析成一個一個的項目名=項目值的字符串。這些字符串又會被再次解析從而影響啓動過程。

2、root=
(1)這個是用來指定根文件系統在哪裏的
(2)一般格式是root=/dev/xxx(一般如果是nandflash上則/dev/mtdblock2,如果是inand/sd的話則/dev/mmcblk0p2)
(3)如果是nfs的rootfs,則root=/dev/nfs。

3、rootfstype=
(1)根文件系統的文件系統類型一般是jffs2、yaffs2、ext3、ubi

4、console=
(1)控制檯信息聲明,譬如console=/dev/ttySAC0,115200表示控制檯使用串口0,波特率是115200.
(2)正常情況下,內核啓動的時候會根據console=這個項目來初始化硬件,並且重定位console到具體的一個串口上,所以這裏的傳參會影響後續是否能從串口終端上接收到內核的信息。

5、mem=
(1)mem=用來告訴內核當前系統的內存有多少

6、init=
(1)init=用來指定進程1的程序pathname,一般都是init=/linuxrc

7、常見cmdline介紹
(1)console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3
第一種這種方式對應rootfs在SD/iNand/Nand/Nor等物理存儲器上。這種對應產品正式出貨工作時的情況

(2)root=/dev/nfs nfsroot=192.168.1.141:/root/s3c2440/build_rootfs/aston_rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200
第二種這種方式對應rootfs在nfs上,這種對應我們實驗室開發產品做調試的時候

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章