# 期末草記
## The Most Lightweight Virtual-Machine Monitor Is No Monitor at All
### 動機
Docker 容器以高權限運行:在預設設定下,每個容器的 root 使用者會對應到主機的 root 命名空間,並且容器可以呼叫完整的 Linux 系統呼叫集合,因此,若容器被入侵,將會暴露出大量的核心攻擊面。
### 主流解決方案
使用虛擬機監控器(VMM)作為客體核心與主機核心之間的中介層。
* AWS Firecracker(micro-VMs),它將最小化的客體使用者空間(例如 Alpine Linux 或 TinyX)與輕量、基於 KVM 的監控器配對使用。
### What does Nabla do?
* Nabla Containers 將專用的虛擬機監控器(例如 Firecracker)替換為 User-Mode Linux(UML)。
* 由於 UML 本身只是一般的主機進程,Nabla 因此可以完全不需要額外的監控器。
#### How many containers per UML process?
* 原生 UML 為每個客體程序建立一個主機程序。它依賴 ptrace 來攔截系統呼叫並操作 pade tables。
* ptrace 的開銷非常高,因此 Nabla 採用無記憶體管理單元(no-MMU)的 UML 版本,將所有客體程序執行在同一個主機程序中,使用單一的位址空間。
* 優點:大幅降低開銷。
* 缺點:環境相對脆弱,若某個客體程序解引用錯誤指標,就可能會破壞整個 UML 實例。
#### 在無 MMU 的情況下執行所帶來的後果
* `mmap()` 的支援受限;無法實現每個程序的記憶體保護。
* Fork/Copy-on-Write 無法運作,因為 COW 需要硬體層級的 pade-fault。
* 因此 Nabla 禁止呼叫 fork()。
* 僅允許使用 vfork() + execve(),確保子程序在轉換為新程式映像前不會修改共用的位址空間。
#### 安全性與功能性的取捨
* Nabla 相較於一般的 Docker 容器,大幅縮減了系統呼叫介面(Firecracker 也做了相同的事,但使用的是硬體模擬)。
* 然而,它犧牲了通用性:
* 無法進行每個程序的隔離
* 不支援獨立的 fork()
* 必須使用 PIE 程式,對於依賴 MMU 的軟體會出現不相容問題,犧牲通用性
### 實作
Nabla 利用自己調整過得 no-mmu kernel 發起 UML,使用 Alpine linux 作為 rootfs, 注意 Nabla 將一般 libc 改為 mucl c 並有實做自己版本之 busybox,於製作 rootfs 時,替換 Alpine 原生檔案。
## LKL
LKL(Linux Kernel Library)的核心理念是讓 Linux 核心程式碼可以像標準 C 函式庫一樣,被當作使用者空間的函式庫來使用。
藉此,開發者可以重複利用 Linux 核心中已經實作好的加密功能、檔案系統驅動與網路功能。且能夠在非 Linux 的作業系統上執行這些功能。
由於 LKL 不依賴特定執行環境,lkl 架構並未使用任何平台依賴的程式碼。相反開發者需提供一小組環境相關 primitive 的實作。稱這些 primitive 為 native operations,LKL 的 generic 架構層使用它們來建立虛擬機器供 Linux kernel 執行。
開發者透過 LKL system call interface 與 LKL 進行互動,這是一組基於 Linux system call 的 API,確保一套穩定且熟悉的介面。
### Architecture
<img src=https://hackmd.io/_uploads/r1dNLgjZgl.png style="width:400px; height:auto;">
LKL 的使用情境不需完整支援的 primitive,舉例來說,由於開發者僅使用 Linux kernel 執行單一應用程式,不需要實作 user/kernel 或多 user address space 間的記憶體區隔與保護,也因此不需要 userspace 的特性 以及 MMU 實現虛擬記憶體。
#### memory management
由於 LKL 不需要記憶體保護機制,其記憶體管理支援變得非常簡單:LKL 僅需一個供 Linux kernel 配置使用的「physical」memory pool。實際記憶體的保留由應用程式控制,包括配置機制與記憶體池大小。該記憶體池會由 kernel 使用 buddy、SLUB / SLAB / SLOB / SLQB 等演算法進行管理,與一般系統相同。此記憶體池僅用於動態配置的緩衝區與結構,kernel 程式碼與靜態資料則不在此池內,它們由載入 LKL 的外部環境負責管理。
但需注意,有些子系統(如 VFS)需要虛擬記憶體管理支援。幸運的是,Linux kernel 即使在沒有 MMU 的架構(如 Motorola m68k)上也實作了虛擬記憶體 API,因此 LKL 無需實作 MMU 模擬,只需將自身標示為 non-MMU 架構即可。
#### Thread management
即使 LKL 不需支援 user process,但仍需支援 kernel threads。Linux kernel 使用 kernel threads 執行內部工作,如處理 I/O 請求、softirqs、tasklets 或 workqueues。
首先由 LKL 為使用者空間函式庫的角度出法:
對於 host 環境來說 LKL 核心只是一個使用者程式,如 linux kerenl 想要在該執行緒的記憶體空間管理多執行緒,只能切割 host kernel 分配給該執行緒之堆疊,導致每個新執行緒的堆疊大小會隨著 LKL 核心中的執行緒數量成比例地減少,考量到這點,現有機制改為直接與 host kernel 請求執行緒,由 host kerenl 分配堆疊。
* 為此,開發者需提供兩個基本 primitive:建立與終結 thread,當 linux kerenl 需要新的執行緒即呼叫此組 primitive,交由 host 建立。
##### Thread switch
至此,LKL 使用的 threads 是由環境進行排程,但 Linux 本身仍需控制 threads 的排程以確保正確性與效能,LKL 在此應用 `semerphone` + `setjump/longjump` 作為解決方案

