Hệ điều hành & Tiến trình/Tiến trình & PCB — chương trình đang chạy khác gì file đĩa
9/28
Bài 9 / 28~12 phútTiến trình — sinh ra, sống, chếtMiễn phí lượt xem

Tiến trình & PCB — chương trình đang chạy khác gì file đĩa

Tiến trình khác chương trình thế nào, OS lưu gì trong PCB (register, PC, trạng thái) để tạm dừng rồi chạy tiếp, và địa chỉ riêng của mỗi tiến trình.

TL;DR: Một chương trình là file thực thi nằm trên đĩa — dữ liệu tĩnh, chết. Một tiến trình là chương trình đó đang chạy: có bộ nhớ riêng, register, program counter chỉ tới lệnh kế tiếp, và các tài nguyên OS cấp (file đang mở, PID). Cùng một chương trình chạy ba lần tạo ra ba tiến trình độc lập. Để có thể tạm dừng một tiến trình giữa chừng (khi hết lượt CPU) rồi chạy tiếp đúng chỗ sau đó, OS lưu toàn bộ trạng thái của nó trong một cấu trúc gọi là PCB (Process Control Block): register, PC, trạng thái, con trỏ tới không gian địa chỉ, bảng file mở. Mỗi tiến trình có một PCB; đây là "hồ sơ" OS dùng để quản lý sự sống của tiến trình.

Bạn mở ba tab terminal, mỗi tab chạy python. Đó là một chương trình (/usr/bin/python3 trên đĩa) nhưng ba tiến trình — mỗi cái có biến riêng, không thấy nhau. Bạn đang chạy một vòng lặp nặng thì hệ thống vẫn mượt, chuột vẫn nhích: OS liên tục tạm dừng tiến trình của bạn, cho tiến trình khác chạy vài mili-giây, rồi quay lại — mà vòng lặp của bạn không hề "biết" mình từng bị ngắt. Làm sao OS dừng một chương trình giữa chừng rồi chạy tiếp đúng chỗ, như thể chưa từng gián đoạn?

Xong bài, bạn explain được tiến trình khác chương trình ở đâu, và OS dùng cấu trúc PCB nào để chụp lại toàn bộ trạng thái một tiến trình — nền tảng để hiểu mọi thứ còn lại của module.

1. Analogy — công thức nấu ăn và bữa đang nấu

Một công thức trong sách là các bước tĩnh: nằm im, ai đọc cũng ra cùng nội dung, không tự làm gì. Khi bạn thật sự vào bếp nấu theo nó, bạn tạo ra một bữa đang nấu: có nồi cụ thể đang sôi, đang ở bước 3, tay cầm muỗng, nguyên liệu đã dùng một phần. Hai người bạn cùng nấu một công thức tạo ra hai bữa riêng — nồi riêng, tiến độ riêng, không ai đụng nồi ai.

Chương trình là công thức; tiến trình là bữa đang nấu. Và nếu có điện thoại reo, bạn ghi lại "đang ở bước 3, nồi đang sôi lửa nhỏ" để nghe xong quay lại đúng chỗ — mẩu giấy ghi đó chính là PCB.

Nấu ănTiến trình
Công thức trong sách (tĩnh)Chương trình (file trên đĩa)
Bữa đang nấu (nồi, bước, nguyên liệu)Tiến trình (bộ nhớ, PC, tài nguyên)
Hai người nấu cùng công thức → hai bữaChạy chương trình 2 lần → 2 tiến trình độc lập
"Đang ở bước 3, nồi đang sôi"Program counter + register
Mẩu giấy ghi để quay lại đúng chỗPCB (Process Control Block)
💡 Cách nhớ

Chương trình là danh từ (một thứ trên đĩa); tiến trình là động từ hoá (một hành động đang diễn ra). PCB là mẩu giấy OS ghi lại "tiến trình này đang làm tới đâu" để dừng rồi tiếp mà không mất dấu.

2. Chương trình và tiến trình khác nhau ở đâu?

Chương trình là một file thực thi trên đĩa — chuỗi byte gồm mã máy (đoạn .text), dữ liệu khởi tạo (.data), và metadata (định dạng ELF trên Linux). Nó tĩnh: copy đi đâu vẫn thế, không có "trạng thái đang chạy".

Tiến trình là một lần thực thi của chương trình đó. Khi bạn gõ ./app, OS nạp file vào bộ nhớ, dựng không gian địa chỉ, cấp một PID (Process ID — số định danh duy nhất), rồi cho CPU bắt đầu chạy. Từ giây phút đó, nó có những thứ mà file trên đĩa không có:

  • Không gian địa chỉ riêng: vùng code, dữ liệu, heap, stack — thay đổi liên tục khi chạy.
  • Register và program counter: giá trị đang tính, lệnh kế tiếp sẽ thực thi.
  • Tài nguyên OS cấp: file descriptor (số nguyên nhỏ OS cấp làm "tay cầm" tới một file/socket/pipe đang mở) đang mở, socket, PID, thông tin user sở hữu.
  • Trạng thái: đang chạy, sẵn sàng chạy, hay đang chờ (chi tiết ở bài Trạng thái & context switch).

