Hệ điều hành & Tiến trình/Interrupt, trap & exception — ba con đường vào kernel
4/28
Bài 4 / 28~12 phútKernel & System CallMiễn phí lượt xem

Interrupt, trap & exception — ba con đường vào kernel

Phân biệt interrupt (phần cứng, bất đồng bộ), trap (chủ động — syscall), exception (lỗi — divide by zero, page fault) và cách kernel xử lý từng loại.

TL;DR: CPU rời user mode để vào kernel qua đúng ba con đường, phân biệt theo nguồn phát sinh. Interrupt đến từ phần cứng bên ngoài (đồng hồ, bàn phím, card mạng), bất đồng bộ — không liên quan lệnh đang chạy. Trap là chương trình chủ động vào kernel, đồng bộ — system call chính là một trap. Exception do chính lệnh đang chạy gây ra: chia cho 0, opcode sai, hay page fault. Interrupt và exception đi qua bảng IDT để tìm handler; syscall hiện đại dùng lệnh syscall riêng. Điểm tinh tế: nhiều exception (như page fault khi demand paging) là bình thường — kernel xử lý xong rồi chạy lại đúng lệnh đó, chương trình không hề biết.

bài 02, bạn thấy syscall là cách chủ động chuyển từ user sang kernel mode. Nhưng đó chưa phải con đường duy nhất. Khi bạn gõ phím giữa lúc chương trình đang tính toán, CPU vẫn phải vào kernel để xử lý phím — dù chương trình chẳng gọi syscall nào. Khi code chia một số cho 0, CPU cũng vào kernel — dù bạn không hề muốn. Ba tình huống này — chủ động gọi, phần cứng báo, và lệnh gây lỗi — là ba lối vào kernel khác nhau về bản chất.

Bài này phân loại chúng theo ba trục (nguồn, đồng bộ hay không, kernel làm gì), giải thích vì sao page fault không phải lúc nào cũng là lỗi, và cho bạn tự tay phân loại vài sự kiện.

1. Analogy — ba kiểu "có việc" gọi quản lý toà nhà

Quay lại toà nhà ở bài 01, nơi ban quản lý (kernel) xử lý các sự cố. Có ba kiểu tình huống khiến một nhân viên (chương trình) phải nhờ tới ban quản lý:

  • Chuông báo cháy reo (interrupt): tín hiệu đến từ bên ngoài, bất ngờ, không liên quan việc nhân viên đang làm. Ai đó ở tầng khác bấm chuông. Nhân viên phải tạm dừng, ban quản lý xử lý, rồi mọi người quay lại việc dở.
  • Nhân viên chủ động gọi tổng đài (trap): nhân viên cố ý nhấc máy xin cấp lại thẻ. Đây là điều đã hẹn trước, có quy trình — chính là system call.
  • Nhân viên làm hỏng thứ gì đó (exception): nhân viên vô tình làm kẹt máy photo, hoặc mở nhầm cửa khoá. Hành động của chính họ gây ra sự cố, buộc ban quản lý can thiệp.
Toà nhàMáy tínhBản chất
Chuông cháy reo (từ bên ngoài)Interrupt (timer, bàn phím, mạng)Bất đồng bộ, từ phần cứng
Chủ động gọi tổng đàiTrap (system call)Đồng bộ, cố ý
Tự làm kẹt máy / mở nhầm cửaException (chia 0, page fault)Đồng bộ, do lệnh gây ra
💡 Cách nhớ

Ba lối vào kernel khác nhau ở nguồn: interrupt đến từ bên ngoài (phần cứng), trap do chương trình chủ động gọi, exception do chính lệnh đang chạy gây ra. Trục thứ hai: chỉ interrupt là bất đồng bộ; trap và exception luôn gắn với một lệnh cụ thể.

2. Ba con đường vào kernel

