🖥️ OS✍️ Khoa📅 19/04/2026☕ 5 phút đọc

OS & Concurrency: Scheduling và Execution Flow

5. Quan hệ OS Thread ↔ Physical Thread

OS Thread là "công việc cần chạy"
Physical Thread là "máy móc thực thi"
Kernel Scheduler gán OS Thread lên Physical Thread
                    HARDWARE LAYER
┌──────────────────────────────────────────┐
│  CPU Package                             │
│  ┌─────────┐  ┌─────────┐               │
│  │ Core 0  │  │ Core 1  │  ...          │
│  │ [HT0]   │  │ [HT0]   │               │
│  │ [HT1]   │  │ [HT1]   │               │
│  └────┬────┘  └────┬────┘               │
└───────┼────────────┼────────────────────┘
        │
        ▼           OS / KERNEL LAYER
┌───────────────────────────────────────────┐
│  Kernel Scheduler                         │
│  Run Queue:  [T3][T7][T1][T9]...          │
│  Wait Queue: [T2][T5][T8]...              │
└───────────────────────────────────────────┘

Quan hệ một-một tại một thời điểm: 1 OS Thread chạy trên 1 Physical Thread, 1 Physical Thread chạy 1 OS Thread. Nhưng kernel liên tục luân phiên hàng nghìn OS Threads lên ít Physical Threads — đây là bản chất của multitasking.

Thread Affinity — Gắn cứng thread vào core

// Linux: gắn thread chỉ chạy trên Core 2 và Core 3
cpu_set_t cpuset;
CPU_SET(2, &cpuset);
CPU_SET(3, &cpuset);
pthread_setaffinity_np(thread, sizeof(cpuset), &cpuset);

Khi dùng affinity, L1/L2 cache của core luôn "nóng" vì thread không bị migrate sang core khác. Hữu ích cho: real-time systems cần latency cực thấp, game engines (render thread riêng core), databases (tách I/O thread và compute thread).


6. Kernel Scheduler hoạt động như thế nào

CFS — Completely Fair Scheduler (Linux)

Linux dùng CFS từ kernel 2.6.23. Nguyên tắc: mỗi thread được dùng CPU công bằng theo đúng tỉ lệ priority — thread nào "thiếu" CPU nhất thì được chạy trước.

Virtual Runtime (vruntime)

Mỗi OS Thread có vruntime = tổng thời gian đã dùng CPU (điều chỉnh theo priority). Scheduler luôn chọn thread có vruntime nhỏ nhất để chạy tiếp.

Ví dụ:
  Thread A: vruntime = 10ms  (đã chạy nhiều)
  Thread B: vruntime = 3ms   ← được chọn ✓
  Thread C: vruntime = 7ms

  Sau khi B chạy 4ms → vruntime B = 7ms → B và C bằng nhau

Cách này đảm bảo không có thread nào bị starvation — thread ít được chạy sẽ có vruntime thấp và được ưu tiên.

Red-Black Tree — Cấu trúc dữ liệu của run queue

CFS lưu run queue trong Red-Black Tree (cây nhị phân tự cân bằng), sorted theo vruntime:

         [7ms - Thread C]
        /                \
  [3ms - B]           [10ms - A]

→ Thread vruntime nhỏ nhất = nút tận cùng trái
→ Pick O(log n) — cực nhanh dù hàng nghìn threads

Khi nào Scheduler chạy?

Trigger 1 — Timer Interrupt (quan trọng nhất)
  Hardware timer ngắt CPU mỗi ~1-10ms
  → Kiểm tra thread hiện tại đã chạy đủ chưa?
  → Nếu rồi → preempt → schedule thread khác

Trigger 2 — Thread tự block
  Thread gọi: sleep(), read(), recv(), mutex.lock()...
  → Tự rời CPU → Scheduler chạy thread khác ngay lập tức

Trigger 3 — Thread mới được tạo
  → Xem xét có nên preempt thread hiện tại không

Trigger 4 — I/O hoàn thành / Signal
  Thread BLOCKED → chuyển sang READY
  → Có thể preempt thread hiện tại nếu mới hơn và important hơn

7. How Work Gets Done — Từ code đến kết quả

CPU Pipeline

Mỗi CPU instruction đi qua pipeline 4 giai đoạn:

result = a + b

Assembly: ADD rax, rbx

CPU Pipeline:
  Fetch   → Decode   → Execute   → Writeback
  (lấy      (hiểu      (ALU         (ghi
  lệnh)      lệnh)      tính)        kết quả)

Kết quả đi: Register → L1 Cache → L2 Cache → L3 Cache → RAM

Modern CPU có multiple pipeline stages và out-of-order execution — thực tế phức tạp hơn nhiều, nhưng đây là mental model cơ bản.

User Mode vs Kernel Mode

Code bạn viết chạy ở User Mode — không được đụng hardware trực tiếp. Mọi thứ liên quan đến hardware (network, disk, screen) phải đi qua syscall để vào Kernel Mode.

┌─────────────────────────────────────────────┐
│           Application (User Mode)           │  ← code bạn viết
└──────────────────┬──────────────────────────┘
                   │ syscall (open, read, write, send...)
                   │ ~200-400ns overhead mỗi lần
┌──────────────────▼──────────────────────────┐
│            Linux Kernel (Kernel Mode)        │
│   Scheduler | Memory Manager | Network | FS  │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│     Physical Hardware: CPU, RAM, NIC, Disk   │
└─────────────────────────────────────────────┘

Syscall overhead (~200-400ns) là lý do Go và Node.js cẩn thận tránh syscall không cần thiết khi xử lý I/O.

Vòng đời một web request

Theo dõi một request từ đầu đến cuối để thấy tất cả các tầng phối hợp:

1. Browser gửi request
   → OS tạo OS Thread
   → Thread gọi send() syscall → vào Kernel Mode
   → Kernel xử lý network stack → gửi packet
   → Thread bị BLOCKED (chờ response) — CPU được giải phóng!

2. Server response về
   → Hardware interrupt từ NIC
   → Kernel đánh thức thread → READY
   → Scheduler gán Core khi có Core rảnh

3. Thread nhận data → xử lý
   → CPU chạy thật sự (RUNNING state)
   → Render kết quả ra màn hình

CPU Idle — CPU làm gì lúc nghỉ?

Khi không có gì để chạy, kernel chạy "idle thread":

while (true) {
    HLT;  // instruction: nghỉ đến interrupt tiếp theo
}

CPU có C-states để tiết kiệm điện khi idle:

C1: ~1µs để thức dậy   (giảm clock)
C3: ~50µs để thức dậy  (giảm voltage)
C6: ~200µs để thức dậy (gần như tắt core)

Đó là lý do laptop mát hơn và pin lâu hơn khi idle — CPU thực sự ở trạng thái tiêu thụ rất ít điện.