Điểm mấu chốt: một chương trình sinh ra nhiều tiến trình. Mỗi python trong ví dụ đầu bài là một tiến trình riêng với PID riêng và bộ nhớ riêng.

// process-identity.c -- in ra PID de thay moi lan chay la mot tien trinh khac
#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("Chuong trinh giong nhau, PID khac nhau: %d\n", getpid());
    return 0;
}
Thử đoán

Biên dịch gcc process-identity.c -o app rồi chạy ./app hai lần liên tiếp. Hai lần in ra cùng một PID hay khác nhau? Vì sao?

Chạy hai lần in ra hai PID khác nhau: cùng một file app (một chương trình) nhưng mỗi lần gõ ./app là OS tạo một tiến trình mới, cấp một PID mới. File trên đĩa không đổi; hai tiến trình là hai thực thể sống độc lập.

3. PCB — hồ sơ OS giữ về mỗi tiến trình

Để quản lý hàng trăm tiến trình cùng lúc — tạm dừng cái này, chạy cái kia, rồi quay lại — OS cần nhớ mọi thứ về từng tiến trình. Nó lưu trong một cấu trúc gọi là Process Control Block (PCB): một PCB cho mỗi tiến trình, sống trong bộ nhớ kernel (user code không đọc trực tiếp được).

PCB chứa (nhóm chính):

NhómNội dungVì sao cần
Định danhPID, PID của cha (PPID), user sở hữuBiết tiến trình là ai, con của ai
CPU contextGiá trị các register, program counter (PC), stack pointerChụp "đang tính tới đâu" để tạm dừng rồi tiếp
Trạng tháiRunning / Ready / Blocked, mã exit khi kết thúcScheduler biết ai được chạy
Bộ nhớCon trỏ tới page table / không gian địa chỉBiết bộ nhớ ảo của tiến trình ánh xạ vào đâu
Tài nguyênBảng file descriptor đang mở, thư mục hiện hànhKhôi phục đúng ngữ cảnh I/O
Kế toánThời gian CPU đã dùng, priorityĐiều phối công bằng

Trên Linux, cấu trúc thật đóng vai PCB là struct task_struct trong kernel (một struct lớn với hàng trăm field). Ta không cần nhớ tên từng field — điều cần hiểu là phân loại thông tin: định danh, CPU context, trạng thái, bộ nhớ, tài nguyên.

Vì sao PCB cho phép tạm dừng rồi chạy tiếp

Khi tiến trình hết lượt CPU, OS lưu toàn bộ register và program counter hiện tại vào PCB của nó, rồi nạp register+PC của tiến trình kế tiếp từ PCB của tiến trình đó. Vì PC được cất và khôi phục chính xác, khi tiến trình cũ được chạy lại, CPU tiếp tục đúng lệnh mà nó bị ngắt — không sai một byte. Đó là lý do vòng lặp của bạn "không biết" mình từng bị dừng. Chi phí của thao tác cất-nạp này (context switch) được bóc kỹ ở Module 3.

flowchart LR
  P1["Tien trinh A dang chay"] -->|"het luot CPU"| S["Cat register + PC vao PCB cua A"]
  S --> L["Nap register + PC tu PCB cua B"]
  L --> P2["Tien trinh B chay tiep dung cho"]
  P2 -.->|"sau nay den luot A"| R["Nap lai context tu PCB cua A"]
  R -.-> P1

4. Vì sao mỗi tiến trình có không gian địa chỉ riêng?

Một tính chất quan trọng của tiến trình: không gian địa chỉ tách biệt. Địa chỉ 0x5000 trong tiến trình A và 0x5000 trong tiến trình B trỏ tới hai ô nhớ vật lý khác nhau. OS + MMU (Memory Management Unit) dịch địa chỉ ảo của mỗi tiến trình sang địa chỉ vật lý riêng thông qua page table riêng của nó — cơ chế bộ nhớ ảo bạn đã học ở khoá Bộ nhớ.

Hệ quả:

  • Cô lập lỗi: tiến trình A ghi bậy vào con trỏ của nó không làm hỏng bộ nhớ tiến trình B. Một tab python crash không kéo theo tab khác.
  • Bảo mật: A không đọc lén được mật khẩu B đang giữ trong RAM (trừ khi qua kênh IPC hợp lệ — xem Module 4).
  • Đơn giản hoá lập trình: mỗi chương trình viết như thể sở hữu toàn bộ bộ nhớ, không phải né địa chỉ của chương trình khác.
