nabla container --- https://www.youtube.com/watch?v=YIsM0zoRzrE https://github.com/nabla-containers/runnc 影片逐字稿 各位好,感謝大家來聽我們的演講。我是 Brandon Lum,在我旁邊的是 Ricardo Koller,我們都來自 IBM Research。 今天我們要討論的是 Nabla 容器——一種容器隔離的新方法。 那麼,我們首先要從一個前提開始:容器其實並沒有被安全地隔離。 這究竟是什麼意思呢? 為什麼虛擬機(VM)被認為是安全的,而容器卻不是呢? 那我們要如何改進容器的隔離性呢? 以下是我們演講內容的概覽。 首先,我們會討論針對隔離方面的威脅模型。 接著,我們將討論如何透過減少攻擊面來實現隔離。 之後,我們會介紹我們的方法——Nabla。 接下來,我們會談談如何衡量隔離程度。 最後,我們會比較 Nabla 和虛擬機的隔離效果。 那麼,「被隔離」意味著什麼? 位於同一主機上的容器彼此之間不應能夠存取對方的資料。 有一種情境是水平攻擊,從某個易受攻擊的服務跨到另一個容器。 另一種場景是原生容器的多租戶雲環境。 現在,我們來談談容器隔離的現實情況。 容器基本上僅僅是具備名稱空間隔離的進程,這表示大多數針對內核的利用攻擊在容器中仍然有效。 例如,2018 年 9 月出現了一個漏洞(CVE-2018-14634)。 還有著名的 DirtyCOW 攻擊(CVE-2016-5195)。 還有許多類似漏洞存在(在 CVE 資料庫中可查到)。 僅在 2018 年,就有 3 個程式碼執行漏洞和 8 個記憶體損毀漏洞影響到內核。 因此,在共用的高權限組件——也就是內核——上,確實可能發生前述水平攻擊。 舉個例子,我們快速看看 DirtyCOW 漏洞是如何利用的。 DirtyCOW 是 Linux 核心中一個知名的漏洞利用案例。 這個漏洞利用的手法是:首先在記憶體中映射一個頁面。 然後,一個執行緒反覆對該頁面呼叫 madvise。 另一個執行緒同時透過 procfs 不停地讀寫該頁面的內容。 上述操作會在內核的記憶體管理代碼中觸發競態條件。 我們更深入地檢視了這樣的現實狀況。 漏洞利用通常透過系統呼叫來針對內核中脆弱的部分下手。 如果我們限制應用程式可使用的系統呼叫數量,那麼可到達的內核函式就會減少。 可到達的內核函式變少,也就意味著潛在漏洞變少。 這就代表可能被利用的攻擊也更少。 Docker 本身提供了預設的 seccomp 安全策略。 但 Docker 的預設政策只封鎖了 300 多個系統呼叫中的約 44 個。 而一般性的 seccomp 策略要做到真正安全是很困難的。 通常,我們對系統呼叫的分析與篩選多半依賴啟發式方法。 我們的方法稱為 Nabla,它採用確定性且通用的策略。 我們實施了一套既確定又通用的 seccomp 政策。 該策略僅允許使用 7 個系統呼叫。 而這是透過使用 LibOS 技術來實現的。 基本上,我們將 Unikernel 理念引入到了容器中。 我們利用了 Rumprun 和 Solo5 社群的工具與技術。 我們對 unikernel 進行了修改,使其能夠作為一個進程來運行。 要製作一個 Nabla 容器,我們目前需要針對應用使用一套自訂的建構流程。 透過這套特製的建構流程,會產生一個 Nabla 執行檔,其中已包含應用程式、LibOS 等所有組件。 然後,我們有一個名為 runnc 的執行時,用來載入這些 Nabla 二進位並設置對應的 seccomp 安全設定。 runnc 可以與容器執行環境整合(例如 Kubernetes 的 containerd)。 現在,我們準備進行現場示範。 在這個示範中,我們會將幾個應用程式作為 Nabla 容器來執行。 首先,我會在普通的 Docker 容器中運行一個 Node.js Express 應用,並追蹤它的系統呼叫。 我們可以看到,它使用了大約 8 種不同的系統呼叫。 接下來,我使用 runnc 將同一個應用作為 Nabla 容器來運行,並再次追蹤其系統呼叫。 現在透過 strace 可以看到,它只使用了 2 種不同的系統呼叫。 我們也用 ftrace 量測一下觸及了多少內核函式。 在標準容器中,應用執行期間觸及了數以百計的內核函式。 而在 Nabla 模式下,只觸及了幾十個內核函式。 我們測量了幾種應用在 Docker 與 Nabla 環境下所使用的唯一系統呼叫數量。 例如,Node.js Express 在 Docker 上用了大約 7~8 個系統呼叫,但在 Nabla 下僅用到 2 個。 再比如 Redis,在 Docker 環境可能用到 8 個左右的系統呼叫,而在 Nabla 下約為 3 個。 我們測試的 Python Tornado 框架在 Docker 上用了大約 8 個系統呼叫,在 Nabla 下只有 2 個。 我們同時利用 ftrace 測量了所訪問的唯一內核函式數量。 在 Docker 下,Node.js Express 觸及了約 500 個內核函式,而在 Nabla 下大概只有 50 個。 Redis 和 Python Tornado 也可以看到類似幅度的減少。 我們還與 Kata Containers 進行了比較——Kata 採用虛擬機技術來實現容器沙盒。 這張圖顯示的是針對 Redis 工作負載的 ftrace 測量結果。 紅色柱代表 Docker,黃色柱代表 Kata,藍色柱則是 Nabla。 在這項測試中,Nabla 觸及的內核函式數是最低的,甚至比 Kata 還更少。 那麼,這對於我們的隔離與虛擬機相比意味著什麼呢? 這些結果顯示,我們或許已經能達到接近虛擬機的隔離效果,至少在內核攻擊面這個角度上是如此。 那麼,我們是否已經超越了虛擬機的隔離能力呢? 實際上,我們在 HotCloud 2018 大會上發表的論文《Say Goodbye to Virtualization for a Safer Cloud》中就探討了這個問題。 或許是超越了,不過這其中有幾個但書需要注意。 存在一些問題,例如:我們比較的對象僅限於 KVM 嗎?換成其他虛擬機管理程式結果會如何? 那如果我們考慮硬體層級的威脅(例如 Spectre 或 Meltdown)又會怎樣? 還有其他指標需要考量,而不僅僅是攻擊面一項。 展望未來,Nabla 下一步的計劃是什麼? 我們希望能夠讓社群參與進來。 我們正持續開發 runnc、nabla-base-build 以及各種 Nabla 示範應用。 我們的一個目標是消除對自訂建構流程的依賴,例如透過支援 LibOS 的動態連結來達成。 我們也希望創建新的基底映像,並支援更多程式語言的應用。 我們也邀請社群一起協作改進安全分析方法和評估指標。 我們在 GitHub 上有一個名為 nabla-measurements 的倉庫,歡迎大家在那裡做出貢獻。 謝謝大家! 您可以在我們的網站 nabla-containers.github.io 上找到更多資訊。 歡迎隨時聯絡我——我是 Brandon Lum,Twitter 帳號是 @lumjjb,或者發郵件到 brandon.lum@ibm.com。 也可以在 Twitter 上關注 #NablaContainers 話題以獲取最新動態。 正如投影片所示,已有一些媒體報導和社群討論對我們的工作表示關注。 再次感謝各位,現在我們願意回答大家的提問。 實驗 --- 由於我的系統是Linux mint 21.3,對於編譯nabla-base-build會有問題 所以我在ubuntu:18.04的docker裡面編譯,而且編譯時需要用到docker build, 所以我在docker參數加了--privileged,才能做Docker in Docker ``` # 1. 先抓原始碼並同步子模組 git clone https://github.com/nabla-containers/nabla-base-build.git cd nabla-base-build git submodule update --init --recursive # 這一步沒有做就會編不動 # 2. 安裝編譯相依套件 sudo apt-get update sudo apt-get install -y zlib1g-dev libseccomp-dev \ build-essential git docker.io genisoimage sudo python \ golang-go docker.io genisoimage jq libseccomp-dev \ build-essential wget curl # 3. Rumprun 舊版只吃 GCC 5/6,較新的 GCC 7+ 會失敗 sudo apt-get install -y gcc-6 g++-6 export CC=gcc-6 # 編譯前先指定 # 原本網址的檔案失效了, 改成其他的 sed -i 's|ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre|https://ftp.exim.org/pub/pcre|' \ rumprun-packages/pcre/Makefile # 開始編譯 make world -j12 # 這以上花了非常多時間try and error, 因為實驗環境有很多過時的東西 ``` 參考 https://cloud.tencent.com/developer/article/1470875 發出http request測試nabla和legacy的nodejs server的效能 nabla ``` root@a932768c39c3:/nabla-demo-apps/redis-test# wrk -t4 -c100 -d30s http://localhost:8081 Running 30s test @ http://localhost:8081 4 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 85.77ms 54.19ms 154.26ms 44.72% Req/Sec 213.43 131.99 420.00 71.43% 161 requests in 30.00s, 32.86KB read Socket errors: connect 0, read 100, write 5802185, timeout 0 Requests/sec: 5.37 Transfer/sec: 1.10KB ``` legacy ``` root@a932768c39c3:/nabla-demo-apps/redis-test# wrk -t4 -c100 -d30s http://localhost:8082 Running 30s test @ http://localhost:8082 4 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 8.83ms 2.88ms 109.73ms 98.98% Req/Sec 2.87k 211.38 3.31k 77.00% 343359 requests in 30.01s, 68.44MB read Requests/sec: 11440.72 Transfer/sec: 2.28MB ``` nabla版的latency明顯比legacy高, 而且用node-express-nabla,會直接閃退 測試system call nabla ``` CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' nabla) sudo strace -f -tt -e trace=all -o nabla_strace.log -p $CONTAINER_PID awk -F"(" '{print $1}' nabla_strace.log | awk '{print $NF}' | sort | uniq -c | sort -nr 15368 clock_gettime 3411 ppoll 2241 read 242 write 1 SIGSEGV 1 --- ``` legacy ``` CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' legacy) sudo strace -f -tt -e trace=all -o nabla_strace.log -p $CONTAINER_PID awk -F"(" '{print $1}' nabla_strace.log | awk '{print $NF}' | sort | uniq -c | sort -nr 160036 epoll_ctl 159935 read 159834 writev 1603 epoll_wait 102 accept4 101 close 78 mmap 32 futex 30 munmap 17 0 9 madvise 5 209 2 40 1 mprotect 1 0x683dbf51000 ``` nabla systam call比legacy的少很多,不過不確定是因為nabla閃退, 所以system call比較少 nabla-linux --- https://www.youtube.com/watch?v=gzxXM8cADRQ https://github.com/nabla-containers/nabla-linux 影片逐字稿 哈囉各位,非常感謝大家來參加我的演講。 我叫 Ricardo Koller,來自 IBM 研究部門。 今天我想談談我們一直在研究的一項成果,我們稱之為「最輕量的虛擬機監控器就是根本沒有監控器」。 (笑)是的,標題有點長,但基本上意思是:如果我們可以在沒有傳統 VM 監控器的情況下運行虛擬機會怎樣呢? 那麼,嗯,首先我先介紹一些背景。 各位很多人都知道,所謂的管理程式或者虛擬機監控器(VMM)是一個軟體層,位於硬體和虛擬機之間。 VMM 負責的是創建虛擬機、隔離它們,以及攔截特權操作等工作。 然而,無論我們將它做得多麼輕量,這個額外的層仍然會帶來額外的負擔。 它帶來了額外的上下文切換、中斷陷阱處理、設備模擬等等。 所以我們一開始提出的問題是:我們能讓虛擬化達到的絕對最低額外負擔是多少? 而我們想到的一個極端答案就是:完全消除 VMM。 換句話說,就是在完全沒有管理程式的情況下運行虛擬機——因此才說「完全沒有監控器」。 現在,你們可能會想:「這樣可行嗎?」 沒有 VMM 怎麼能有 VM,對吧? 嗯,事實證明是有辦法的,我們使用了一種基於所謂使用者模式 Linux (User-Mode Linux, UML)的方式。 使用者模式 Linux(UML)其實已經出現很久了。 基本上,UML 就是將 Linux 核心移植成為一個普通的使用者空間行程,在主機的 Linux 系統上運行。 也就是說,你可以把一個 Linux 核心在 Linux 上作為一個行程啟動——這很像一個虛擬機,但它不需要像 KVM 或 Xen 那樣的管理程式。 理論上,這給了我們隔離性(因為 UML 核心只是主機作業系統用常規行程隔離機制隔離的一個使用者行程),而不需要一個特殊的 VMM。 因此,UML 利用主機核心本身來實現資源控制和隔離,以達到虛擬化的目的。 但傳統 UML 在性能上有一些嚴重的缺點。 舉例來說,在 UML 內(也就是「客體」)的一個行程所做的每一次系統呼叫都必須被捕捉並特別處理。 在經典的實作中,UML 使用 ptrace 機制攔截系統呼叫。 基本上,ptrace 允許一個行程(UML 核心)控制並監視另一個行程(UML 中的使用者行程),以捕捉那些系統呼叫。 這方法雖然可行,但真的非常慢。 UML 使用者程式的每一次系統呼叫都因 ptrace 而造成一次上下文切換,帶來巨大的開銷。 如果你過去曾經跑過 UML,你可能記得它比在相同硬體上跑普通 Linux 要慢很多。 所以我們的工作就是要消除那種額外負擔,使 UML 成為一個可行的高性能「無管理程式」虛擬化解決方案。 好的,那麼我們到底做了什麼? 我們為 UML 實現了一種新模式——我們稱之為「無記憶體管理單元模式(No-MMU 模式)」的 UML。 在這種模式下,UML 核心的編譯是不假定有記憶體管理單元的。 這意味著 UML 核心及其使用者行程本質上共享相同的位址空間。 因此 UML 核心可以直接存取其使用者行程的記憶體。 而這讓我們能夠避免使用 ptrace 來處理系統呼叫。 取而代之的是,我們建立了一個特殊的系統呼叫處理機制。 具體來說,在 UML 核心啟動過程中,我們在一個固定的位址(我們這裡是 0x0 位址)安裝了一小段跳板(trampoline)程式碼。 這段跳板程式碼基本上攔截 UML 使用者程式所做的系統呼叫。 當 UML 中的一個使用者程式發出一個系統呼叫(比如 open() 或 read())時,它並不透過 ptrace,而是實際跳轉到這個跳板程式碼。 這個跳板程式碼然後將流程直接重導到 UML 核心自身的系統呼叫處理函式中。 所以實際效果是,UML 核心可以直接呼叫其系統呼叫,而不需要透過 ptrace 進行核心/使用者模式的轉換。 結果就是每次系統呼叫的額外負擔大幅降低。 事實上,在我們的原型中,UML 程式的大多數系統呼叫執行速度幾乎跟在原生系統上一樣快。 嗯,給大家一些具體的數字,我們跑了一些 lmbench 的微基準測試。 例如空系統呼叫的開銷、上下文切換時間等等。 與傳統 UML 相比,我們的無 MMU UML 在這些指標上快了一個數量級左右。 舉例來說,在經典 UML 中,由於 ptrace 導致空系統呼叫延遲是幾微秒,而用我們的方法則降低到接近不到一微秒。 基本上,這已經和原生 Linux 的系統呼叫開銷非常接近了。 所以這是一個巨大的改進。 現在你可能會問:我們放棄了什麼?天下沒有白吃的午餐,對吧? 這裡主要的權衡在於記憶體隔離。 因為在無 MMU 模式下,UML 核心和它的使用者行程共享同一個位址空間,「客體核心」和「客體使用者」之間的隔離就較弱。 在帶管理程式的正常 VM 甚至普通 UML 中,客體核心不能隨便存取使用者程式的記憶體,除非通過適當的機制。 而在我們這裡,由於它們共享記憶體,如果 UML 核心有個 bug,可能會破壞使用者行程的記憶體,反之亦然。 不過重要的是,這並不會危及到主機系統的安全。 從主機的角度來看,所有這些都發生在一個使用者空間的行程中。 所以即使 UML 核心和其內的行程互相搞亂了,它們仍然和真實的主機核心隔離開。 換句話說,「虛擬機」可能會崩潰,但不會崩潰你的主機。 所以我們基本上是改變了某些隔離邊界:我們犧牲了 VM 內部核心與使用者的隔離,以移除管理程式的負擔,同時保留了 VM 與主機之間的隔離。 對於很多使用情境來說,這其實是可以接受的。 例如,如果你是該 VM 的唯一使用者(不存在不受信任的多租戶情況),而你只想要非常快速的虛擬化,用於測試或某些沙盒,那這種方式就非常適合。 另一個場景是:容器隔離。 人們會使用 VM 來隔離容器以增強安全性(比如 Kata Containers、Nabla 容器等)。 在那些情況下,通常是每個 VM 基本上對應一個「應用」。 所以在 VM 內缺少多個使用者行程之間的強隔離並不算大問題,因為在那個 VM 裡基本上就只有一個行程或一組固定的行程。 因此,像我們這樣的解決方案可以用非常低的額外負擔來運行那些容器工作負載。 我們實際上做了一些實驗,在 UML 裡面跑 Docker 容器,來看看效能表現如何。 結果是很有前景的——CPU 和記憶體操作幾乎達到原生效能,I/O 方面有一些開銷,但比起完整的管理程式要小得多。 I/O 有點棘手,因為目前我們的方法仍然使用主機的系統呼叫來進行 I/O,這些會經過主機核心。 比如,在 UML VM 裡的一次磁碟讀取會轉化為主機上的一次磁碟讀取。 在那裡我們沒有消除開銷——這跟任何 VM 的情況類似,只是不需要管理程式層去模擬磁碟而已。 因此 I/O 效能大致上可與其它輕量虛擬化方案(例如在 KVM 中使用 virtio 驅動程式)相媲美。 真正的大獲益是在 CPU 和記憶體操作方面——主要就是系統呼叫、上下文切換這類的負擔上。 所以技術部分總結一下: 我們透過利用無 MMU 模式的 UML,去除了獨立的管理程式/VMM。 我們安裝了一個系統呼叫攔截跳板,來避免 ptrace 並將系統呼叫直接導入 UML 核心。 相比傳統 UML(進而相比基於管理程式的 VM),我們在大量系統呼叫的操作上獲得了一個數量級的性能提升。 我們為了達成這一點,犧牲了一些 VM 內部行程之間的隔離。 但對於許多情境而言,這種取捨是可以接受的。 好的,那麼從大的角度來看,這意味著什麼呢? 這意味著虛擬化不一定總是需要一個龐大的專用管理程式。 我們可以透過在主機作業系統上使用巧妙的技術來獲得 VM 的隔離性和大部分優點。 以我們的例子來說,主機 Linux 一人分飾兩角:它既是主機,同時部分地充當了「管理程式」。 但它並不是傳統意義上的管理程式——我們沒有添加新的層,我們只是重新利用了現有的 Linux 機制來隔離一個客體核心。 我們演講的標題有點戲謔,顯然我們確實有東西在扮演監控的角色,但它是如此輕量,以至於基本上算不上是一個獨立的監控器。 最輕量的監控器就是沒有監控器。 而我們多少是透過使用 Linux 本身達成了這點。 在結束之前,我想感謝我的合作者和社群。 這是我們 IBM 研究部門這邊的團隊合作的結果,我們也從 Linux UML 社群獲得了幫助。 其中一些想法最初在 Linux Plumbers 會議上展示過,我們在那裡收到了很棒的反饋。 另外也非常感謝正在審查我們提交給上游 UML 補丁的維護者們。 我們希望這個功能能被合併進主線,這樣大家只需用主線核心就可以輕鬆體驗無監控器的 VM。 好的,我想我要在這裡做個總結了。 關鍵結論是:在沒有傳統管理程式的情況下運行 VM 是可行的,而且仍然可以獲得非常好的效能。 透過利用現有的作業系統功能,我們能夠消除大量的額外負擔。 這可能為我們部署隔離工作負載的方式開創新的可能性。 所以,非常感謝各位的聆聽! 我很樂意回答大家的問題。 (觀眾鼓掌) 觀眾提問:[聽不清楚] …好的,我來重複一下問題:問題是,如果客體核心是惡意的,那移除監控器是否會對安全性產生影響? 這是個很好的問題。 如我剛才提到的,主機依然是受保護的,因為 UML 核心是以非特權行程的方式運行。 即使一個惡意的客體核心充其量只能嘗試透過系統呼叫來利用主機,但除了普通程式的權限之外,它沒有任何特殊權限。 至於 VM 內部,是的,如果客體核心被攻破,它可以更容易地攻擊 VM 內自己的使用者行程,因為它們共享位址空間。 不過那種情況有點像單一作業系統的情境——如果你的核心被攻破,在其下運行的所有行程都被攻破了。 所以從那個角度來看,它其實並沒有比標準的單一 Linux 系統更糟糕。 只不過在典型 VM 中,你可能會認為客體作業系統是不完全可信的,尤其在雲端多租戶情境下。 對於那類情境,我仍然會使用真正的管理程式。 我們的方法更適用於客體完全在你掌控之下,或者你可以接受那種風險的場景。 也許用這個方式去跑不受信任的程式碼不是最佳主意,但用來跑你信任的一個容器或單一應用應該是沒有問題的。 希望這有回答到你的問題。 提問:[聽不清楚] 問題是問這種方法與 KVM 的輕量模式或像 Firecracker 這樣的其他極小管理程式相比如何。 很好的問題。 比如說 Firecracker,它是一種設計得非常輕量的 microVM,但它底下仍然使用管理程式(KVM)。 Firecracker 移除了很多設備,使用了精簡的 VMM,所以它比通用管理程式快很多,但它並沒有完全移除管理程式。 而我們的方法如我們一直討論的,是完全移除了管理程式。 所以理論上,我們可以更加輕量。 不過,有一方面 Firecracker(和類似方案)可能仍然有優勢,那就是強隔離。 它們仍然透過硬體虛擬化在客體和主機之間保持明確的隔離。 因此對於雲端多租戶場景而言,Firecracker 在隔離不受信任的工作負載方面可能更安全。 但如果不需要那種程度的隔離,我們的方法可能提供甚至更低的負擔。 在效能方面,我們相信對於許多操作我們可以媲美甚至超過像 Firecracker 這樣的 microVM,因為我們完全繞過了管理程式層。 但再說一次,這取決於使用案例和信任模型。 如果你信任客體,無監控器方案會很棒。如果你不信任,你可能還是會想要那個管理程式當安全網。 提問:[聽不清楚] 你的問題是關於記憶體佔用,以及不使用管理程式能節省多少記憶體。 是的,這是個不錯的重點。 管理程式確實會消耗記憶體——例如,它們需要執行一份客體 OS 或相關的資料結構來模擬硬體。 在我們的情況下,沒有一個單獨運行的管理程式 OS。 我們只有作為一個行程的 UML 客體核心。 所以記憶體佔用基本上就是客體核心及其行程本身需要的記憶體。 在這之外沒有重複的快取、設備模型等等。 我們還沒有直接比較過與 KVM 的記憶體使用量,但定性來說,它應該更小。 因為使用 KVM 時,你有主機核心加上客體核心(每個都有自己的資料),而使用無 MMU UML 時,我們基本上只有一個核心(UML 核心),主機核心只是做正常的事情,並沒有為每個 VM 存儲很多額外狀態。 所以我預期記憶體開銷會更低,當你跑很多 VM 的時候這會是個大優點。 我們這裡的一個動機其實就是想在沒有傳統 VM 那種沉重負擔的情況下,能夠運行大量隔離的工作負載。 想像一下跑 1000 個微小 VM——使用無監控器的方法可能更可行。 而 1000 個 KVM 實例,每個都有自己的核心和監控器,會吃掉很多記憶體。 所以是的,記憶體效率是另一個優點。 好的,我想我們還有時間再問一個問題… 提問:[聽不清楚] 問題是:這能否在其他主機作業系統上運作,還是特定於 Linux? 目前,它非常針對 Linux,因為它依賴於 Linux 核心的 UML 基礎架構。 UML 是 Linux 的一個功能,所以要在比如 Windows 作為主機上做類似的事情,你需要一個 Windows 核心的移植版能作為行程運行,而據我所知這並不存在。 所以是的,這種方法很大程度上綁定於 Linux 作為主機。 不過考慮到 Linux 無處不在——在雲端、伺服器等——它已經涵蓋了很大一片領域。 我想或許有人可以想像把這個概念移植到其他類 Unix 系統上,如果它們允許類似 UML 的東西,但 Linux 是這個概念大展身手的地方。 好的,我想我們的時間到了。 再次感謝大家的蒞臨和提問! 如果想要進一步討論,會後可以隨時來找我。謝謝!