Hệ điều hành & Tiến trình/Kernel mode vs user mode — ranh giới bảo vệ phần cứng
2/28
Bài 2 / 28~12 phútKernel & System CallMiễn phí lượt xem

Kernel mode vs user mode — ranh giới bảo vệ phần cứng

Vì sao CPU có hai mode đặc quyền, kernel bảo vệ phần cứng khỏi chương trình lỗi/độc hại thế nào, và điều gì xảy ra khi user code làm điều bị cấm.

TL;DR: CPU chạy code ở một trong hai chế độ đặc quyền: user mode (chương trình của bạn — bị cấm chạy lệnh đụng trực tiếp phần cứng) và kernel mode (nhân hệ điều hành — được làm mọi thứ). Một mode bit trong CPU quyết định chế độ hiện tại (trên x86 là CPL trong thanh ghi CS: ring 0 = kernel, ring 3 = user). Khi user code cố chạy lệnh đặc quyền (dừng CPU, cấu hình MMU, đọc cổng I/O) hoặc chạm vùng nhớ kernel, CPU ném fault và trao quyền cho kernel — thường kết thúc bằng SIGSEGV giết tiến trình. Ranh giới này là nền của mọi bảo vệ, cô lập và công bằng trong hệ điều hành.

Bạn viết một chương trình C, gọi fopen, đọc file, in ra màn hình. Chưa bao giờ bạn tự tay điều khiển đầu đọc ổ đĩa hay ghi vào thanh ghi của card màn hình — và nếu bạn thử, chương trình sẽ chết ngay. Nhưng cùng lúc đó, hệ điều hành lại làm những việc này liên tục cho mọi tiến trình. Vì sao cùng một CPU, cùng tập lệnh, mà code của bạn thì bị cấm còn kernel thì được phép?

Bài này giải thích cơ chế phần cứng đằng sau sự phân biệt đó: hai chế độ đặc quyền, cách CPU biết mình đang ở chế độ nào, điều gì xảy ra khi vượt rào, và vì sao thiết kế "hai tầng" này là nền móng của mọi hệ điều hành hiện đại.

1. Analogy — toà nhà văn phòng và phòng máy

Hình dung một toà nhà văn phòng lớn. Nhân viên (chương trình của bạn) có thẻ từ mở được phòng làm việc của chính mình, dùng máy tính trong phòng, uống cà phê ở pantry. Nhưng thẻ đó không mở được phòng máy chủ, tủ điện tổng, hay phòng điều khiển thang máy. Chỉ ban quản lý toà nhà (kernel) mới có chìa khoá vạn năng vào những nơi đó.

Vì sao chia vậy? Vì nếu bất kỳ nhân viên nào cũng vào được tủ điện tổng, chỉ một người bất cẩn (hoặc cố ý phá hoại) là cả toà nhà mất điện. Tách quyền ra khiến sai lầm của một người không lan thành thảm hoạ chung. Nếu một nhân viên cố cạy cửa phòng máy chủ, hệ thống an ninh (CPU) phát hiện ngay và mời anh ta ra khỏi toà nhà (kết thúc tiến trình).

Toà nhàMáy tính
Nhân viên thườngChương trình chạy ở user mode
Ban quản lýKernel chạy ở kernel mode
Thẻ từ phòng riêngQuyền truy cập vùng nhớ của chính tiến trình
Chìa khoá vạn năngQuyền chạy lệnh đặc quyền, đụng phần cứng
Phòng máy chủ, tủ điệnPhần cứng: ổ đĩa, MMU (Memory Management Unit — đơn vị dịch địa chỉ), cổng I/O, bảng ngắt
An ninh mời ra ngoàiCPU ném fault → kernel giết tiến trình
💡 Cách nhớ

CPU có hai chế độ đặc quyền. Code của bạn luôn chạy ở chế độ hạn chế (user mode); chỉ kernel chạy ở chế độ toàn quyền (kernel mode). Muốn làm việc cần đặc quyền, chương trình phải nhờ kernel — đó chính là system call ở bài sau.

