National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 18

虛擬化與容器

一台機器假裝成很多台:虛擬機、namespace 與 cgroup
Interaction實作 30 minute(s)

Learning Objectives

  • 說明 trap-and-emulate 怎麼讓客體作業系統以為自己掌控硬體,以及 Popek 與 Goldberg 的條件
  • 比較 Type 1 與 Type 2 Hypervisor、虛擬機與容器
  • 預測不同 namespace 下同一個行程看到什麼,說明容器為什麼共用核心
  • 用 cgroup 的 memory.max、pids.max、cpu.max 判斷超過限制時會發生什麼
  • 把整個作業系統課程串成一條線,知道接下來可以往哪裡學

作業系統也可以被騙

從第 1 課開始,作業系統都是「掌控一切」的角色:只有它能跑在核心模式,只有它能碰頁表和硬體。

現在有兩個需求:一台實體機器要同時跑 Windows 和 Linux;或是同一台機器上要跑一百個服務,彼此不能互相干擾、也不該互相看到。

第一個需求要讓作業系統以為自己獨佔硬體,這是虛擬機。第二個需求要讓行程以為自己獨佔一台機器,這是容器。兩者聽起來很像,做法與代價完全不同。

虛擬機為什麼可行:trap-and-emulate

Hypervisor(也叫 VMM,虛擬機監視器)是管理虛擬機的軟體。難處在於:客體作業系統(guest OS)會執行特權指令,例如關中斷、改頁表。真的讓它跑在核心模式,它就接管了整台機器。

解法用到第 1 課的機制:

  1. 把客體作業系統放在使用者模式跑,一般指令直接由 CPU 執行,速度不受影響。
  2. 它一執行特權指令,CPU 因為權限不足引發陷阱(trap),控制權回到 Hypervisor。
  3. Hypervisor 模擬這個指令的效果,但只改客體自己看到的那份狀態(例如「客體的中斷開關」),然後讓它繼續。

客體完全不知道這件事,這叫 trap-and-emulate。

Popek 與 Goldberg 在 1974 年的論文(Communications of the ACM)把這個想法寫成條件。他們把指令分成兩類:

  • 特權指令(privileged):在使用者模式執行會引發陷阱。
  • 敏感指令(sensitive):會讀取或改變機器的狀態,或行為依目前狀態而不同。

只要所有敏感指令都是特權指令,就能用 trap-and-emulate 做出有效的虛擬機。 反過來,有敏感指令不會引發陷阱的話,Hypervisor 就看不到客體做了這件事。

早期的 x86 正好不符合這個條件。例如 POPF:Intel 手冊寫明權限不足時不會產生例外,特權位元就是不改變。客體以為關了中斷,其實什麼都沒發生。下面用 Python 模擬這件事。

一個攔不到的指令

cli、sti 是關中斷與開中斷的特權指令。popf_off 代表用 POPF 關中斷。程式裡的指令名稱與流程是教學用的簡化,不是真正的 Hypervisor。注意 guest_if 和 wanted_if 什麼時候開始不一樣。

實作:這台機器能用 trap-and-emulate 嗎

補完 problems:回傳「敏感但不是特權」的指令(排序後的串列)。串列是空的,就符合 Popek 與 Goldberg 的條件。

機器 A、B 是假設的;第三組是 VT-x 之前的 x86 實際的幾個例子(課程講義記載的 popf、sgdt、sidt、sldt、smsw,不是完整清單)。

你的輸出
預期輸出

Hypervisor 的兩型與硬體輔助

x86 不符合條件,早期只能繞路:二進位轉譯(執行前把敏感指令改寫掉)、半虛擬化(修改客體作業系統,讓它主動呼叫 Hypervisor)。2005 年前後,Intel VT-x 與 AMD-V 加入硬體支援:CPU 多了一個給 Hypervisor 用的模式,客體可以跑在原本的特權層,敏感操作由硬體攔下來交給 Hypervisor。之後的 EPT、NPT 再讓硬體直接處理第 14 課的頁表轉換多的那一層,降低記憶體虛擬化的成本。

類型 位置 例子 適合
Type 1(原生) 直接跑在硬體上 VMware ESXi、Xen、Microsoft Hyper-V 資料中心
Type 2(寄宿) 跑在一般作業系統上 VirtualBox、VMware Workstation 開發者的個人電腦

Linux 的 KVM 是核心裡的一個模組,把 Linux 本身變成 Hypervisor,所以不太能明確歸到哪一型。

虛擬機的隔離很強,因為每台都有自己的核心;代價是每台都要帶一整份作業系統,開機要時間、記憶體也要先分出去。

互動練習:虛擬機與容器的結構

把應用程式數量拉高,比較兩邊的方塊怎麼長。記憶體與啟動時間是示意值,重點是量級差異。