Định nghĩa gọn từng loại:

  • Interrupt (ngắt phần cứng): tín hiệu điện từ một thiết bị bên ngoài CPU — đồng hồ (timer), bàn phím, chuột, ổ đĩa báo "đọc xong", card mạng báo "có gói tới". Nó bất đồng bộ: đến vào bất kỳ thời điểm nào, không liên quan tới lệnh CPU đang chạy. CPU chạy nốt lệnh hiện tại rồi mới xử lý.
  • Trap: một cú chuyển vào kernel do chương trình chủ động phát ra bằng một lệnh đặc biệt. System call là trap phổ biến nhất: bạn cố ý chạy syscall để nhờ kernel. Trap đồng bộ — xảy ra đúng tại lệnh phát ra nó, có thể đoán trước.
  • Exception (ngoại lệ): CPU phát hiện chính lệnh đang thực thi không hợp lệ hoặc cần kernel can thiệp — chia cho 0, opcode không tồn tại, truy cập trang nhớ chưa có mặt. Cũng đồng bộ (gắn với một lệnh cụ thể) nhưng không chủ ý — nó là hệ quả ngoài mong muốn của lệnh.
Thuật ngữ 'trap' bị dùng lẫn lộn

Cảnh báo về từ ngữ: Intel gọi một lớp con của exception là "trap" (loại báo cáo sau khi lệnh chạy xong, như breakpoint). Nhiều sách OS lại dùng "trap" cho mọi lối vào kernel đồng bộ (gồm cả syscall lẫn exception). Trong module này, để rõ ràng, ta dùng: trap = cú vào kernel chủ động do syscall, tách riêng khỏi exception = do lệnh gây lỗi. Khi đọc tài liệu khác, luôn kiểm nghĩa tác giả dùng.

3. Cơ chế — ba trục phân loại

Dựng bảng so sánh theo ba trục độc lập: nguồn, đồng bộ hay bất đồng bộ, và kernel làm gì sau đó.

TrụcInterruptTrap (syscall)Exception
NguồnThiết bị phần cứng bên ngoàiChương trình chủ độngChính lệnh đang chạy
Đồng bộ?Bất đồng bộ (bất cứ lúc nào)Đồng bộ (tại lệnh syscall)Đồng bộ (tại lệnh gây lỗi)
Cố ý?Không liên quan chương trìnhCó, cố ýKhông, ngoài mong muốn
Ví dụTimer, bàn phím, gói mạngread, write, openChia 0, page fault, opcode sai
Kernel làm gìPhục vụ thiết bị, rồi trả về lệnh đang dởChạy handler syscall, trả kết quảSửa được → chạy lại lệnh; không → gửi signal (giết)
Lệnh sau khi vềLệnh kế tiếpLệnh kế tiếp sau syscallThường chạy lại chính lệnh đó (nếu sửa được)

Cả interruptexception đều dùng chung một cơ chế tra bảng: CPU có IDT (Interrupt Descriptor Table) — mảng ánh xạ mỗi số vector (0 = chia 0, 14 = page fault, 32+ = các IRQ phần cứng...) tới địa chỉ handler. Khi sự kiện xảy ra, CPU tra IDT theo vector, chuyển kernel mode, nhảy vào handler.

System call x86-64 hiện đại thì không đi qua IDT: lệnh syscall nhảy thẳng tới một địa chỉ kernel đã ghi sẵn trong một thanh ghi đặc biệt gọi là MSR (Model-Specific Register) — đây là "đường nhanh" thay cho cách cũ int 0x80 (vốn có đi qua IDT). Đó là lý do bài 02 nói syscall tới "một điểm vào cố định".

flowchart TD
  A["CPU dang chay user code"] --> B{"Su kien gi?"}
  B -->|"Tin hieu tu thiet bi ngoai"| C["INTERRUPT<br/>tra IDT theo vector IRQ"]
  B -->|"Lenh syscall (chu dong)"| D["TRAP<br/>nhay diem vao syscall"]
  B -->|"Lenh gay loi (chia 0, page fault)"| E["EXCEPTION<br/>tra IDT theo vector loi"]
  C --> K["Kernel mode: chay handler"]
  D --> K
  E --> K
  K --> R{"Xong -> tra ve dau?"}
  R -->|"Interrupt / Trap"| N["Lenh KE TIEP"]
  R -->|"Exception sua duoc"| S["Chay LAI chinh lenh do"]
  R -->|"Exception khong sua duoc"| X["Gui signal -> thuong giet tien trinh"]

4. Interrupt — bất đồng bộ, từ phần cứng