2. Hai chế độ đặc quyền của CPU

Privilege mode (chế độ đặc quyền) là một trạng thái của CPU quyết định tập lệnh và vùng tài nguyên mà code đang chạy được phép dùng. Mọi CPU hiện đại (x86, ARM, RISC-V) đều hỗ trợ ít nhất hai mức:

  • User mode (còn gọi là unprivileged mode): chế độ cho code ứng dụng. Chạy được các lệnh tính toán bình thường (cộng, trừ, load/store bộ nhớ của chính tiến trình), nhưng bị cấm các lệnh đụng phần cứng dùng chung.
  • Kernel mode (còn gọi là supervisor mode, privileged mode): chế độ cho nhân hệ điều hành. Chạy được mọi lệnh, kể cả lệnh cấu hình phần cứng.

Kiến trúc cụ thể đặt tên khác nhau nhưng ý tưởng như nhau:

Kiến trúcMức userMức kernelGhi chú
x86 / x86-64ring 3ring 0Có 4 ring (0–3); thực tế OS chỉ dùng 0 và 3
ARM (AArch64)EL0EL1Còn EL2 (hypervisor), EL3 (secure monitor)
RISC-VU-modeS-modeCòn M-mode (machine) cao nhất

Điểm chung: user mode là tập con nghiêm ngặt của những gì kernel mode làm được. Không có lệnh nào user mode chạy được mà kernel mode không chạy được; ngược lại thì có nhiều.

3. CPU biết đang ở mode nào bằng cách nào?

CPU cần một cách để biết ngay lúc này nó đang ở chế độ nào, và một cách để cưỡng chế giới hạn. Cả hai đều nằm trong phần cứng, không phải phần mềm.

3.1 Mode bit — CPU tự nhớ mình đang ở đâu

CPU lưu chế độ hiện tại trong một vài bit trạng thái. Trên x86-64, đó là CPL (Current Privilege Level) — 2 bit thấp của thanh ghi đoạn code CS. Giá trị 0 nghĩa là ring 0 (kernel), giá trị 3 nghĩa là ring 3 (user).

Trước khi thực thi bất kỳ lệnh nào, phần cứng đọc mode bit này. Nếu lệnh thuộc nhóm đặc quyền mà mode bit đang là user → CPU từ chối thực thi và ném fault. Việc kiểm tra do transistor làm, xảy ra ở mọi lệnh, gần như không tốn thời gian — nên không có cách nào "lách" bằng phần mềm.

3.2 Lệnh đặc quyền — thứ user mode không được đụng

Một số lệnh chỉ hợp lệ ở kernel mode. Ví dụ trên x86-64:

  • hlt — dừng CPU cho tới ngắt kế tiếp (chỉ scheduler của kernel mới được ru CPU ngủ).
  • mov cr3, ... — nạp gốc bảng trang (page table) mới, tức đổi không gian địa chỉ ảo. Cho user chạm là phá tan cô lập bộ nhớ.
  • in / out — đọc/ghi trực tiếp cổng I/O phần cứng.
  • lgdt / lidt — nạp bảng mô tả đoạn / bảng ngắt.
  • cli / sti — tắt/bật nhận ngắt (cho user tắt ngắt là treo cả máy).

Ngoài lệnh, ranh giới còn được cưỡng chế ở bộ nhớ: mỗi trang nhớ có một bit "user/supervisor" trong page table. Trang của kernel bị đánh dấu supervisor; user mode chạm vào → page fault. Đây là lý do một con trỏ hỏng trong chương trình của bạn không đọc trộm được dữ liệu kernel — cùng cơ chế bảo vệ, tầng bộ nhớ (liên hệ Course 2 — Bộ nhớ ảo).