Tien trinh A                 Tien trinh B
+-----------------+          +-----------------+
| dia chi ao 0x5000|         | dia chi ao 0x5000|
+--------+--------+          +--------+--------+
         |  page table A              |  page table B
         v                            v
   RAM vat ly 0x9A00            RAM vat ly 0x1C00
   (hai o nho KHAC nhau -- khong dung cham)

Đây là điểm phân biệt cốt lõi giữa tiến trình (không gian địa chỉ riêng) và thread (chia sẻ không gian địa chỉ) — trục chính của bài Thread vs process.

5. Pitfall tổng hợp

Nhầm 1 — "chạy chương trình 2 lần thì chúng chia sẻ biến":

// Chay ./counter hai lan cung luc, moi ban tang bien 'count' rieng
static int count = 0;   // moi tien trinh co ban sao RIENG cua count

✅ Mỗi lần chạy là một tiến trình với không gian địa chỉ riêng. Biến count của tiến trình này và tiến trình kia là hai ô nhớ khác nhau; tăng cái này không ảnh hưởng cái kia. Muốn hai tiến trình chia sẻ dữ liệu, phải dùng IPC (shared memory, pipe, socket — Module 4), không phải biến thường.

Nhầm 2 — "PID gắn liền với chương trình":

✅ PID gắn với tiến trình, không với chương trình. Cùng file app chạy 3 lần có 3 PID khác nhau. Sau khi tiến trình chết và được cha wait (reap), PID của nó mới được OS thu hồi và có thể cấp lại cho tiến trình mới sau này — nên đừng giả định "PID 1234 luôn là app của tôi".

Nhầm 3 — "PCB nằm trong bộ nhớ của tiến trình":

✅ PCB sống trong bộ nhớ kernel, không trong không gian địa chỉ user của tiến trình. Nếu PCB nằm trong vùng nhớ user, một tiến trình lỗi có thể tự sửa trạng thái/priority của mình — phá cô lập và bảo mật. User code chỉ truy cập thông tin PCB gián tiếp qua system call (getpid, getppid, đọc /proc/<pid>/).

6. 📚 Deep Dive

📚 Đào sâu (tuỳ chọn)

Nguồn chuẩn:

  • getpid(2) — man7.org — system call trả PID của tiến trình gọi; getppid() trả PID của cha. Đây là cách chuẩn để tiến trình tự biết định danh của mình.
  • proc(5) — man7.org — filesystem ảo /proc/<pid>/ phơi bày thông tin từ PCB kernel ra không gian user: status, stat, fd/ (file đang mở), maps (không gian địa chỉ). Đây là "cửa sổ" đọc PCB mà không cần viết code kernel.
  • OSTEP — chương "The Abstraction: The Process" (file cpu-intro.pdf) — giải thích khái niệm process, machine state (memory, register, PC), và process API ở mức nền tảng.

Ghi chú: Trên Linux, cấu trúc kernel đóng vai PCB là task_struct (khai báo trong include/linux/sched.h). Bạn không cần đọc source kernel để hiểu bài này — chỉ cần nắm loại thông tin PCB giữ. Muốn "nhìn" PCB thực tế, chạy cat /proc/self/status để xem OS đang giữ gì về chính tiến trình cat đó.

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

  • fork, exec, wait — vòng đời một tiến trình: bài kế dùng chính khái niệm tiến trình + PCB ở đây để giải thích fork nhân đôi một tiến trình (chép PCB + không gian địa chỉ) như thế nào.
  • System call là gì: getpid, fork, exec đều là system call — cách duy nhất user code nhờ kernel thao tác trên tiến trình.
  • Thread vs process: thread dùng chung không gian địa chỉ của tiến trình — đối lập trực tiếp với "mỗi tiến trình một không gian riêng" ở section 4.
  • Trạng thái & context switch: đào sâu thao tác cất-nạp CPU context vào/ra PCB và chi phí thật của nó.

8. Tóm tắt

  • Chương trình là file tĩnh trên đĩa; tiến trình là một lần chạy của nó — có bộ nhớ, register, PC, tài nguyên riêng.
  • Một chương trình sinh nhiều tiến trình độc lập; mỗi tiến trình có một PID duy nhất (được thu hồi và tái sử dụng sau khi tiến trình chết).
  • PCB (Process Control Block) là cấu trúc kernel lưu toàn bộ trạng thái tiến trình: định danh, CPU context (register + PC), trạng thái, bộ nhớ, tài nguyên, kế toán.
  • Nhờ cất/nạp CPU context vào/ra PCB, OS tạm dừng rồi chạy tiếp một tiến trình đúng chỗ mà nó không nhận ra.
  • Mỗi tiến trình có không gian địa chỉ ảo riêng → cô lập lỗi, bảo mật, và lập trình đơn giản. Đây là ranh giới phân biệt tiến trình với thread.
  • PCB nằm trong bộ nhớ kernel; user code chỉ đọc gián tiếp qua system call (getpid) hoặc /proc/<pid>/.