Đặc trưng khiến interrupt khác hẳn hai loại kia: nó không liên quan lệnh nào đang chạy. Card mạng nhận một gói tin lúc 3 giờ 07 phút 22.000481 giây — đúng lúc CPU của bạn đang cộng hai số trong vòng lặp. Card phát tín hiệu interrupt; CPU chạy nốt lệnh cộng, lưu chỗ đang dở, rồi vào kernel chạy handler để nhận gói. Xong, nó quay lại lệnh kế tiếp trong vòng lặp như chưa có gì xảy ra.

Hai interrupt bạn đã gặp gián tiếp:

  • Timer interrupt: đồng hồ phần cứng phát ngắt đều đặn (thường khoảng 100 tới 1000 lần mỗi giây, tuỳ cấu hình CONFIG_HZ). Đây chính là thứ ở bài 01 cho phép kernel giành lại CPU từ một vòng lặp vô tận. Không có timer interrupt thì không có điều phối preemptive.
  • I/O completion: khi bạn read() từ đĩa và tiến trình bị cho "ngủ" chờ dữ liệu, chính một interrupt từ ổ đĩa ("đọc xong rồi") sẽ đánh thức tiến trình dậy.

Vì interrupt bất đồng bộ, kernel phải xử lý cực nhanh và cẩn thận (không được giả định gì về trạng thái chương trình bị ngắt). Đây là lý do handler interrupt thường rất ngắn, đẩy phần việc nặng cho sau.

5. Page fault có phải luôn là lỗi?

Câu trả lời ngắn: không. Đây là hiểu nhầm phổ biến nhất về exception.

Page fault là exception vector 14: CPU cố truy cập một địa chỉ ảo mà MMU chưa ánh xạ tới trang vật lý. Có hai khả năng hoàn toàn khác nhau:

  • Page fault "bình thường" (major/minor fault): trang hợp lệ nhưng chưa nạp — ví dụ demand paging (trang code chưa đọc từ đĩa), hay copy-on-write (trang dùng chung, giờ mới ghi lần đầu). Kernel lặng lẽ nạp/tạo trang, cập nhật page table, rồi chạy lại đúng lệnh vừa fault. Chương trình không hề biết mình vừa fault — chỉ thấy hơi chậm một chút. Bạn đã học cơ chế này ở Course 2 — Page fault & swap.
  • Page fault "lỗi thật" (segfault): truy cập địa chỉ không hợp lệ — con trỏ NULL, con trỏ hỏng, ghi vào vùng chỉ đọc. Kernel không có cách sửa, nên gửi SIGSEGV; mặc định giết tiến trình với dòng Segmentation fault.

Cùng một cơ chế phần cứng (exception vector 14), hai kết cục trái ngược — điều quyết định là kernel xử lý được hay không. Đây là minh hoạ đẹp cho dòng "Exception sửa được → chạy lại lệnh" trong bảng section 3.

Với exception không sửa được, ví dụ chia số nguyên cho 0:

// divzero.c
#include <stdio.h>
int main(void) {
    int z = 0;
    int x = 10 / z;      // chia nguyen cho 0 -> exception #DE
    printf("%d\n", x);
    return 0;
}
Thử đoán trước khi chạy

Trước khi đọc output: chương trình này in ra gì? Tiến trình có chạy tới printf không, hay chết trước? Nếu chết, kernel gửi tín hiệu nào?

$ gcc divzero.c -o divzero && ./divzero
Floating point exception (core dumped)

Lệnh chia gây exception #DE (Divide Error), kernel gửi SIGFPE, tiến trình chết (phần core dumped chỉ hiện nếu hệ thống bật core dump qua ulimit -c). (Tên thông báo nhắc "floating point" nhưng đây là phép chia số nguyên — một cái tên lịch sử gây nhầm.)

6. Thử sức — tự phân loại

Trước khi đọc đáp án, hãy tự xếp mỗi sự kiện sau vào interrupt / trap / exception, và đoán CPU sẽ quay về lệnh kế tiếp hay chạy lại lệnh cũ:

Thử đoán trước khi mở đáp án
  1. Chương trình gọi open("data.txt", O_RDONLY).
  2. Bạn di chuột trong lúc video đang phát.
  3. Code dereference một con trỏ NULL.
  4. Tiến trình chạm trang stack chưa được cấp, nhưng địa chỉ hợp lệ (stack đang lớn ra).
  5. Đồng hồ hệ thống phát ngắt định kỳ để scheduler chạy.

Đáp án:

#Sự kiệnLoạiQuay về đâu
1open(...)Trap (syscall)Lệnh kế tiếp
2Di chuộtInterrupt (chuột)Lệnh kế tiếp (chương trình chạy tiếp)
3Dereference con trỏ NULLException (page fault, không sửa được)Không về — SIGSEGV giết tiến trình
4Trang stack hợp lệ chưa cấpException (page fault, sửa được)Chạy lại chính lệnh đó sau khi cấp trang
5Timer định kỳInterrupt (timer)Lệnh kế tiếp (hoặc context switch sang tiến trình khác)

Chú ý cặp 3 và 4: cùng là page fault nhưng một cái giết tiến trình, một cái trong suốt — đúng như section 5.

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

Nhầm 1 — "Exception luôn nghĩa là chương trình có bug."

✅ Không. Page fault do demand paging hay copy-on-write là exception xảy ra liên tục trong mọi chương trình khoẻ mạnh; kernel xử lý và chạy lại lệnh, không ai chết. Chỉ exception không sửa được mới thành crash.

Nhầm 2 — "System call và interrupt là một, đều 'ngắt' CPU vào kernel."

✅ Khác nguồn và khác tính đồng bộ. System call (trap) do chương trình chủ động phát, đồng bộ, đoán trước được. Interrupt do phần cứng bên ngoài phát, bất đồng bộ, tới bất kỳ lúc nào không liên quan chương trình. Gộp chúng làm một là mất đi điều quan trọng nhất: ai gây ra và khi nào.

Nhầm 3 — "Sau khi kernel xử lý xong, CPU luôn chạy lệnh kế tiếp."

✅ Với interrupt và trap thì đúng — về lệnh kế tiếp. Nhưng exception sửa được (page fault bình thường) thì kernel chạy lại chính lệnh vừa fault, vì lệnh đó chưa hoàn thành. Nếu chạy lệnh kế tiếp, thao tác bộ nhớ ban đầu sẽ bị bỏ lỡ.

8. 📚 Deep Dive

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

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

Ghi chú: OSTEP đủ cho ý tưởng OS-level; man page signal(7) cho ánh xạ exception → signal trên Linux; Intel SDM cho phân loại phần cứng chính xác.

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

10. Tóm tắt

  • CPU vào kernel qua ba con đường, phân biệt theo nguồn: interrupt (phần cứng ngoài), trap (syscall — chủ động), exception (lệnh gây lỗi).
  • Trục đồng bộ: chỉ interrupt là bất đồng bộ (tới bất kỳ lúc nào); trap và exception luôn gắn với một lệnh cụ thể.
  • Interruptexception tra bảng IDT (vector → handler); syscall x86-64 hiện đại dùng lệnh syscall riêng, không qua IDT.
  • Sau xử lý: interrupt và trap về lệnh kế tiếp; exception sửa được thì chạy lại chính lệnh vừa fault.
  • Page fault không phải luôn là lỗi: demand paging / copy-on-write là page fault bình thường, kernel xử lý trong suốt; chỉ truy cập không hợp lệ mới thành SIGSEGV.
  • Exception không sửa được → kernel gửi signal: SIGSEGV (nhớ sai), SIGFPE (chia 0), SIGILL (opcode sai) — thường giết tiến trình.

11. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao interrupt được gọi là 'bất đồng bộ' còn exception và trap là 'đồng bộ'? Cho một ví dụ cho thấy khác biệt này quan trọng thế nào.

Đồng bộ nghĩa là sự kiện gắn chặt với một lệnh cụ thể và có thể tái lập: chạy lại lệnh đó sẽ lại gây ra nó. Trap (lệnh syscall) và exception (lệnh chia cho 0) đều vậy — chúng xảy ra tại một lệnh xác định. Bất đồng bộ nghĩa là sự kiện tới từ bên ngoài, không liên quan lệnh nào đang chạy và có thể đến vào bất kỳ thời điểm nào — như timer, bàn phím, gói mạng.

Khác biệt này quan trọng vì nó quyết định kernel giả định được gì. Với exception, kernel biết chính xác lệnh nào gây ra và có thể sửa rồi chạy lại. Với interrupt, kernel không được giả định gì về trạng thái chương trình bị ngắt — nó có thể đang ở giữa bất kỳ tính toán nào — nên handler interrupt phải cực kỳ cẩn thận và ngắn gọn.