容器不是虛擬機

容器沒有自己的核心。容器就是一般的行程,核心替它加了兩種限制:

  • namespace:決定行程看得到什麼。
  • cgroup:決定行程能用多少。

依 man7.org 的 namespaces(7),Linux 有這幾種 namespace:

namespace 隔離什麼
PID 行程編號
Mount 掛載點
Network 網路裝置、協定堆疊、連接埠
UTS 主機名稱
IPC System V IPC 與 POSIX 訊息佇列
User 使用者與群組編號
Cgroup cgroup 根目錄
Time 開機時鐘與單調時鐘

建立的方式是三個系統呼叫:clone(建立新行程時帶 CLONE_NEW* 旗標)、unshare(把自己搬到新的 namespace)、setns(加入既有的 namespace)。每個行程的 /proc/<pid>/ns/ 列出它屬於哪些 namespace,編號相同就是同一個。

下面這支程式印出自己的 cgroup 與 namespace,再用 unshare 建立新的 user 與 PID namespace(unshare 對 PID namespace 不會搬動自己,只影響之後的子行程),然後 fork。真實環境執行的結果:

kernel: 7.0.0-1014-aws
--- /proc/self/cgroup
0::/../../ce-sandbox/NSJAIL.13781
--- /proc/self/ns
pid:[4026532466]
mnt:[4026532461]
net:[4026532468]
uts:[4026532464]
user:[4026532460]
cgroup:[4026532467]
my pid (as I see it): 2
child in new PID namespace: getpid() = 1, getppid() = 0
parent sees child as: 3

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

看兩件事。第一,同一個子行程:父行程看到的 PID 是 3,子行程自己看到的是 1,它的 getppid() 是 0,因為父行程在它的 namespace 之外。第二,這段輸出來自 Compiler Explorer,它本身就把每支程式關在 NSJAIL 沙箱裡,所以 cgroup 路徑寫著沙箱的名字、自己的 PID 也只有 2。你正在看容器技術跑在容器技術裡。

互動練習:站在不同地方看同一台機器

一台主機跑著兩個容器。選你站在哪裡再下指令,也可以關掉容器 A 的某個 namespace,看隔離怎麼消失。這是模擬器,規則依 man page,但 PID、編號、版本都是示意值。

cgroup:能用多少

namespace 只管看得到什麼。一個容器看不到別人,還是可以把整台機器的記憶體吃光。cgroup(control group)負責限制與統計資源。

cgroup v2 是一個檔案系統形式的階層,每個群組是一個目錄,把 PID 寫進 cgroup.procs 就把行程放進這個群組;/proc/self/cgroup 顯示自己在哪裡,v2 的格式是 0::/路徑(上面的真實輸出就是)。

kernel.org 的 cgroup v2 文件規定的語意:

檔案 意思 超過時
memory.max 記憶體硬性上限 回收不了就在這個 cgroup 裡觸發 OOM killer
memory.high 節流用的上限 行程被放慢並承受回收壓力,不會觸發 OOM killer
pids.max 行程數上限 建立新行程回傳 EAGAIN
cpu.max 格式 $MAX $PERIOD,每個週期最多用多少微秒,預設 max 100000 用完就等下一個週期(節流,throttle)
io.max 每個裝置的讀寫速率上限 I/O 被延後

Docker 的選項就是在設定這些:docker run --memory、--cpus、--pids-limit。

互動練習:超過限制時發生什麼

容器 A 跑失控的程式。先不設限制看看,再設上限比較。機器的記憶體大小是示意值,配額的效果由模擬器依 cgroup v2 的語意算出來。

隔離的代價,以及怎麼選

虛擬機 容器
核心 每台自己一個 和主機共用
能跑什麼 任何作業系統 同一種核心上的程式
啟動與額外負擔 要開機、要一整份作業系統 只是開一個行程
隔離邊界 硬體虛擬化,攻擊面較小 系統呼叫介面,攻擊面是整個核心
資源分配 預先切分 cgroup 動態限制

容器的隔離不只靠 namespace 和 cgroup,實務上還會降低權限:capabilities 拿掉不需要的 root 權力,seccomp 過濾能呼叫哪些系統呼叫。

怎麼選?

  • 程式互相信任、要快速部署很多份:容器。
  • 要跑不同的作業系統、要載入核心模組、或是跑不信任的別人的程式(公有雲的多租戶):虛擬機。
  • 兩者常一起用:公有雲給你一台虛擬機,你在裡面跑容器。

第 1 課說系統呼叫是唯一的門。容器把這扇門當成邊界,虛擬機把邊界往下移到硬體指令。邊界越低,要騙的東西越多,但隔離也越強。

確認一下

1.