9. Tự kiểm tra

Tự kiểm tra
Q1
Bạn chạy cùng một file thực thi ./server hai lần cùng lúc. Đó là một hay hai tiến trình? Chúng có chia sẻ biến toàn cục không? Giải thích bằng khái niệm không gian địa chỉ.
Đó là hai tiến trình độc lập, dù cùng một chương trình (cùng file trên đĩa). Mỗi lần gõ ./server, OS tạo một tiến trình mới với PID riêng và không gian địa chỉ ảo riêng. Biến toàn cục count của tiến trình thứ nhất và thứ hai là hai ô nhớ vật lý khác nhau — page table của mỗi tiến trình ánh xạ cùng địa chỉ ảo sang vùng RAM riêng. Tăng biến ở tiến trình này không ảnh hưởng tiến trình kia. Muốn chia sẻ dữ liệu thật sự phải dùng cơ chế IPC (shared memory, pipe...), không phải biến thường.
Q2
PCB lưu những nhóm thông tin gì, và vì sao nhờ nó mà OS tạm dừng một tiến trình rồi chạy tiếp đúng chỗ được?
PCB (Process Control Block) lưu: định danh (PID, PPID, user), CPU context (register, program counter, stack pointer), trạng thái (running/ready/blocked, exit code), con trỏ bộ nhớ (page table), tài nguyên (bảng file mở), và kế toán (thời gian CPU, priority). Khi tiến trình hết lượt CPU, OS cất register và PC hiện tại vào PCB. Vì program counter — địa chỉ lệnh kế tiếp — được lưu chính xác, khi tiến trình được chạy lại, OS nạp lại register+PC từ PCB và CPU tiếp tục đúng lệnh bị ngắt. Không có PCB thì OS không biết khôi phục tiến trình về đâu.
Q3
Vì sao PCB được đặt trong bộ nhớ kernel thay vì trong không gian địa chỉ user của tiến trình?
Nếu PCB nằm trong vùng nhớ user mà tiến trình đọc-ghi tự do được, một tiến trình có thể tự sửa trạng thái, priority, hay quyền của mình — ví dụ nâng priority để độc chiếm CPU, hoặc giả mạo user sở hữu. Điều đó phá vỡ cô lập và bảo mật giữa các tiến trình. Đặt PCB trong bộ nhớ kernel (chỉ kernel mode truy cập) đảm bảo chỉ OS mới thay đổi được thông tin điều khiển. User code muốn đọc thông tin PCB phải đi qua system call (như getpid) hoặc filesystem ảo /proc/<pid>/ — kênh có kiểm soát, chỉ đọc cái được phép.
Q4
Một tiến trình đang chạy vòng lặp nặng, nhưng chuột và các app khác vẫn phản hồi mượt. Điều này liên quan gì tới PCB?
OS chạy đa nhiệm bằng cách luân phiên nhiều tiến trình trên CPU: cho tiến trình vòng lặp chạy vài mili-giây, rồi tạm dừng nó để chạy tiến trình khác (trình quản lý cửa sổ, driver chuột...), rồi quay lại. Mỗi lần dừng, OS cất CPU context của tiến trình vào PCB của nó; mỗi lần cho chạy lại, nạp context từ PCB ra. Vì việc cất-nạp giữ nguyên program counter và register, vòng lặp tiếp tục đúng chỗ như chưa từng bị ngắt — nó "không biết" mình đã nhường CPU. Cảm giác mọi thứ chạy song song là ảo giác do OS chuyển đổi rất nhanh, và PCB là thứ làm cho việc chuyển đổi đó không mất dữ liệu.
Q5
Bạn thấy PID 4821 là app của mình lúc 10 giờ sáng. Chiều bạn lại thấy PID 4821 nhưng là một tiến trình khác. Có mâu thuẫn không? Giải thích.
Không mâu thuẫn. PID gắn với tiến trình, không với chương trình, và PID được tái sử dụng. Khi app của bạn (PID 4821) kết thúc và được cha wait thu hồi, OS giải phóng số 4821 khỏi bảng tiến trình. Sau đó, khi một tiến trình mới bất kỳ được tạo, OS có thể cấp lại đúng số 4821 cho nó. Vì vậy PID chỉ định danh duy nhất trong khoảng thời gian tiến trình còn sống, không phải mãi mãi. Bài học thực tế: đừng lưu PID rồi giả định nó vẫn là tiến trình cũ sau một thời gian — hãy kiểm tra lại (ví dụ so khớp tên/thời điểm khởi động).

Bài tiếp theo: fork, exec, wait — vòng đời một tiến trình

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