Q2
Hai lần dereference con trỏ đều gây page fault, nhưng một lần chương trình chạy tiếp bình thường còn một lần bị 'Segmentation fault'. Giải thích vì sao cùng cơ chế mà hai kết cục trái ngược.
Cả hai đều là exception vector 14 (page fault): CPU chạm một địa chỉ ảo chưa được MMU ánh xạ. Điều quyết định kết cục là địa chỉ đó có hợp lệ với tiến trình không. Nếu hợp lệ nhưng trang chưa nạp (demand paging, copy-on-write, stack đang lớn ra), kernel sửa được: nó nạp hoặc tạo trang, cập nhật page table, rồi chạy lại đúng lệnh vừa fault — chương trình chỉ thấy hơi chậm, không biết mình vừa fault. Nếu địa chỉ không hợp lệ (con trỏ NULL, con trỏ hỏng, ghi vào vùng chỉ đọc), kernel không có cách sửa nên gửi SIGSEGV, mặc định giết tiến trình. Cùng cơ chế phần cứng, khác nhau ở chỗ kernel có xử lý được hay không.
Q3
Vì sao sau khi xử lý một interrupt hoặc trap, CPU quay về LỆNH KẾ TIẾP, còn sau một page fault sửa được thì CPU CHẠY LẠI chính lệnh cũ?
Với trap (syscall), lệnh syscall đã hoàn thành nhiệm vụ của nó (nhờ kernel làm việc); quay về lệnh kế tiếp là đúng. Với interrupt, lệnh đang chạy cũng đã chạy xong trước khi CPU xử lý ngắt, nên tiếp tục từ lệnh kế tiếp. Nhưng với page fault sửa được, lệnh gây fault chưa hoàn thành — nó đang cố đọc/ghi một địa chỉ mà lúc đó chưa ánh xạ, nên thao tác bộ nhớ chưa thực sự xảy ra. Kernel chỉ làm cho địa chỉ đó dùng được (nạp/tạo trang), rồi phải để CPU thử lại chính lệnh đó để nó hoàn thành thao tác bộ nhớ. Nếu bỏ qua sang lệnh kế tiếp, thao tác đọc/ghi ban đầu sẽ bị mất.
Q4
Timer interrupt liên quan thế nào tới khả năng kernel dừng một vòng lặp vô tận (nhắc lại từ bài 01)?
Một vòng lặp vô tận ở user mode không gọi syscall nào, nên không tự nguyện trao CPU lại cho kernel. Nhưng timer interrupt là bất đồng bộ và đến từ phần cứng: cứ vài mili-giây, đồng hồ phát ngắt, cưỡng chế CPU chạy nốt lệnh hiện tại rồi vào kernel — bất kể chương trình đang làm gì. Vì lệnh tắt ngắt (cli) là lệnh đặc quyền, user code không chặn được timer. Khi vào kernel qua timer interrupt, scheduler có cơ hội quyết định cho tiến trình khác chạy (context switch). Nói cách khác, timer interrupt là cơ chế phần cứng đảm bảo kernel luôn giành lại được CPU — nền tảng của điều phối preemptive.
Q5
Khi đọc tài liệu, bạn thấy từ 'trap' được dùng với nghĩa khác nhau. Vì sao cần cẩn thận, và trong module này 'trap' nghĩa là gì?
Từ "trap" bị dùng lẫn lộn giữa các nguồn. Intel dùng "trap" cho một lớp con của exception — loại được báo cáo sau khi lệnh chạy xong (như breakpoint, overflow), phân biệt với "fault" (báo trước, chạy lại được) và "abort" (nặng, không phục hồi). Nhiều sách hệ điều hành lại dùng "trap" cho mọi lối vào kernel đồng bộ, gồm cả system call lẫn exception. Vì thế cùng một từ có thể chỉ hai thứ khác nhau tuỳ ngữ cảnh. Trong module này, để tránh nhầm, ta dùng "trap" hẹp lại: cú vào kernel chủ động do system call, và tách riêng "exception" cho các lỗi do lệnh gây ra. Khi đọc tài liệu khác, luôn kiểm xem tác giả định nghĩa "trap" theo nghĩa nào.

Bài tiếp theo: Đọc syscall bằng strace — nhìn chương trình nói chuyện với OS

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