貢獻者: jserv
Linux Kernel Library (LKL) 設計為 Linux 核心的移植版本,在目錄 arch/lkl
中,約有 3500 行的程式碼。LKL 與應用程式連結,以運作於使用者空間,依賴由主機作業系統提供的一組主機端的功能,例如 semaphore, POSIX Threads, malloc、計時器 (timer) 等。LKL 擁有良好定義的外部介面,例如系統呼叫及 virtio-net。LKL 允許 FreeBSD, macOS, Windows, 和(舊版的) Linux 存取各式 Linux 核心的成果。
LKL 執行在非特權的使用者空間,無需記憶體保護機制,只依賴由本機作業系統配置一塊記憶體空間。LKL 的記憶體管理遵循與常規 Linux 核心相同的原則,也就是 SLUB/SLAB 演算法。
其中一個應用場景是,搭配 FUSE,掛載 Linux 核心支援的多種檔案系統。
藉由 LKL,可將 TCP/IP 的處理從核心空間轉移到使用者空間,而不必修改標的應用程式。LKL 還提供使用 DPDK 支援的選項,這能提供快速的 TCP/IP 通訊協定堆疊處理。二者都能降低網路通信中的瓶頸。
上圖的左半部分顯示 Node.js 應用程式如何運作於 Linux。右半部分展示改進的方案,包含 LKL 和 DPDK。
此外,LKL 允許在使用者空間中對 Linux 核心程式碼進行模糊測試 (fuzzing),可搭配 libFuzzer 使用。該手法的優點包括:
示意圖如下:
為了讓模糊測試行為更可預測 (deterministic),甚至可在 LKL 中引入特製的排程器,作法是在核心的同步處理機制 (如 spinlock 和 mutex) 前後插入 yield point。
延伸閱讀:
取得原始程式碼:
安裝依賴的套件:
編譯 LKL:
若要執行內建的測試,需要安裝以下套件:
執行內建測試:
參考執行結果:
在上述的 tools/lkl
目錄,預期可見名為 lklfuse
的執行檔,以下示範藉由 LKL 掛載 ext2 檔案系統,不依賴作業系統原生的檔案系統實作。
可將檔案放入掛載的 /tmp/mnt
目錄,隨後執行 umount
卸載。
相關工具:
前述編譯成功後,預期會在 tools/lkl
目錄產生 liblkl.a
檔案,我們可用來整合,下方的程式碼和操作也在 tools/lkl
目錄中進行。
準備以下 C 程式 (檔名: min.c
)
編譯和連結:
該程式得以在 Linux (或 LKL 支援的作業系統) 環境中,執行部分 Linux 功能,以下是參考輸出:
當 LKL 停留在最後一行訊息 Run /init as init process
,就意味著其無法讀取到有效的 init
程式,作為 PID=1 的行程 (相當於 systemd 一類的系統程式)。
接著準備以下 C 程式 (檔名: readfs.c
)
編譯並連結:
接著準備提供給 LKL 掛載為 rootfs 的檔案系統:
至此我們終於可指定 rootfs 並啟動 LKL:
參考執行輸出:
知曉 LKL 的使用,我們來嘗試傾印 /proc
目錄的所有內容。
準備以下 C 程式: (檔名: cat_proc.c
)
編譯並連結:
參考執行輸出:
LKL 不提供建立執行緒的服務給使用它的程式,不過在 LKL 內部仍仰賴執行緒確保 Linux 核心得以正確運作。在 LKL 的早期版本中,藉由內部執行緒來模擬 Linux 核心所需要的機制,但後者必須有單獨的堆疊,而堆疊的配置是由主機作業系統完成,結果 LKL 無法為執行緒配置新的堆疊,不得不將主機作業系統配置的堆疊分成多個部分。因此,每個執行緒的堆疊大小會隨著 LKL 核心中的執行緒數量成比例地減少,對模擬的執行緒數量施加嚴格的限制。開發人員最終決定放棄在 LKL 內部模擬執行緒的想法,倘若 Linux 核心需要執行緒,LKL 會要求本機作業系統建立執行緒,實作二個中介方法:一個呼叫系統呼叫 thread_create
,另一個呼叫系統呼叫 thread_exit
。關於 LKL 的主機端操作介面,可見 arch/lkl/include/uapi/asm/host_ops.h, LKL 的主機端操作介面
tools/lkl/lib/posix-host.c 是針對 POSIX 相容作業系統的橋接實作,提供上方操作介面的功能:
不過這種方法在解決堆疊問題的同時,產生新問題:Linux 核心必須確保它控制執行緒切換,但使用上述方法,執行緒切換由主機作業系統控制。為了解決這個問題,LKL 中設計模擬執行緒管理的機制。
如何在 LKL 中實作執行緒管理的模擬?需要施加一個特殊限制:在任何給定時刻,LKL 只能在一個執行緒中運行。這個限制顯著降低核心的性能,但簡化執行緒系統的實作。單執行緒透過在設定檔中指定相應的項目來實作,例如指定核心是針對單處理器架構,也就是取消核心選項 CONFIG_SMP
。LKL 從本機作業系統建立執行緒,並獲取相應的系統 semaphore。由於此資源是從主機作業系統獲取的,因此透過回呼函式實作。
Linux 排程器在 LKL 改寫,顧及二個層次:一個系統層次(為此我們從系統獲取 semaphore),另一個屬於 LKL 的內部層次(管理 LKL 內部的執行緒邏輯)。
lkl_host_ops
提供的回呼函式實作。lkl_host_ops
的回呼函式。為了理解負責執行緒管理如何從主機作業系統獲取外部資源 (執行緒和 semaphore),以下分析排程器的主要函式 __schedule()
,其程式碼在檔案 kernel/sched/core.c
中。該函式是排程器的關鍵操作,進入該函式會發生於以下情況:
__schedule()
在切換行程時,排程器除了其他工作外,還處理上下文切換。在 __schedule()
函式中,呼叫函式 context_switch(rq, prev, next)
,然後呼叫在檔案 linux/include/asm-generic/switch_to.h
中定義的巨集 switch_to(prev, next, last)
:
在此操作過程中,呼叫函式 __switch_to(struct task_struct *, struct task_struct *)
,其位於專為 LKL 設計的檔案 linux/arch/lkl/kernel/threads.c
中。這就是在 LKL 中的 Linux 核心內部實作執行緒管理邏輯的地方。
當在 LKL 中由 Linux 核心建立執行緒時,執行下方回呼函式:
並將其添加到 threadinfo
結構體中,該結構體定義於 arch/lkl/include/asm/thread_info.h
這個結構保存執行緒的信息。結構體中的 void *sched_sem
,是內部執行緒排程器用來啟動/停止執行緒的 semaphore。在初始化時,每個新執行緒被配置一個由主機作業系統配置的 semaphore,初始值為零:
因此,新建立的 semaphore 將被阻塞,直到 LKL 排程器決定啟動此執行緒。修改排程器的主要工作實作於函式:
該函式的實作在檔案 arch/lkl/kernel/thread.c
中。函式的主要三個操作階段是:
__switch_to
函式返回一個指向呼叫 __switch_to
函式的行程的 task_struct
的指標。步驟 1 中保存正在切換的執行緒的 task_struct 指標,確保總是返回被切換行程的 task_struct
指標。
例如:假設在給定時刻,LKL 中的 Linux 核心在執行緒 1 中運行。__switch_to
函式被呼叫,經由 sem_up
切換到執行緒 2,然後執行緒 1 使用 sem_down
自行阻塞。在此之前,執行緒 1 將其 task_struct
指標保存到全域變數 abs_prev
中,該變數對所有 __switch_to
函式呼叫都是通用的。結果,__switch_to
函式將返回指向執行緒 1 的 task_struct
的指標。假設在後續工作中,切換到執行緒 3,並且執行緒 3 在其 __switch_to
函式呼叫中喚醒執行緒 1。這樣,函式返回指向執行緒 3 的 task_struct
的指標,而此時執行緒 1 正在執行。
因此,當內部執行緒排程器需要切換到新執行緒時,它會關閉目前執行緒的外部 semaphore,並打開所需執行緒的外部 semaphore。由於所有其他執行緒都被 semaphore 封鎖,主機系統排程器只能啟動內部 LKL 執行緒排程器所需的那一個執行緒。如果不這樣做,外部執行緒排程器可以按照其邏輯啟動執行緒,內部 LKL 執行緒排程器將失去對執行緒啟動順序的控制,破壞 Linux 核心的內部邏輯。與此同時,內部 LKL 層如常運行,封鎖同步的執行緒等。內部 LKL 執行緒排程器根據從其核心接收到的執行緒封鎖/啟動請求打開和關閉外部 lock。
LKL for device drivers, disk filesystems, and network stack.
Linux compatibility environment based on a library/loader and several special filesystems.