* 當 LKL 核心需要新執行緒時,除了與 host kernel 發送建立執行緒的請求外,還需取得該執行緒的 semaphore
* LKL 為實現執行緒管理還需要施加一個特殊限制:在任何給定時刻,LKL 只能在一個執行緒中運行
* 當一執行緒被建立、執行任何 Linux 程式碼前,首先 acquires自己的 semaphore,因其初值為 0,新執行緒將被阻塞。
* 當 Linux scheduler 選出新的 thread 時,會 release 該 thread 的 semaphore ,並 acquire 自己的 semaphore。
* 這種 token-passing 機制保證任一時間點僅有一個 thread 執行,且排程順序完全由 Linux 控制,環境無法干涉
* 需提供 semaphore 支援作為 native operations,包含:配置 semaphore(初值)、釋放 semaphore、以及 up 與 down 操作。
#### IRQ support
LKL 的應用需要與外部裝置互動。例如一個為 Linux ext4 檔案系統設計的驅動,需從磁碟讀取資料。該應用將使用兩個裝置驅動代表該磁碟:一個是 Linux 的 block device driver,另一個是 native kernel 的驅動。
Linux device driver 作為翻譯者,呼叫 native driver 以設定硬體進行 I/O 操作。當硬體完成操作後會產生 native IRQ,由 native driver 處理,並需通知 Linux kernel 該操作已完成。
LKL 的 API 中包含觸發 IRQ 的操作,但需目注意,目前 LKL 的實作尚不支援 SMP 與 preemption。由於 native 環境可能支援 SMP,可能從與 LKL thread 平行執行的 thread 中觸發 IRQ,透過上述我們已經釐清,LKL 假設任何時間點只有一個 LKL 執行緒,故不該直接發起 IRQ handler。
因此需要對 IRQ handler 與 kernel thread 進行序列化。LKL 透過 idle thread 建立一個 IRQ 隊列,並依序處理。
#### idle CPU support
當無任何 thread 可執行時,LKL kernel 支援 “idle thread” 進入等待直到有 IRQ 喚醒,但因其需運作於 user space,無法與一般 linux kernel 一樣使用底層降低 CPU 功耗之指令,但若透過忙等方式模擬 IRQ,將浪費電力與 CPU 資源。
為此,LKL 要求提供兩個 native operations:`enter_idle` 與 `exit_idle`。前者在 idle thread 中呼叫,後者由 IRQ routine 呼叫。典型實作會在 `enter_idle` 呼叫 semaphore down,在 `exit_idle` 中呼叫 up。由於 semaphore 也在其他元件中需要 (執行緒管理),LKL 改為統一要求環境提供基本 semaphore 操作,簡化實作與需求。
#### Timer support
LKL 需要兩個時間相關的 native operations:一是回傳自 Unix epoch 起動後經過的奈秒數;另一是設定於指定奈秒後觸發一個 TIMER IRQ。
### LKL syscall
LKL 對系統呼叫有若干個考量:
* 應用程式可以直接呼叫任何匯出的 kernel function,但這通常是不合適的,因為這會繞過 kernel 中的保護機制,應該只使用公開的 kernel API,也就是 system calls。
* LKL 不支援 SMP,讓應用程式直接呼叫 system call handler 並不合適,因為可能會發生 race conditions
* 例如在 system call handler、kernel threads、IRQ handlers 或其他平行 system call handler 執行間產生競爭。
為解決上述問題,LKL 提供應用程式一組預定義的 system call wrapper 來存取 kernel。
1. 這些函式會觸發一個 "with data IRQ",IRQ 編號設為 IRQ_SYSCALL,而 data 則設為一個包含 system call 編號與參數的 structure。
2. 接著,一個用於存放 system call 結果的欄位會被初始化,並配置一個新的 native semaphore(for caller thread)。這兩者會被存放在和步驟一相同的 structure 中。
3. 最後,呼叫的執行緒 (host thread) 會在該 semaphore 上進入睡眠,直到 system call 的結果就緒。
那麼上述第三點是在等待誰將任務完成? LKL 使用以下機制實現
* IRQ handler 會將所有 system call 請求加入一個 work_queue
* LKL 會執行一個特殊用途的 routine,此 routine 等待 system call work_queue 上的事件
* 對於每個請求,init thread 會呼叫對應的 system call handler,將 handler 回傳的結果填入與該 system call 關聯的 structure 中的 result 欄位,並釋放該 semaphore,以解除原本呼叫 執行緒的阻塞狀態。
* 執行緒被解除阻塞後,system call wrapper 會釋放相關的 structure,並將結果回傳給其呼叫者。
### LKL native operaions
LKL 需要透過 native operations 存取其所處的環境,其開發團隊發現,對於特定環境(例如 Linux user space、Windows kernel 等),實作方式與應用程式本身無關,因此決定將 native operations 的實作納入 LKL 本身
主要需實作以下功能
* 印出訊息至 console(相當於 printk 的功能)
* 配置、釋放、取得與釋放 semaphore
* 建立與銷毀 thread;配置與釋放記憶體
* 取得目前時間;排程未來某一時間點觸發 LKL timer interrupt
1. 在 LKL 架構的底層,定義了一個小型的結構 struct lkl_host_ops,作為當 LKL 需要底層作業系統時的回呼函式:記憶體配置(malloc、free、mmap …)、執行緒建立(pthread_create、CreateThread)、計時器、Console I/O、futex-like primitives 等。
2. 虛擬裝置層 (native stub)
* 為了避免重寫原始的 Linux 裝置驅動程式,LKL 導出了一個虛擬裝置層。
* host 會註冊一組回呼函式(struct virtio_dev_ops)進 native stub,這些函式將每個 LKL device driver 請求對應到主機的原生 API
* 例如對 virtio-net 使用 send()/recv(),對 virtio-blk 使用 pread()/pwrite(),對 virtio-9p 則直接使用 POSIX 檔案呼叫。
* 當 LKL 的某個子系統(如 TCP stack、ext4、9p)使用某個裝置時
* Linux kernel 仍然會發出正常的向 device driver 發出請求(例如從磁碟讀/寫資料)
* device driver 呼叫 native stub 將這些請求轉換給主機處理
* 環境透過 native IRQ 或其他 native 通知機制告知 native stub 該請求已完成
* native stub 透過 LKL IRQ 通知 Linux kernel 該操作已完成
<img src=https://hackmd.io/_uploads/rkgQ_tCWll.png style="width:400px; height:auto;">
#### 實作
LKL 的排程中,根據 `thread_info` 裡的 `TIF_SCHED_JB` 標誌,可以將 LKL 執行緒分為兩類:
* non‐host thread:
LKL 首先建立新執行緒之 `thread_info` 結構用於 LKL 核心排程,皆著透過 `copy_thread` + `thread_create` 創建宿主執行緒(eg. pthread),執行核心內部工作。
* 處理 I/O 請求、softirqs、tasklets 或 workqueues。
* 第 16 行 if 判斷此執行緒是否為「應使用者請求建立」,如為真則跳過執行緒建立,標注存放於 `thread_info` 之 `TIF_HOST_THREAD` 旗幟
* 反之則呼叫 native operation 建立宿主執行緒,並統一設定 `thread_bootstrap` 作為任務進入點
* 第 6 行可以看到執行緒首先會將自己阻塞,直到排程器將 sem 釋放,執行緒完成任務(`f(arg)`)後返回。
```c=
static void thread_bootstrap(void *_tba)
{
// ... 略
int (*f)(void *) = tba->f;
void *arg = tba->arg;
lkl_ops->sem_down(ti->sched_sem);
// .... 略
f(arg);
do_exit(0);
}
int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
{
unsigned long esp = (unsigned long)args->fn;
struct thread_info *ti = task_thread_info(p);
struct thread_bootstrap_arg *tba;
if ((int (*)(void *))esp == host_task_stub) {
set_ti_thread_flag(ti, TIF_HOST_THREAD);
return 0;
}
tba = kmalloc(sizeof(*tba), GFP_KERNEL);
if (!tba)
return -ENOMEM;
tba->f = (int (*)(void *))esp;
tba->arg = (void *)args->fn_arg;
tba->ti = ti;
ti->tid = lkl_ops->thread_create(thread_bootstrap, tba);
// ... 略
return 0;
}
```
* host thread
直接對應到宿主(host)上的原生執行緒,在以下情況被標記為 host thread:
* SYS_CALL (arch/lkl/kernel/syscall.c)
* 當應用程式呼叫任一 `lkl_syscall_*()` wrapper 時,LKL 並不會 fork 出新執行緒;只初始化該執行緒的 `thread_info`(重用當前 host thread),並設定 `TIF_SCHED_JB` ( by `copy_thread()` )。
* 欲排程此類執行緒,wrapper 呼叫 `switch_to_host_task`
* 當 `__switch_to()` 切換到設有 `TIF_SCHED_JB` 標誌之執行緒時,用 `longjmp()` 回到先前在 wrapper 裡的 `setjmp()` 點,呼叫 `run_syscall` 執行系統呼叫並回傳結果。
* 注意流程如上面敘述,LKL 在初始化 `thread_info` 取得該執行緒 semephone,並阻塞直到排程器選擇其作為 next。
* IRQ (待釐清)
* idle_host_task
* 在 `syscalls_init()` 時,LKL 透過 kernel_thread(`idle_host_task_loop`,…) 建立了一個名為 `idle_host_task` 的 host thread,並標記 `TIF_HOST_THREAD`。
* 在沒有其他執行緒 runnable 時,一直 `sem_down()` 在自己的 semaphore 上;而任何需要驅動 LKL scheduler(例如新的 IRQ、workqueue)時,呼叫 `wakeup_idle_host_task()` → `wake_up_process(idle_host_task)`,讓它跑起來處理隊列中的事件。
此函式第 12 行,為標有 `TIF_SCHED_JB` 標誌之執行緒執行 `setjump()` 後將其加入排程
```c=
void switch_to_host_task(struct task_struct *task)
{
if (WARN_ON(!test_tsk_thread_flag(task, TIF_HOST_THREAD)))
return;
task_thread_info(task)->tid = lkl_ops->thread_self();
if (current == task)
return;
wake_up_process(task);
thread_sched_jb();
lkl_ops->sem_down(task_thread_info(task)->sched_sem);
schedule_tail(abs_prev);
}
```
在` __switch_to()` 函式,利用 `test_bit(TIF_SCHED_JB, &_prev_flags)` 決定此次 "switch from" 之執行緒的性質,host thread 則 longjump 回 setpoint 並阻塞,一般核心執行緒則單純阻塞直到下一次被喚醒。
```c
struct task_struct *__switch_to(struct task_struct *prev,
struct task_struct *next)
{
// ... 略
lkl_ops->sem_up(_next->sched_sem);
if (test_bit(TIF_SCHED_JB, &_prev_flags)) {
lkl_ops->jmp_buf_longjmp(&_prev_jb, 1);
} else {
lkl_ops->sem_down(_prev->sched_sem);
}
if (_prev->dead) {
kasan_unpoison_stack();
lkl_ops->thread_exit();
}
return abs_prev;
}
```
#### VirtIO
> 參考
> https://hackmd.io/@sysprog/ryG0h25I0#TODO-%E7%A0%94%E8%AE%80-KVM-Linux-%E8%99%9B%E6%93%AC%E5%8C%96%E5%9F%BA%E7%A4%8E%E5%BB%BA%E8%A8%AD-%E5%92%8C-%E6%89%93%E9%80%A0%E4%BB%A5-KVM-%E7%82%BA%E5%9F%BA%E7%A4%8E%E7%9A%84%E7%B2%BE%E7%B0%A1%E8%99%9B%E6%93%AC%E6%A9%9F%E5%99%A8%E7%AE%A1%E7%90%86%E7%A8%8B%E5%BC%8F%EF%BC%8C%E6%91%98%E9%8C%84-kvm-host-%E9%81%8B%E4%BD%9C%E5%8E%9F%E7%90%86

