執行人: kk908676
專題解說影片
jserv
dingsen-Greenhorn
「採用 vfork 的優勢在於省去頁表複製與寫時複製機制(copy-on-write, CoW)造成的延遲與 CPU 開銷,對於緊接著要執行 exec 的流程,效能提升尤為明顯。但 vfork 僅限於「子代行程執行完畢後立即呼叫 exec」的場景,且因共用位址空間,一旦子代行程出錯,也可能影響親代行程。」
「子代行程執行完畢後立即呼叫 exec」 建議改成「子代行程執行完畢前必須立即呼叫 exec」,比較符合原意,vfork限制是子程序必須在呼叫exec或exit前不做其他事。
otischung
你的專題影片提到你目前使用的記憶卡是 16GB 的,有沒有嘗試使用 64GB 以上的進行測試呢?是否需要因為磁碟格式不同而有所調整?
將 Linux v6.14.y 移植到 STM32F429i-Discovery 平台並客製化相關的軟體組成,包含探討 Arm Cortex-M4 開機流程、記憶體佈局、中斷處理機制、輕量級 boot loader、Linux 核心組態、rootfs 建構,和降低佔用的空間等客製化。
待整理的筆記 應適度展開於本頁面
至少涵蓋 Arm Cortex-M4 的開機流程、記憶體佈局 (含 XIP)、中斷處理等機制,以 STMF429i-Discovery 為例
參照: 嵌入式系統建構:開發運作於STM32的韌體程式
搭配閱讀下方網頁,以得知 Linux(-nommu) 如何移植到 Arm Cortex-M 處理器架構:
Arm Cortex-M 是款 32 位元微控制器架構,設計目標包括低功耗、高效能與即時反應,廣泛應用於物聯網裝置、工業控制、感測器節點與可穿戴裝置;支援 Thumb-2 混合指令長度設計、內建 NVIC (Nested Vectored Interrupt Controller) 、SysTick 定時器與可選用 MPU,並具備小封裝、低成本與較長的產品生命週期特性。
特性 | Cortex-M | Cortex-A |
---|---|---|
主要目標 | 低功耗、即時且可預測 | 高效能、豐富作業系統支援 |
深度睡眠 | μA 等級 (極佳) | mA 等級 (要考慮 SoC 整理功耗) |
低活動 | 數百 μA 到若干 mA (如 Cortex-M0+) | 數萬 mA (閒置/輕量負載) |
高活動 | 數十 mA 到 ~100+ mA (如 Cortex-M7) | 數百 mA 到若干 A (高度負載) |
複雜度 | 簡單、較少晶片面積 | 較複雜、晶片面積更大,常整合 GPU |
作業系統 | 裸機、RTOS、Linux-noMMU | 完整的 Linux、Android、Windows 等 |
Linux-noMMU (過往稱為 uClinux 專案,針對 m68k 一類缺乏 MMU 的硬體) 是針對無 MMU 環境調整的 Linux 核心組態,其優勢包括:可重用龐大且成熟的驅動程式與應用生態,降低開發者學習成本;並藉由現有工具鏈、檔案系統與網路堆疊,快速建置複雜系統。此外,無 MMU 設計可簡化記憶體管理與啟動流程,適合資源有限但需整合 Linux 系統的專案。開發者需考量記憶體足跡、啟動時間、即時性需求與系統穩定性,並為行程隔離、中斷延遲與除錯提供替代機制,如利用 MPU、最佳化中斷優先權、採用精簡 rootfs 與串列除錯策略。
此外,單一定址空間設計消除頁表切換與快取資料不一致的風險,使行程切換與系統呼叫的延遲更可預測,對即時處理需求尤為有利;vfork 替代 fork 亦避免複製定址空間的高延遲開銷,有助於降低背景作業對即時性能的影響。在無 MMU 系統中,核心與應用共用同一實體定址空間,省去分頁表格管理、TLB 查詢與分頁異常處理,讓核心程式碼精簡、RAM 使用更有效率;應用程式直接以實體位址存取記憶體,除錯過程更直覺,跨行程共享記憶體也只需傳遞指標。所有記憶體存取皆繞過位址轉譯,消除因 TLB 未命中或分頁中斷帶來的非預期延遲,而行程切換時不必切換頁表或清除 TLB,也減少切換延遲,使中斷服務行為更可預測,對需要毫秒級甚至亞毫秒級反應的馬達控制或感測器擷取等嵌入式工作負載尤為關鍵。儘管此架構放棄 MMU 提供的行程隔離與記憶體保護,可能導致單一程式錯誤波及全域穩定性,但在 Cortex-M 這類 RAM/Flash 容量受限且追求極低功耗與高決定性的微控制器上,簡化的記憶體管理與更可預測的執行時間,往往遠超過虛擬記憶體帶來的好處;若硬體支援 MPU,則可對核心與特定行程或記憶區段給予存取權限,可彌補缺少 MMU 帶來的限制。
摘自〈Context Switching and IPC Performance Comparison between uClinux and Linux on the ARM9 based Processor〉對於 Linux + MMU (預設組態) 和 Linux no-MMU 在 ARM920T 硬體的測試表現。
基準測試結果顯示使用 LMbench 套件中的
lat_ctx
工具測得的情境切換延遲。評測涵蓋行程存取不同資料大小 (0 KB、1 KB、16 KB) 及不同並行的行程數量。在圖表或數據中,以實心點 (•
或x
) 表示具備 MMU 的標準 Linux 測量值,以空心圈 (o) 表示 Linux no-MMU 的測量值。
當資料大小設定為 0 KB 時,代表每個行程並未配置專屬的資料存取緩衝區,而僅靠暫存器與最基本的堆疊上下文切換即可完成。在具備 MMU 的標準 Linux 中,切換過程還包含頁表切換與 TLB 失效處理,而在無 MMU 的 Linux no-MMU 上,則省略這些步驟,使得在 0 KB 模式下的延遲更能反映最小化上下文保存與回復所需的時間。
對於追求毫秒級或更低切換延遲的即時系統,理解這種「無額外資料存取」的基準測試結果,有助於評估不同作業核心設計或排程策略對總體反應速度的影響。
所有行程建立機制(包括 fork, vfork, kthread_create)最終都會進入核心的 copy_process
,由它根據參數為新行程分配所需資源。當使用 vfork 取代 fork 時,子代行程不會複製親代行程的記憶體定址空間,而是與親代行程共用同一實體定址空間,也就是說,子代行程對資料的任何修改,親代行程都能立即讀寫。為維護正確性,vfork 執行後會暫停親代行程,直至子代行程呼叫 exec 或自行結束。
採用 vfork 的優勢在於省去頁表複製與寫時複製機制(copy-on-write, CoW)造成的延遲與 CPU 開銷,對於緊接著要執行 exec 的流程,效能提升尤為明顯。但 vfork 僅限於「子代行程執行完畢後立即呼叫 exec」的場景,且因共用位址空間,一旦子代行程出錯,也可能影響親代行程。
POSIX 規範中另提供 posix_spawn 作為行程建立的替代方案,原為不支援 fork 的平臺設計。posix_spawn 等同於 fork 加上 exec,但可擴充參數較少、語意更簡潔,其執行時間不受親代行程記憶體大小影響,適合對行程建立延遲要求嚴格的場景。
呼叫 fork 後,核心會複製親代行程的 mm_struct
、區域結構與頁表,並將所有分頁標為只讀,當任一行程嘗試寫入時才觸發寫時複製機制,將需要修改的頁面真正複製到新的實體記憶體;親代與子代行程因此共享初始全域變數、暫存器與堆疊內容,並各自擁有獨立的定址空間。相較之下,vfork 不會在啟動時執行任何頁表複製或私有化,必須等子代行程呼叫 exec 或 exit 後,親代行程才會解除阻塞並繼續執行。行程內的多執行緒則始終共用同一地址空間與全域資料。
在 Arm Cortex-M 平台上執行 Linux-noMMU 需應當考慮以下底層議題:
CONFIG_MMU=n
編譯核心。Thumb‑2 採用混合指令長度設計 (16 位元與 32 位元) ,其目標是提高指令編碼密度;需確保核心與裝置驅動均以 Thumb 模式編譯、連結,並在最小化指令集切換開銷與維持高效管線使用間取得平衡。--specs=nosys.specs
或 --specs=nano.specs
以縮減函式庫大小;運用 buildroot 和 Yocto 建構精簡的 rootfs (BusyBox + uClibc-ng/musl),搭配 initramfs 或外部 Flash (JFFS2/UBIFS)。BSP 應包含 Cortex-M 特定組態 (啟用 CONFIG_ARM_THUMB
選項、設定 NVIC、驅動 MPU),以加速移植並確保與 Linux 上游開發相容。參照 uClinux flat file format 和 MMU-less systems and FDPIC,探討 Flat File Format 和 FDPIC 等議題,應涵蓋動態連結的處理機制。
注意: 若干網頁的資訊已過時,斟酌閱讀並謹慎求證
在無 Linux no-MMU 環境裡,傳統 ELF 可執行檔(Executable and Linkable Format)面臨虛擬位址與實體位址映射缺失的挑戰:ELF 的程式區段(.text
, .data
, .bss
)通常依賴 p_vaddr
欄位指定的虛擬定址,再由 MMU 於載入時建立對應到實體記憶體的映射,缺少這層映射便無法直接將 ELF 匯入至任意記憶體位置執行。雖然 ELF 本身並不強制要有虛擬記憶,但現有的載入器(loader)與核心 binfmt_elf 模組都假設底層具備 MMU 支援,因此在無 MMU 架構下只能倚靠額外機制才能運作。
對此,Linux no-MMU 系統多採用 bFLT (Binary Flat Format) 作為可執行檔格式。bFLT 以扁平化記憶體模型為主,亦即所有區段既無虛擬定址也無分頁映射,而是由核心載入器根據檔頭 (flat_hdr
) 中的 p_mem_start
, p_mem_end
, p_bss_end
與堆疊大小等欄位,在一塊連續的實體 RAM 範圍中依序佈置 .text
, .data
與 .bss
,再依靠重定位條目 (reloc entries) 修正程式碼或資料裡的絕對位址。elf2flt 工具會自動解析 ELF 檔案,提取這些必要欄位並生成相應的重定位表,使得在載入階段,只要把 bFLT 標頭、程式區段與重定位列表逐項讀入並依序處理,就能在任意實體位址執行原先以 ELF 編譯的程式。
在 Linux 核心原始程式碼,bFLT 載入器位於 fs/binfmt_flat.c,部分廠商也為 bFLT 增補對位置獨立程式碼 (PIC) 與共享函式庫的支援。若要在 Linux no-MMU 直接使用 ELF,可改採編譯器與連結器支援的簡易 flat-ELF 方案:將所有 PT_LOAD
程式標頭 (program header) 的 p_paddr
欄位設定為實體定址,並在核心以相同邏輯載入,省去 bFLT 格式轉換。
bFLT 的實務操作流程為:核心讀取 flat_hdr
標頭、建立夠大的連續 RAM 區塊,將 .text
與已初始化的 .data
複製到該區,再將 .bss
全部歸零,接著依序走訪重定位條目,將程式內所有絕對位址修正至實際載入基址,並配置行程堆疊。最後核心將程式參數 (argc
, argv
) 及環境變數放到堆疊上,跳至 header 中的入口函式地址開始執行。由於此流程不涉及 MMU,適合 RAM/Flash 容量有限、卻仍需多行程支援的 Cortex-M 或其他缺乏 MMU 的微控制器。
至於如何產生 bFLT,常見做法是先用交叉編譯器(如 arm-none-eabi-gcc)搭配 -fPIE
或 -fPIC
產生位置獨立的 ELF,再經由 elf2flt 工具轉換。buildroot 和 Yocto 已整合 elf2flt,可在連結階段藉由 -Wl,-elf2flt
旗標自動指定,直接輸出 bFLT 而無需手動執行。
使用 buildroot 2025.02 或之後的版本,建構 Linux 核心和 rootfs 映像檔
Buildroot 是一個用於建立嵌入式 Linux 系統的建構框架,它可以用來自動化建構包含 root filesystem
、toolchain
、 bootloader
、 Linux kernel
及應用程式等元件的整個系統映像。整個 Buildroot 系統是透過 Kconfig
和 Makefile
進行配置,使用方式與編譯 Linux kernel 類似,可以透過 menuconfig
等工具進行設定。雖然 Buildroot 通常會搭配 Linux kernel 使用,但它也可以在沒有 kernel 的情況下建立檔案系統,例如用於 bare-metal
環境
當我們編譯 Linux
系統時,往往需要自行準備交叉編譯器(cross-compiler)、解決套件編譯相依性、手動建構 kernel、rootfs 和 boot loader,過程繁瑣易出錯。
Buildroot
正是為了簡化這一流程而設計的工具。它提供一個整合式的建構系統,讓你可以:
menuconfig
介面進行自定義設定適合嵌入式開發,能大幅減少設定錯誤與整合時間
下載 Buildroot 原始碼 :
使用預設的 config
選擇我們需要的特定 Linux Kernel 版本
位置: Kernel Kernel version
需要做的更動
以及 In-tree Device Tree Source file names
位置
接著更改用來建構 C 標頭檔( kernel headers ) 的來源版本
位置 : Toolchain Custom kernel headers series
另外我們在編譯時, Buildroot 為了安全性會去強制驗證下載的 linux-6.14.5.tar.xz
的 SHA256 雜湊值是否與 Buildroot 官方所設定的一致
為避免因為沒有對應的雜湊, 所以先將強制驗證關閉
位置 : Build options Advanced Force all downloads to have a valid hash
將設定保存後, 緊接著就是編譯
完成後會在 /buildroot/output/images/
看到 xipImage
好處是精簡、可追蹤開機流程,可斟酌 fork
搭配「闡述 Arm Cortex-M4 關鍵特性」,解說在 STM32F4 如何初始化硬體並載入 Linux 核心
下載 afboot-stm32
直接編譯後會發現這個問題
這是因為鏈接器從 gcc
改為直接使用 ld
,但 ld
的新版本不再接受 -nostartfiles
選項,導致編譯錯誤( Error: unable to disambiguate: -nostartfiles
)
修改 Makefile
修改完後又出現了另一個問題
因為在 gcc
編譯時,編譯器會將某些 hand-written code
(例如看起來像 memcpy
或 memset
的迴圈)最佳化為對 C 標準庫函數的呼叫。但是, 在 bare-metal
環境中, 這些標準庫函數不可用,導致鏈接錯誤( undefined reference to memcpy
和 memset
)
再對 Makefile 做以下修改
修改完之後執行
就會看到目錄中多了 stm32f429i-disco.bin
,之後將它燒入開發板中
確定 Linux 核心可從 SD 卡讀取並正確啟動,包含 rootfs 也比照處理
為了避免在實務過程中反覆的燒錄韌體, 所以將必要的 Linux Kernel image 以及 Device Tree 燒錄在 SD card 並且透過 SPI 傳輸資料
STM32 PIN | VCP |
---|---|
PE2 | SPI4_SCK |
PE4 | SPI4_NSS |
PE5 | SPI4_MISO |
PE6 | SPI4_MOSI |
USB to TTL 接線:
下方是 Linux Kernel image 以及 Device Tree 擺放位置
之後就藉由 afboot 做 :
下載 afboot patch 來修改原本的程式碼
執行
燒錄進板子內
有了 bootloader 之後我們就只需要將 Linux Kernel image 以及 Device Tree 燒錄在 SD card ,這樣 bootloader 就可以到指定的地點透過 SPI 抓取這些資料以此來啟動 kernel
在準備 image 和 device tree 時依據上方的記憶體配置圖修改 SDRAM 大小以及位置、 xipImage 執行位置
位置 : System Type Set flash/sdram size and base addr
更改 (S)DRAM Base Address 為 0x90300000
更改 (S)DRAM SIZE 為 0x00500000
位置 : Boot options Kernel Execute-In-Place from ROM
更改 XIP Kernel Physical Location 為 0x90000000
改完之後就能直接編譯產生我們要的 Linux Kernel image 以及 Device Tree
會在 buildroot/output/images
看到 xipImage 以及 stm32f429-disco.dtb
接著就要將這兩個檔案燒錄進 SD card ,下載 :
然後還需要一個 SD card 的映像檔用來定義 SD card 的分割區結構
執行
就可以看到 sdcard.img
的映像檔了
接下來就把 SD card 插入電腦
可以看到 sdd
就是剛剛插入大小為 15G 的 SD card
透過以下命令將 sdcard.img
燒錄進 SD card
便能看到 Linux Kernel image
和 Device Tree
隨後將 SD card 插入 microsd card adapter
待 bootloader 成功讀抓取到這些資料後便能啟動 Linux kernel
開啟 fbdev, touchscreen, rng 等裝置,見 AdrianHuang/uclinux-robutest
整合 Linux Tiny Patches for Linux 6.9.1
和 OpenWrt 的修改