flowchart TD
  A["CPU sap chay mot lenh"] --> B{"Lenh dac quyen?"}
  B -->|"Khong (add, mov, load...)"| C["Chay binh thuong"]
  B -->|"Co (hlt, mov cr3, out...)"| D{"Mode bit = kernel?"}
  D -->|"Co (ring 0)"| E["Chay lenh dac quyen"]
  D -->|"Khong (ring 3)"| F["Nem fault -> chuyen quyen cho kernel"]

4. Worked example — chuyện gì xảy ra khi user code vi phạm

Lý thuyết đủ rồi; hãy làm thật. Chương trình C dưới cố chạy lệnh hlt (dừng CPU) — một lệnh đặc quyền — từ user mode:

// priv.c -- thu chay lenh dac quyen tu user mode
int main(void) {
    // "hlt" = halt: chi kernel (ring 0) duoc phep chay lenh nay
    __asm__ volatile ("hlt");
    return 0;
}
Thử đoán trước khi chạy

Trước khi đọc tiếp: theo bạn, khi biên dịch và chạy chương trình này, điều gì xảy ra? CPU có thật sự dừng lại không? Chương trình in ra gì?

Biên dịch và chạy:

$ gcc priv.c -o priv
$ ./priv
Segmentation fault (core dumped)

CPU không dừng. Diễn biến từng bước:

  1. Lệnh hlt được nạp. Phần cứng thấy đây là lệnh đặc quyền.
  2. Mode bit đang là ring 3 (user) → CPU không thực thi, thay vào đó ném một exception gọi là #GP (General Protection fault).
  3. #GP chuyển CPU sang kernel mode và nhảy tới handler của kernel (cơ chế trap, bài 03).
  4. Kernel thấy tiến trình user vừa làm điều bị cấm. Nó gửi tín hiệu SIGSEGV cho tiến trình.
  5. Tiến trình không bắt SIGSEGV → hành vi mặc định là chết, in dòng Segmentation fault (phần core dumped chỉ xuất hiện nếu hệ thống bật core dump qua ulimit -c).

Điểm mấu chốt: user code không thể "trốn" vào phần cứng. Mọi cố gắng vượt rào đều bị phần cứng bắt và giao lại cho kernel xử lý. Kernel giữ quyền định đoạt — ở đây là giết tiến trình phạm luật, chứ không để nó làm hại phần còn lại của hệ thống.

5. Vì sao CPU cần tới hai chế độ?

Ba lý do, theo thứ tự quan trọng:

1. Bảo vệ và cô lập. Nếu mọi chương trình đều chạy toàn quyền, một lỗi con trỏ trong trình soạn thảo văn bản có thể ghi đè bộ nhớ của trình duyệt, hoặc format nhầm ổ đĩa. Ranh giới hai mode giam mỗi tiến trình trong "hộp cát" của nó: nó chỉ đụng được tài nguyên của chính mình, mọi thứ khác phải xin phép kernel. Một tiến trình crash không kéo theo cả hệ thống.

2. Công bằng và điều phối (preemption).cli (tắt ngắt) là lệnh đặc quyền, user code không thể tắt được ngắt đồng hồ (timer interrupt). Nghĩa là dù chương trình của bạn có vòng lặp vô tận, cứ vài mili-giây timer lại ngắt, đưa CPU về kernel, và scheduler có cơ hội chuyển sang tiến trình khác. Không có ranh giới này, một chương trình tham lam sẽ độc chiếm CPU mãi mãi (chi tiết ở module scheduler sau).

3. An ninh. Truy cập phần cứng bị kiểm soát nghĩa là malware ở user mode không tự ý đọc bàn phím của cả máy, không tự ghi vào đĩa vùng của người khác, không tự mở cổng mạng đặc quyền — nó phải đi qua kernel, nơi có thể kiểm tra quyền.

Cả ba đều quy về một ý: kernel phải luôn giành lại được quyền kiểm soát, và user code không bao giờ được ở vị trí phá vỡ điều đó.