在 Linux 核心中,VirtIO 介面常用於 KVM 等虛擬化機制,提供虛擬機(guest)與宿主機(host)之間共享資源的通用方式。guest kernel 利用已實作的 VirtIO 驅動,透過操作特定的記憶體區塊(稱為 virtqueue 或 vqueue),來發送請求或讀取資料。
這段記憶體為虛擬機器可存取的共享區域,實際上會被映射至 host 側的地址空間。當 guest 需要使用 host 的裝置(如網路卡、磁碟),便會將請求資料寫入 virtqueue,並透過寫入指定的 MMIO 通知區域(notify region) 來告知 host。host 側接收到通知後,會解析對應的 queue,從共享記憶體中取出資料,並執行對應的裝置操作;處理完成後,再將結果寫回 virtqueue,通知 guest。
guest kernel 會透過 PCI BAR 所註冊的中斷狀態暫存器(ISR, Interrupt Status Register) 接收 host 側傳回的完成通知,進而讀取結果,完成一次完整的通訊流程。
在 LKL(Linux Kernel Library) 的實作中,Virtual-device layer(虛擬裝置層) 便是負責對應 virtqueue 與 host kernel 間的轉換與操作。該層會讀取 LKL driver 所操作的 queue 結構,並轉換為對應的 host 系統呼叫(例如 socket tap 裝置、檔案 I/O 等),最後再將結果寫回 virtqueue,供 guest kernel 使用。
藉由這種架構,LKL 能夠重用 Linux 核心內建的驅動邏輯,並實現在不同作業系統平台上對裝置的統一操作介面,而無需仰賴特定平台的原生驅動支援。
## UML
> 參考
> https://hackmd.io/@sysprog/user-mode-linux-env
### 架構
User-mode Linux 是將 Linux kernel 移植到 userspace 的一個版本。它在 Linux host 上以**一組 processes** 的形式執行一個 Linux virtual machine。一個 UML virtual machine 幾乎能夠執行與 host 相同的一組 processes。以下介紹其實作時的考量
#### 核心模式/使用者模式切換
由於 UML 核心對於宿主來說為一般執行緒,故無法透過硬體判斷是否進入特權模式,因此 UML 採用 `Ptrace system call` 的追蹤機制自行建構這個模式切換。
UML 中有一個特殊的 thread,其主要任務是使用 ptrace 來追蹤幾乎所有其他 threads。當一個 process 處於 user space 時,其 system calls 會被這個 tracing thread 截獲
當它在 kernel 中時,就不會被追蹤 system call。這就是 UML 中對 user mode 與 kernel mode 的區別方式。
從 user mode 切換到 kernel mode 是由 tracing thread 處理的。當一個 process 執行 system call 或接收到 signal 時,tracing thread 若有必要就會強制該 process 轉入 kernel 執行,並讓它在沒有 system call tracing 的狀態下繼續執行。
反過來說,從 kernel mode 回到 user mode 也是由 tracing thread 負責處理,但這次是由 process 主動請求的。當 process 在 kernel 執行完畢(無論是 system call 或 trap 結束)時,它會對自己發送訊號。tracing thread 會攔截這個訊號,必要時恢復 process 的狀態,然後讓 process 在開啟 tracing 的狀態下繼續執行。
#### 系統呼叫
透過以上模式切換機制,能夠模擬系統呼叫 :
當 UML 行程呼叫系統呼叫,此系統呼叫將在 host kernel 中被「取消(annulled)」。這是透過將儲存 系統呼叫編號的 register 改成 `NR_getpid` 來完成的。
當這個動作完成後,tracing thread 會將該 process 的 registers 儲存至 thread structure,並套用一些預先儲存的狀態。這個新狀態會讓 process 開始執行 system call handler,而該 handler 會從儲存的 registers 中讀取 system call 編號與參數,然後呼叫對應的 system call 函式。
當 system call 執行結束後,process 會把 return value 存入其儲存的 registers,然後請求 tracing thread 將它切回 user mode。tracing thread 會恢復之前儲存的 registers,並讓該 process 在啟用 system call tracing 的狀態下繼續執行。
#### Trap & Fault
類似於系統呼叫,當一個 process 收到 signal 時,tracing thread 會比該 process 更早察覺此事件。
當這種情況發生時,該 process 會被轉為在 kernel mode 繼續執行,但不會儲存其執行狀態,也不會套用任何新的狀態。UML 會針對所有重要的 signal 註冊自己的 handler,因此當 process 被繼續執行時,會進入其中一個 UML handler,該 handler 會實作 kernel 對該 signal 的解釋與處理邏輯。
##### 外部裝置中斷(external device interrupts)
在 UML 中是透過 `SIGIO` 來實作的。驅動程式會設置,在每當有輸入抵達時,系統就會產生一個 `SIGIO`。
SIGIO handler 會使用(如`select`)來判斷是哪些 file descriptors 有等待中的輸入。接著,它會根據這些資訊判定每個 descriptor 對應的是哪一個 IRQ。一旦確定對應的 IRQ,它就會呼叫標準的 IRQ 處理程式碼來處理該中斷。
##### 記憶體錯誤(Memory faults)
Memory faults 是透過 SIGSEGV(Segmentation Fault)來實作。
當一個 UML process 發生 無效的記憶體存取(invalid memory access) 時,host 會為它產生一個 `SIGSEGV`。
接著由 UML kernel 中的 SIGSEGV handler 處理這個 signal,並判斷該記憶體存取是否是:
* 合法的存取,但 fault 是因為該 page 尚未被映射進 process 的記憶體空間
* 或是純粹非法的存取(illegal access)
若是第一種情況,會呼叫 generic page fault handler,將新的 page 映射(map)到該 process 的 address space 中。
若是第二種非法存取,則會將 `SIGSEGV` 加入該 process 的 signal queue。當 process 嘗試返回 userspace 時,將會被這個 signal 終止,或者由 process 本身的 signal handler 處理。
#### 虛擬記憶體
目前認為最複雜的部份,UML 也支援虛擬記憶體,維護自已的 page table,與宿主端之 page table 為相異個體。由於 UML 核心與 UML 行程對宿主端而言皆存在於使用者空間,故無法藉由宿主端的虛擬記憶體達成存取控制,維護 UML 自己的 page table 能夠保護 UML 核心 page 遭使用者存取,
當 UML Ghuest 行程發生記憶體錯誤,UML 核心進行對應處理 :
### 實際操作
編譯 UML,基本上與編譯正常核心相似,首先取得 linux kernel 程式碼並進編譯,注意使用 `ARCH=um` 指定 UML
```shell
$ make mrproper
$ make defconfig ARCH=um SUBARCH=x86_64
$ make linux ARCH=um SUBARCH=x86_64 -j `nproc`
```
命令成功即可得到可執行於使用者空間之 linux 核心。
接著須準備此 UML kernel 所需之檔案系統,即 rootfs,這裡其實非常類似contianer 中的 image,可在此階段將所需資源放入,
UML 所使用的檔案系統對宿主 Linux 來說也不過只是單純的檔案,一切都好比置身於保護的 sandbox,經由適當配置,我們大可放心對虛擬機器作任何更動,而不必擔憂損害到真實的硬體與系統。
* 注意,`mount -t hostfs root=/ …` 不會呼叫宿主的 `sys_mount()`。
它只在 UML 核心裡建一顆新的 super-block,然後透過普通 `open()/read()` 去嘗試存取 `/…`(in host)。
* 限制因素其實是「檔案系統權限」:
若以一般帳號執行 `./vmlinux`,就算 guest root 指定 `root=/`,也只能讀那些進程可讀的檔案; `/etc/shadow` 依舊讀不到。
(但一旦使用 `sudo ./vmlinux`,宿主層面變 root ⇒ 讀寫任何檔皆允許。)
* 針對 file system,此使用 mount hostfs 修改 prefix 的方法無法做到檔案系統之隔離,這是因為 uml guest 仍可以透過 mount -t hostfs root=/ 覆寫 prefix.
```sh
make ARCH=um x86_64_nommu_defconfig O=build
scripts/config --file build/.config --set-val CONFIG_STATIC_LINK y
make -j$(nproc) ARCH=um O=build CONFIG_UML_NET_VECTOR=y CFLAGS+="-DCONFIG_UML_NET_VECTOR"
cid=$(docker create \
--entrypoint /bin/true \
ghcr.io/thehajime/alpine:3.20.3-um-nommu)
docker export "$cid" > alpine-nommu.tar
docker rm "$cid"
dd if=/dev/zero of=alpine.ext4 bs=1 count=0 seek=1G
chmod o+rw alpine.ext4
yes | mkfs.ext4 alpine.ext4 || true
mnt=$(mktemp -d)
sudo mount alpine.ext4 "$mnt"
sudo tar -xf alpine-nommu.tar -C "$mnt"
sudo umount "$mnt"
rmdir "$mnt"
./build/vmlinux vec0:transport=tap,ifname=tap100,depth=128,gro=1 ubd0=./alpine.ext4 rw mem=1024m loglevel=8 console=tty init=/sbin/init
Run /sbin/init as init process
with arguments:
/sbin/init
with environment:
HOME=/
TERM=linux
uml-vector uml-vector.0 vec0: tap: using vnet headers for tso and tx/rx checksum
uml-vector uml-vector.0 vec0: netif_napi_add_weight_locked() called with weight 128
(none):/# ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
vec0 Link encap:Ethernet HWaddr C2:70:AB:8E:03:D0
inet addr:192.168.122.2 Bcast:192.168.122.255 Mask:255.255.255.0
inet6 addr: fe80::c070:abff:fe8e:3d0/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:7 errors:0 dropped:0 overruns:0 frame:0
TX packets:15 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:500 (500.0 B) TX bytes:1174 (1.1 KiB)
Interrupt:12
(none):/# ps aux
PID USER TIME COMMAND
1 root 0:00 /sbin/init
2 root 0:00 [kthreadd]
3 root 0:00 [pool_workqueue_]
4 root 0:00 [kworker/R-slub_]
5 root 0:00 [kworker/R-netns]
6 root 0:00 [kworker/0:0-eve]
7 root 0:00 [kworker/0:0H-kb]
8 root 0:00 [kworker/u4:0-ip]
9 root 0:00 [kworker/R-mm_pe]
10 root 0:00 [ksoftirqd/0]
11 root 0:00 [kdevtmpfs]
12 root 0:00 [kworker/R-inet_]
13 root 0:00 [kworker/R-write]
14 root 0:00 [kworker/R-kbloc]
15 root 0:00 [hwrng]
16 root 0:00 [kworker/0:1-eve]
17 root 0:00 [kswapd0]
18 root 0:00 [kworker/R-mld]
19 root 0:00 [kworker/R-ipv6_]
20 root 0:00 [kworker/u4:1-ev]
21 root 0:00 [jbd2/ubda-8]
22 root 0:00 [kworker/R-ext4-]
23 root 0:00 [kworker/0:1H-kb]
30 root 0:00 -/bin/sh
45 root 0:00 ps aux
(none):/# ping -c 3 192.168.122.1
PING 192.168.122.1 (192.168.122.1): 56 data bytes
64 bytes from 192.168.122.1: seq=0 ttl=64 time=0.105 ms
64 bytes from 192.168.122.1: seq=1 ttl=64 time=0.126 ms
64 bytes from 192.168.122.1: seq=2 ttl=64 time=0.126 ms
--- 192.168.122.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.105/0.119/0.126 ms
ps aux | grep vmlinux
leo 38746 0.1 0.0 1065104 39084 pts/1 Sl+ 16:14 0:00 ./build/vmlinux vec0:transport=tap,ifname=tap100,depth=128,gro=1 ubd0=../alpine.ext4 rw mem=1024m loglevel=8 console=tty zpoline=1 init=/sbin/init
leo 38803 0.0 0.0 17820 2152 pts/2 S+ 16:17 0:00 grep --color=auto vmlinux
leo@leo-B850-GAMING-X-WIFI6E:~$ cat /proc/38746/maps
00000000-00001000 --xs 00000000 00:01 10344 /dev/zero (deleted)
60000000-60001000 r--p 00000000 103:04 3451096 /home/leo/linux2025/linux-nommu-z/build/vmlinux
60001000-6045b000 r-xp 00001000 103:04 3451096 /home/leo/linux2025/linux-nommu-z/build/vmlinux
6045b000-605a3000 r--p 0045b000 103:04 3451096 /home/leo/linux2025/linux-nommu-z/build/vmlinux
605a3000-605a6000 rwxp 005a3000 103:04 3451096 /home/leo/linux2025/linux-nommu-z/build/vmlinux
605a6000-605b8000 rw-p 005a6000 103:04 3451096 /home/leo/linux2025/linux-nommu-z/build/vmlinux
605b8000-605c0000 rwxp 005b8000 103:04 3451096 /home/leo/linux2025/linux-nommu-z/build/vmlinux
605c0000-606ba000 rw-p 005c0000 103:04 3451096 /home/leo/linux2025/linux-nommu-z/build/vmlinux
606c0000-606d0000 rw-p 00000000 00:00 0
606d0000-606d8000 rwxp 00000000 00:00 0
606d8000-60713000 rw-p 00000000 00:00 0
60713000-60735000 rw-p 00000000 00:00 0 [heap]
60735000-60c00000 rwxs 00000000 00:01 19484 /dev/zero (deleted)
60c00000-700d3000 rwxs 00000000 00:01 19483 /dev/zero (deleted)
700d3000-700d4000 r-xs 0f4d3000 00:01 19483 /dev/zero (deleted)
700d4000-a0000000 rwxs 0f4d4000 00:01 19483 /dev/zero (deleted)
7ffff6ff7000-7ffff6ff8000 ---p 00000000 00:00 0
7ffff6ff8000-7ffff77f8000 rw-p 00000000 00:00 0
7ffff77f8000-7ffff77f9000 ---p 00000000 00:00 0
7ffff77f9000-7ffff7ff9000 rw-p 00000000 00:00 0
7ffff7ff9000-7ffff7ffd000 r--p 00000000 00:00 0 [vvar]
7ffff7ffd000-7ffff7fff000 r-xp 00000000 00:00 0 [vdso]
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
```
```
# This is a default site configuration which will simply return 404, preventing
# chance access to any other virtualhost.
server {
listen 80 default_server;
listen [::]:80 default_server;
# For demo testing
location / {
root /var/www/html;
index index.html;
try_files $uri $uri/ =404;
}
# You may need this to prevent return 404 recursion.
location = /404.html {
internal;
}
}
~
(none):/# vi /etc/nginx/http.d/default.conf
(none):/# mkdir -p /var/www/html
(none):/# echo '<h1>Hello UML-noMMU!</h1>' > /var/www/html/index.html
(none):/# mkdir -p /run
(none):/# nginx -g "master_process off; daemon off;" &
(none):/# nginx -g "master_process off; daemon off;" &
[1] 46 nginx -g master_process off; daemon off;
(none):/# curl -I http://127.0.0.1
HTTP/1.1 200 OK
Server: nginx
Date: Fri, 27 Jun 2025 12:14:23 GMT
Content-Type: text/html
Content-Length: 26
Last-Modified: Fri, 27 Jun 2025 12:13:03 GMT
Connection: keep-alive
ETag: "685e8acf-1a"
Accept-Ranges: bytes
(none):/# ps aux
PID USER TIME COMMAND
1 root 0:00 /sbin/init
2 root 0:00 [kthreadd]
3 root 0:00 [pool_workqueue_]
4 root 0:00 [kworker/R-slub_]
5 root 0:00 [kworker/R-netns]
6 root 0:00 [kworker/0:0-eve]
7 root 0:00 [kworker/0:0H]
8 root 0:00 [kworker/u4:0-ip]
9 root 0:00 [kworker/R-mm_pe]
10 root 0:00 [ksoftirqd/0]
11 root 0:00 [kdevtmpfs]
12 root 0:00 [kworker/R-inet_]
13 root 0:00 [kworker/R-write]
14 root 0:00 [kworker/R-kbloc]
15 root 0:00 [hwrng]
16 root 0:00 [kworker/0:1-eve]
17 root 0:00 [kswapd0]
18 root 0:00 [kworker/R-mld]
19 root 0:00 [kworker/R-ipv6_]
20 root 0:00 [kworker/u4:1-ip]
26 root 0:00 -/bin/sh
46 root 0:00 nginx -g master_process off; daemon off;
48 root 0:00 [kworker/u4:2-ev]
49 root 0:00 ps aux
```
### Unify LKL to UML
> Unifying LKL into UML
> https://lwn.net/Articles/804177/
根據 RPC 中所描述,其目標為將 LKL 的程式碼整合到 UML 裡面,成為 UML 的一種「library mode」
* 將 LKL 放入 `arch/um/lkl`,並以 `ARCH=um UMMODE=library` 方式編譯,讓 library-OS 功能和現有的 UML 共存於同一套原始碼樹中,允許同一顆 UML 核心下,選擇要跑「完整核心模式」還是「library 模式」。
* 不再使用獨立的 `arch/lkl`,而是把將 LKL 當作 UML 的一種執行模式(mode),以最小改動導入 Linux 主線。
LKL 本身採取最大化保留核心實作的方式開發,此策略即借鑑自 UML:
* 為了減少對 Linux kernel 的侵入式修改,將 LKL 實作為對 Linux kernel 的虛擬架構移植
* 採用 UML 使用 host threads 的方法,而非在 LKL 中實作 micro-threads。
故對於 UML 與 LKL 而言,構築虛擬環境所需的底層元件──如記憶體映射(Memory map)、排程同步(sem_up/down)、VirtIO MMIO、網路、block device等都能共用。整合後能大幅減少重複實作,降低核心持續演進時的維護負擔,LKL 也不再需要自行實作 generic layer。
## TODO
* To find how to implement chroot using uml machanism
* 分析 LKL 與 UML 與 um-no-mmu 執行緒行為
* banchmark: oci runtime spec performance
* why the uml throught put will have latency
## linux KVM
## Question
* 當從部署的角度來看,犧牲通用性似乎難以被市場廣泛接受。現有的 VMM 解決方案(如 Firecracker)已能夠滿足絕大多數的應用需求,因此採用 UML 所帶來的效能提升與實作簡化,是否足以彌補通用性受限所造成的負面影響?
* 在 LKL 對執行緒的排程中,想不通什麼狀況會導致 `lkl_ops->sem_down(_prev->sched_sem);` 以及 `return abs_prev;` 被呼叫,在什麼狀況下為 none Host‐thread ? 已解惑,紀錄於 thread switch
* 同樣在 `__switch_to` line 17 為何必須在喚醒 next 清除旗幟,目前認為就算在 wake 後清理應也可以得到相同結果。
*