在 trap-and-emulate 下,客體作業系統執行一般的加法指令,會發生什麼?

2.

一台架構有一個指令:在使用者模式執行時讀到「目前是使用者模式」,不引發陷阱。這對 trap-and-emulate 有什麼影響?

3.

容器裡的行程執行 ps,看到 PID 1 是自己的 web 程式。主機上的 ps 看到這個行程是 2301。下面哪個正確?

4.

容器設了 memory.max,裡面的程式配置超過上限,而且記憶體回收不了。會發生什麼?

5.

要在同一台 Linux 伺服器上讓客戶上傳並執行任意程式,而且客戶彼此不信任。比較合理的是?

第 1 次作答

全軌回顧:作業系統做了哪三件事

十八課走完,可以把它們收成三個問題。(這個分法來自 OSTEP 這本免費教科書:虛擬化、並行、持久性。)

1. 怎麼讓每個程式以為自己獨佔機器?(虛擬化)

課 內容 帶走一句話
1 作業系統與系統呼叫 程式碰不到硬體,系統呼叫是唯一的門
2 行程與狀態 行程是跑起來的程式,CPU 靠內容切換輪流用
3 fork、exec、wait Shell 就是這三個系統呼叫的迴圈
4 管線與訊號 fd 是一層間接,所以能重新導向
5 CPU 排程 周轉時間與回應時間互相拉扯
6 MLFQ 與多核 不知道工作長度,就邊跑邊觀察
12 位址空間與 malloc 每個行程看到的記憶體都是自己的
13 分頁與 TLB 用查表換隔離,用快取補回速度
14 多層頁表與寫入時複製 需要時才配置,fork 才會便宜
15 分頁置換與顛簸 記憶體不夠時,區域性決定一切
18 虛擬化與容器 同一招再用一次,連作業系統都騙

2. 大家一起跑時,怎麼不出錯?(並行)

課 內容 帶走一句話
7 執行緒 共用位址空間,好用也危險
8 競爭條件 測試通過不能證明沒有競爭條件
9 鎖 鎖本身要靠硬體的原子指令
10 條件變數與號誌 條件一定要用 while 檢查
11 死結 實務上最有效的是固定鎖的順序

3. 斷電之後,資料還在嗎?(持久性)

課 內容 帶走一句話
16 I/O 與磁碟排程 看裝置的成本結構決定策略
17 檔案系統與當機復原 讓最後一步成為唯一的決定點

貫穿全軌的是幾個反覆出現的想法:

  • 多一層間接:頁表、fd 表、inode、FTL、namespace,都是在名稱和實體之間加一層,換到隔離與彈性。
  • 用快取補回速度:TLB、頁快取、dentry cache。多一層間接會變慢,就靠區域性補回來。
  • 把機制和策略分開:內容切換是機制,排程演算法是策略;頁表是機制,置換演算法是策略。
  • 沒有免費的東西:每個設計都有代價,要問的是這個情況付得起哪一種。

往下學什麼

  • 動手用容器:Docker 容器軌 把這一課的 namespace 與 cgroup 用起來。
  • 管理真實系統:Linux 軌 練 ps、df、mount、權限與服務。
  • 寫更多系統程式:C++ 軌 的指標、堆積與執行緒,可以看到這些概念在語言裡的樣子。
  • 讀原文:OSTEP(Operating Systems: Three Easy Pieces)作者免費公開,涵蓋本軌的所有主題,附有實作作業。
  • 讀真的核心:xv6 是 MIT 用來教學的簡單類 Unix 作業系統,程式碼短到可以讀完;另外 man 2 fork、man 7 namespaces 這些 man page 就是最準確的文件。

要記得:這一軌的教學核心為了讓你看得見,做了很多簡化(沒有位址隨機化、記憶體模型順序一致、機器指令是簡化過的)。走到真實系統時,以 man page 與實際執行的結果為準。

重點整理

  • 虛擬機靠 trap-and-emulate:一般指令直接跑,特權指令交給 Hypervisor 模擬。Popek 與 Goldberg 的條件是所有敏感指令都要是特權指令;早期 x86 不符合,後來靠硬體輔助解決。
  • 容器是行程加上 namespace(看得到什麼)與 cgroup(能用多少),共用同一個核心。
  • 同一個行程在不同 PID namespace 有不同的 PID;連接埠屬於 network namespace;掛載屬於 mount namespace。
  • memory.max 超過時在 cgroup 內 OOM,pids.max 超過時 fork 失敗,cpu.max 用完就節流。
  • 容器的邊界是系統呼叫,虛擬機的邊界是硬體指令。要跑不信任的程式,邊界要低。

作業系統軌到這裡結束。遇到問題時,回來問那三個問題,通常知道該看哪裡。

這一課完成了!
看我的學習進度
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。