6. Pitfall — những hiểu nhầm thường gặp

Nhầm 1 — "Chương trình của tôi đọc file, tức là nó tự đụng ổ đĩa."

✅ Không. Chương trình của bạn gọi hàm thư viện (fread), hàm này phát một system call nhờ kernel đọc đĩa hộ. Bản thân code user chưa từng chạy một lệnh I/O phần cứng nào. Cây cầu qua ranh giới là system call — bài 02.

Nhầm 2 — "Kernel mode và user mode là hai tiến trình khác nhau."

✅ Không phải hai tiến trình, mà là hai chế độ của cùng một CPU khi đang phục vụ cùng một tiến trình. Khi bạn gọi read(), CPU chuyển từ user mode sang kernel mode trong bối cảnh tiến trình của bạn, chạy code kernel, rồi chuyển lại. Đây gọi là mode switch, và nó khác hẳn context switch (đổi hẳn sang tiến trình khác) — phân biệt kỹ ở bài 02.

Nhầm 3 — "Chạy sudo là chạy ở kernel mode."

✅ Không. sudo cho bạn quyền root (uid 0) — một khái niệm của hệ điều hành về danh tính người dùng. Root vẫn chạy ở user mode; nó chỉ được kernel cho phép làm nhiều system call nhạy cảm hơn. Ring 0 (kernel mode) là khái niệm phần cứng, hoàn toàn khác với root.

7. 📚 Deep Dive

📚 Deep Dive — nguồn chính thức

Spec / tài liệu tham khảo:

Ghi chú: OSTEP là nguồn dễ đọc nhất để nắm ý tưởng; Intel SDM là tài liệu tra cứu khô khan nhưng chính xác tuyệt đối cho x86. Với người mới, đọc OSTEP trước là đủ.

8. Liên hệ các bài khác

