2.1 Program vs Process — đừng nhầm!
Program
- File tĩnh trên disk (vd
/usr/bin/ls) - Chứa instructions + initial data
- Không "chạy", không có state
- Là một entity passive
Process
- Program đang chạy trong RAM
- Có PID, state, registers, stack, heap riêng
- Được kernel quản lý qua PCB
- Là một entity active
1 program → nhiều process: mở 5 cửa sổ Chrome = 1 program nhưng nhiều process.
1 process → 1 program tại 1 thời điểm: nhưng có thể "thay" program khác qua execve (xem 2.6).
2.2 Memory Layout của 1 Process
Khi process chạy, kernel cấp cho nó một virtual address space, chia làm các vùng:
Chi tiết từng vùng
- Text (Code): instructions của program. Read-only — bảo vệ khỏi modify.
- Data: biến global đã khởi tạo (vd
int x = 5;). - BSS: biến global chưa khởi tạo (vd
int y;) — kernel zero-fill khi load. - Heap: cấp phát động bằng
malloc/new. Tăng lên (low → high) qua syscallbrk/sbrk. - Stack: function call. Mỗi call push 1 frame (return address, params, local vars). Tăng xuống (high → low).
Phân biệt 3 lỗi memory phổ biến
| Lỗi | Nguyên nhân | Vùng liên quan |
|---|---|---|
| Stack Overflow | Đệ quy quá sâu, mảng stack quá lớn | Stack đụng heap |
| Heap Overflow / Buffer Overflow | Ghi quá kích thước buffer cấp phát | Heap |
| Memory Leak | Cấp phát mà quên free | Heap (lớn dần không thu hồi) |
| Use-after-free | Dùng pointer sau khi free | Heap (UB) |
| Segmentation Fault | Truy cập memory không thuộc process | Bất kỳ — kernel block |
# Xem memory layout của process running trên Linux
cat /proc/$$/maps | head
# 555555554000-555555558000 r--p 00000000 fd:01 ... /usr/bin/bash (text)
# 555555558000-555555637000 r-xp 00004000 fd:01 ... /usr/bin/bash (text exec)
# ...
# 7fffffde0000-7fffffe01000 rw-p 00000000 00:00 0 [stack]
# ...
2.3 Process Control Block (PCB) — "thẻ căn cước" của process
Kernel quản lý mỗi process bằng cấu trúc PCB (trong Linux gọi là task_struct, ~10KB).
Mỗi process có 1 PCB lưu mọi thứ cần để khôi phục trạng thái khi switch CPU.
PCB chứa gì?
Khi context switch xảy ra, kernel:
- Lưu CPU state hiện tại vào PCB của process đang chạy
- Load CPU state từ PCB của process kế tiếp
- Nhảy đến program counter của process mới → process tiếp tục
Tất cả diễn ra trong <1 microsecond. Hàng nghìn lần mỗi giây trên máy bận.
2.4 Process States — vòng đời 5 trạng thái
- NEW — vừa được tạo, chưa load đầy đủ
- READY — sẵn sàng chạy, đang đợi CPU
- RUNNING — đang sử dụng CPU
- BLOCKED — đợi I/O hoặc event (vd đọc file, đợi network)
- TERMINATED — đã exit, đợi parent collect
Linux state code (xem trong ps)
| Code | Ý nghĩa |
|---|---|
R | Running hoặc Runnable (ready) |
S | Sleeping (interruptible — đợi I/O bình thường) |
D | Uninterruptible sleep (không thể bị interrupt — vd đợi disk) |
Z | Zombie |
T | Stopped (bằng SIGSTOP) |
ps -ax -o pid,state,comm | head
# PID S COMMAND
# 1 S systemd
# 123 S /usr/bin/python
# 456 R top
# 789 D rsync ← đợi disk, không thể kill được
2.5 fork() — sao chép process
fork() tạo process con bằng cách sao chép parent. Sau khi gọi:
- Có 2 process: parent và child
- Child có PID khác, PPID = PID của parent
- Child có copy của memory parent (text, data, heap, stack)
- Child có copy của file descriptors (cả 2 cùng trỏ vào file đang mở)
fork()trả về 2 lần: trong parent trả PID con; trong child trả 0
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// Child code
printf("[child] PID=%d, PPID=%d\n", getpid(), getppid());
} else {
// Parent code
printf("[parent] PID=%d, child PID=%d\n", getpid(), pid);
}
return 0;
}
// Output (thứ tự có thể khác do scheduling):
// [parent] PID=1234, child PID=1235
// [child] PID=1235, PPID=1234
Copy-on-Write — tối ưu thông minh
Tưởng fork phải copy hàng GB memory? Không! Linux dùng Copy-on-Write (CoW):
- Sau fork, parent & child ban đầu chia sẻ page memory (đánh dấu read-only)
- Khi một bên ghi vào page, kernel mới copy page đó cho bên kia
- Trong nhiều trường hợp (fork rồi exec ngay), không bao giờ phải copy thật
Đây là lý do fork trong Linux nhanh đến không ngờ.
2.6 exec() — biến process thành program khác
exec() family (execve, execvp...) thay thế toàn bộ memory image của process hiện tại
bằng program mới. Cùng PID, cùng PPID, nhưng code hoàn toàn khác.
#include <unistd.h>
int main() {
char *args[] = { "/bin/ls", "-l", "/tmp", NULL };
execv("/bin/ls", args);
// Nếu thành công, dòng này không bao giờ được chạy
perror("exec failed");
return 1;
}
Pattern fork + exec — chạy program khác
// Đây là cách Unix shell chạy command:
pid_t pid = fork();
if (pid == 0) {
// Child: replace mình thành /bin/ls
execlp("ls", "ls", "-l", NULL);
exit(1); // chỉ chạy nếu exec fail
} else {
// Parent: đợi child kết thúc
int status;
waitpid(pid, &status, 0);
}
Bash, zsh, sh đều làm thế này khi bạn gõ command. Đó là tại sao child process có cùng PPID = shell PID.
2.7 wait() & exit() — kết thúc process
exit(code) kết thúc process, trả về exit code (0 = success, ≠ 0 = error).
wait(&status) hoặc waitpid() để parent đợi child kết thúc và lấy exit code.
pid_t pid = fork();
if (pid == 0) {
// Child
sleep(2);
exit(42); // exit code 42
} else {
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child exit code: %d\n", WEXITSTATUS(status));
}
}
// Output: Child exit code: 42
Trong shell
ls /no-such-dir
echo $? # 2 — exit code của lệnh trước
true
echo $? # 0
# Chạy chain dựa trên exit code
make && ./run # chỉ chạy ./run nếu make success
make || echo "build failed"
2.8 Zombie & Orphan Process — bug "kinh dị" cần biết
Zombie Process 🧟
Khi child kết thúc (exit) nhưng parent chưa gọi wait(), kernel vẫn giữ PCB của child
(chỉ giữ exit code, đã giải phóng memory) đợi parent collect.
Process trong trạng thái này gọi là zombie. Nó "chết" nhưng "chưa được chôn". Nếu parent là buggy không bao giờ wait, zombie tích tụ → cạn PID.
// Code tạo zombie
pid_t pid = fork();
if (pid == 0) {
exit(0); // child exit
} else {
// parent KHÔNG gọi wait!
sleep(60); // child là zombie suốt 60s
}
# Quan sát zombie:
ps -ax -o pid,state,comm | grep Z
# 1234 Z defunct ← zombie!
Orphan Process
Ngược lại: parent kết thúc trước child. Child thành "mồ côi". Linux: kernel tự động re-parent orphan về PID 1 (init/systemd).
init làm việc wait() giúp, nên orphan không thành zombie.
pid_t pid = fork();
if (pid == 0) {
sleep(10);
printf("My new parent: %d\n", getppid()); // PPID = 1 (init)
} else {
exit(0); // parent chết ngay
}
Cách xử lý zombie trong code thật
// Cách 1: Bắt SIGCHLD và auto-reap
#include <signal.h>
signal(SIGCHLD, SIG_IGN); // ignore SIGCHLD → kernel auto-reap
// Cách 2: Bắt SIGCHLD handler
void handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0) {}
}
signal(SIGCHLD, handler);
node app.js làm PID 1 trong container, app trở thành init.
Nó phải biết reap orphan. Nếu không, zombie có thể tích tụ. Cách giải quyết: dùng tini làm PID 1 hoặc --init flag của Docker.
2.9 Quan sát Process trên Linux/macOS
ps — snapshot
ps # process của shell hiện tại
ps aux # tất cả process, format BSD
ps -ef # format System V
ps -ax -o pid,ppid,state,comm,%cpu,%mem # custom columns
# Tree view
ps auxf # Linux
pstree -p # tree với PID
top / htop — real-time
top # built-in
htop # đẹp hơn, interactive (cần install)
btop # modern alternative
/proc filesystem (Linux only) — kho báu
ls /proc/ # mỗi PID là 1 directory
ls /proc/1/ # info của init
cat /proc/1/status # state, memory
cat /proc/1/cmdline # command line khi start
cat /proc/1/maps # memory regions
ls /proc/1/fd/ # file descriptors
kill / killall
kill 1234 # gửi SIGTERM (15) — process tự cleanup
kill -9 1234 # SIGKILL — force, không clean
kill -STOP 1234 # tạm dừng
kill -CONT 1234 # tiếp tục
killall node # kill tất cả process tên "node"
pkill -f myapp.py # kill bằng pattern command line
2.10 Node.js child_process — tạo process trong JS
const { spawn, exec, fork, execSync } = require('child_process');
// 1. spawn — chạy command, stream stdout/stderr
const ls = spawn('ls', ['-lh', '/tmp']);
ls.stdout.on('data', (data) => {
console.log(data.toString());
});
ls.on('close', (code) => {
console.log(`exit code: ${code}`);
});
// 2. exec — chạy + buffer toàn bộ output (cẩn thận memory!)
exec('ls /tmp', (err, stdout, stderr) => {
if (err) console.error(err);
console.log(stdout);
});
// 3. execSync — đồng bộ (block event loop, hạn chế dùng!)
const result = execSync('date').toString();
// 4. fork — chỉ cho Node child, có IPC channel
const child = fork('./worker.js');
child.send({ task: 'process_data' });
child.on('message', (msg) => {
console.log('from child:', msg);
});
Trong worker.js:
process.on('message', (msg) => {
if (msg.task === 'process_data') {
// ... heavy work
process.send({ result: 'done' });
}
});
fork của Node.js KHÔNG phải fork() syscall. Nó tạo Node process mới
(qua execve) và setup IPC channel để 2 process nói chuyện.
Bài tập
Viết C program dùng fork() in "I am parent" / "I am child". Compile + chạy. Quan sát output.
Viết C program tạo zombie (parent fork rồi sleep, child exit ngay). Mở terminal khác chạy ps -ax | grep Z.
Tương tự nhưng cho parent exit trước. Quan sát PPID của child sau đó (nên là 1).
Viết C program đơn giản: đọc command từ stdin, fork + exec để chạy command đó, parent wait. Đây là bare-bones của bash.
Trên Linux, viết C program in địa chỉ của: 1 biến global, 1 biến local, kết quả malloc, function pointer.
Sắp xếp theo địa chỉ. Mỗi địa chỉ thuộc vùng nào (text/data/heap/stack)?
Viết Node script dùng spawn chạy ping google.com -c 5, in từng line từ stdout với prefix timestamp.
Lệnh nào liệt kê tất cả zombie trên máy? ps aux | awk '$8 ~ /Z/' hay tương tự.
🧪 Quiz cuối chương
Câu 1. Sau fork(), parent và child có chia sẻ heap không?
Đáp án: Không chia sẻ. Mỗi process có address space riêng. CoW giúp fork nhanh nhưng về logic process vẫn cô lập memory.
Câu 2. Khi fork() trả về 0, đó là code đang chạy ở đâu?
Đáp án: Trong child. Parent nhận PID con; child nhận 0; lỗi nhận -1.
Câu 3. Zombie process là gì?
Đáp án: đã exit nhưng chưa được wait. Kernel giữ PCB cho đến khi parent wait. Nếu parent buggy, zombie tích tụ.
Câu 4. Sau khi parent exit trước child, child trở thành?
Đáp án: Orphan, re-parent về init. init định kỳ wait() để dọn orphan, không tích tụ zombie.
Câu 5. Vùng nào của process tăng/giảm khi malloc() và free()?
Đáp án: Heap. Stack cho local var; Heap cho dynamic alloc.
Câu 6. Tại sao fork() trên Linux nhanh dù process có nhiều GB memory?
Đáp án: Copy-on-Write. Linux đánh dấu page read-only, copy thật chỉ khi 1 bên ghi. Pattern fork+exec không bao giờ phải copy.
Câu 7. execve() làm gì?
Đáp án: thay thế image. PID không đổi, code hoàn toàn khác. Pattern fork+exec là cách shell chạy command.
Câu 8. Process trong trạng thái D (uninterruptible sleep) trên Linux nghĩa là?
Đáp án: đợi I/O không thể interrupt. Vì thế kill -9 cũng vô tác dụng cho đến khi I/O xong. Hay gặp khi NFS hang.
Tổng kết chương 2
- ✅ Program (file tĩnh) ≠ Process (đang chạy có PCB)
- ✅ Memory layout: text, data, BSS, heap (↑), stack (↓)
- ✅ PCB chứa PID, state, registers, page table, fd table — kernel quản lý mỗi process
- ✅ 5 trạng thái: NEW → READY → RUNNING → BLOCKED → TERMINATED
- ✅
fork()trả 2 lần (parent: PID con; child: 0); Copy-on-Write làm fork nhanh - ✅
exec()thay thế image, cùng PID. Pattern fork + exec là cách shell chạy command - ✅ Zombie: child exit, parent chưa wait. Orphan: parent chết trước, re-parent về init
- ✅ Quan sát process:
ps,top,htop,/proc,kill - ✅ Node.js:
spawn(stream),exec(buffer),fork(IPC channel)