9. Tóm tắt

  • CPU luôn chạy ở một trong hai chế độ: user mode (hạn chế) hoặc kernel mode (toàn quyền).
  • Một mode bit trong CPU (x86: CPL trong CS, ring 0 vs 3) cho phần cứng biết chế độ hiện tại; việc kiểm tra do transistor làm, không lách được bằng phần mềm.
  • Lệnh đặc quyền (hlt, mov cr3, in/out, cli/sti) chỉ chạy được ở kernel mode; ranh giới bộ nhớ được cưỡng chế thêm bằng bit user/supervisor trong page table.
  • User code chạy lệnh đặc quyền → CPU ném fault (#GP) → chuyển quyền cho kernel → thường kết thúc bằng SIGSEGV giết tiến trình.
  • Ranh giới tồn tại vì ba lý do: bảo vệ/cô lập (lỗi không lan), công bằng (kernel luôn giành lại CPU nhờ timer interrupt), và an ninh.
  • sudo/root là quyền của hệ điều hành (vẫn user mode), khác với ring 0 là khái niệm phần cứng.

10. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao CPU cần một mode bit trong phần cứng, thay vì để hệ điều hành tự kiểm tra quyền bằng phần mềm trước mỗi lệnh?
Nếu việc kiểm tra quyền do phần mềm làm, thì chính phần mềm đó cũng là lệnh chạy trên CPU — và không có gì ngăn user code bỏ qua bước kiểm tra bằng cách nhảy thẳng tới lệnh nguy hiểm. Mode bit đặt việc cưỡng chế xuống phần cứng: trước mỗi lệnh, transistor đọc mode bit và từ chối lệnh đặc quyền nếu đang ở user mode. Vì đây là mạch điện chứ không phải lệnh, user code không có cách nào can thiệp hay vượt qua. Đó là lý do bảo vệ phải bắt đầu từ phần cứng — phần mềm không thể tự bảo vệ chính nó.
Q2
Một chương trình chạy vòng lặp vô tận không có system call nào. Vì sao hệ điều hành vẫn có thể dừng nó lại và cho tiến trình khác chạy?
Vì lệnh cli (tắt nhận ngắt) là lệnh đặc quyền, chương trình ở user mode không thể tắt được timer interrupt. Cứ sau một khoảng cố định (vài mili-giây), đồng hồ phần cứng phát một ngắt, cưỡng chế CPU chuyển sang kernel mode và nhảy vào handler của kernel — bất kể chương trình đang làm gì. Lúc đó scheduler của kernel giành lại quyền và có thể chọn cho tiến trình khác chạy. Nếu user code tắt được ngắt, một vòng lặp vô tận sẽ treo cứng cả máy; ranh giới hai mode chính là thứ đảm bảo điều đó không xảy ra.
Q3
Nếu tiến trình cài một handler bắt SIGSEGV thì chuỗi sự kiện ở section 4 đổi thế nào — có 'cứu' được lệnh hlt để nó chạy không?

Chuỗi vẫn như cũ cho tới bước kernel gửi SIGSEGV: CPU nạp hlt, thấy đây là lệnh đặc quyền ở ring 3, ném #GP, chuyển vào kernel, kernel gửi SIGSEGV. Khác biệt ở bước cuối: vì tiến trình handler, kernel không giết ngay mà chuyển quyền điều khiển sang hàm handler (vẫn chạy ở user mode). Handler có thể in log, dọn dẹp, rồi chủ động thoát.

Nhưng nó không "cứu" được lệnh hlt: lệnh vi phạm chưa bao giờ được thực thi, và địa chỉ trở về mà kernel lưu vẫn trỏ đúng vào lệnh hlt đó. Nếu handler chỉ return bình thường, CPU quay lại chính lệnh hlt → lại #GP → lại SIGSEGV, thành một vòng lặp. Vì thế handler bắt SIGSEGV ở đây gần như luôn kết thúc bằng việc chủ động exit; nó dùng để ghi lại thông tin lúc crash, không phải để "chạy tiếp" lệnh cấm.

Q4
Phân biệt 'chạy sudo (quyền root)' và 'chạy ở kernel mode (ring 0)'. Chúng có phải một không?
Không, đây là hai khái niệm ở hai tầng khác nhau. Root (uid 0) là khái niệm của hệ điều hành về danh tính người dùng: kernel cho phép tiến trình root thực hiện những system call nhạy cảm mà user thường bị từ chối (mount đĩa, bind cổng đặc quyền, đọc file của người khác). Nhưng tiến trình root vẫn chạy ở user mode — nó vẫn phải gọi system call để làm mọi việc đặc quyền. Kernel mode (ring 0) là khái niệm của phần cứng: chỉ code của nhân hệ điều hành mới chạy ở đó. Một hacker chiếm được root vẫn ở user mode; muốn vào ring 0 phải khai thác thêm lỗ hổng trong kernel.
Q5
Vì sao một con trỏ hỏng trong chương trình của bạn không thể đọc trộm dữ liệu của kernel hay của tiến trình khác?
Vì ranh giới hai mode được cưỡng chế cả ở tầng bộ nhớ, không chỉ ở tầng lệnh. Mỗi trang nhớ có một bit "user/supervisor" trong page table; các trang của kernel được đánh dấu supervisor. Khi code user mode (ring 3) cố đọc một địa chỉ thuộc trang supervisor, MMU phát hiện và ném page fault ngay, trước khi dữ liệu được đọc. Tương tự, mỗi tiến trình có không gian địa chỉ ảo riêng, nên con trỏ hỏng cùng lắm chỉ chạm tới vùng nhớ (thường chưa ánh xạ) của chính nó và nhận SIGSEGV, chứ không với tới bộ nhớ tiến trình khác. Đây là cùng một cơ chế bảo vệ, áp ở tầng bộ nhớ ảo.

Bài tiếp theo: System call — cách chương trình xin kernel làm việc

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên