Hệ điều hành & Tiến trình/Mini-challenge — mổ xẻ cây tiến trình bằng ps, pstree, kill
13/28
Bài 13 / 28~15 phútTiến trình — sinh ra, sống, chếtMiễn phí lượt xem

Mini-challenge — mổ xẻ cây tiến trình bằng ps, pstree, kill

Tự dùng ps/pstree/kill: tìm quan hệ cha-con từ PID 1 xuống shell của bạn, tự tạo một zombie rồi diệt nó, và thử SIGTERM vs SIGKILL.

TL;DR: Đủ lý thuyết — giờ cầm công cụ thật. Bài này cho bạn ba nhiệm vụ trên máy Linux của chính mình: (1) dùng pstree/ps truy vết chuỗi cha-con từ init/systemd (PID 1) xuống đúng cái shell bạn đang gõ; (2) cố tình tạo một zombie rồi quan sát nó bằng ps (trạng thái Z, nhãn <defunct>) và dọn nó; (3) gửi SIGTERM rồi SIGKILL lên một tiến trình thật để thấy tận mắt khác biệt "xin dừng" và "giết cứng". Xong bài, bạn không chỉ biết về tiến trình — bạn đọc được cây tiến trình đang sống trên máy mình.

🎯 Đề bài

Bạn cần một terminal trên Linux (hoặc WSL2 / macOS — lệnh gần giống, pstree có thể cần cài). Ba nhiệm vụ độc lập:

Nhiệm vụ 1 — Truy vết cây tiến trình. Từ terminal của bạn, tìm chuỗi cha-con đầy đủ từ PID 1 (init/systemd) đi xuống cho tới chính tiến trình shell bạn đang dùng. Với mỗi mắt xích, ghi lại PID và tên tiến trình. Trả lời: shell của bạn có PPID là gì? Đi ngược lên nữa tới đâu thì gặp PID 1?

Nhiệm vụ 2 — Tạo và dọn một zombie. Viết một chương trình (bash hoặc C) fork ra con, con thoát ngay, còn cha không wait và ngủ một lúc. Trong lúc đó, dùng ps tìm con ở trạng thái Z. Sau đó dọn zombie — bằng đúng cách đã học ở bài 03.

Nhiệm vụ 3 — SIGTERM vs SIGKILL. Chạy một tiến trình đơn giản (ví dụ sleep 1000), rồi lần lượt thử dừng nó bằng SIGTERM và bằng SIGKILL. Sau đó viết một tiến trình phớt lờ SIGTERM và xác nhận rằng chỉ SIGKILL giết được nó.

Đây là bài independent: cố làm cả ba nhiệm vụ trước khi mở phần Lời giải. Bạn đã có đủ công cụ từ bốn bài trước.

🔍 Phân tích I-P-O

Mô tả
InputMáy Linux đang chạy, quyền chạy ps/pstree/kill, gcc (cho phần C)
ProcessQuan sát (đọc trạng thái tiến trình) + tác động (gửi signal, tạo/dọn tiến trình)
OutputChuỗi cha-con tới PID 1; một zombie quan sát được rồi dọn; khác biệt SIGTERM/SIGKILL xác nhận bằng mắt

Điều kiện làm đúng:

  • Không cần sudo cho các tiến trình của chính bạn.
  • Cẩn thận: chỉ kill các tiến trình bạn tự tạo (đừng kill PID lạ, nhất là số nhỏ).
  • Mở hai terminal: một để chạy tiến trình thí nghiệm, một để ps/kill quan sát.

📦 Concept mapping

Bài này tổng hợp trực tiếp cả bốn bài của module:

Kiến thức cầnBài gốcDùng ở đây
PID, PPID, cây cha-conBài 01 — Tiến trình & PCBTruy vết chuỗi PPID lên tới PID 1
fork tạo con, wait thu xácBài 02 — fork, exec, waitViết chương trình fork ra con
Zombie (Z, <defunct>), reap qua chaBài 03 — Zombie & orphanTạo zombie, nhận diện, dọn bằng cách xử lý cha
SIGTERM vs SIGKILL, bắt được hay khôngBài 04 — SignalGửi hai signal, quan sát khác biệt

▶️ Starter

Nhiệm vụ 1 — vài lệnh để bắt đầu:

# Xem cay tien trinh co PID, tu goc xuong
pstree -p

# Shell hien tai cua ban co PID nao?
echo "Shell PID = $$"

# Xem PID + PPID + lenh cua mot tien trinh cu the
ps -o pid,ppid,comm -p $$

Nhiệm vụ 2 — khung tạo zombie bằng bash (điền phần còn thiếu):

# zombie.sh -- cha tao con, con chet, cha khong wait ngay
# TODO: cho con thoat ngay, cha ngu 60s de quan sat zombie
( sleep 0 ) &        # tien trinh con chay roi thoat gan nhu tuc thi
CHILD=$!
echo "Con PID = $CHILD, dang ngu de ban quan sat..."
# TODO: them lenh giu cha song ma khong wait con
Bash có thể reap ngầm

Bash tương tác thường tự wait (reap) job nền của nó ngay khi job chết, nên bạn có thể không thấy trạng thái Z với khung bash này. Nếu vậy, dùng bản C ở phần Lời giải (zombie.c) — nó kiểm soát chính xác việc cha không wait, đảm bảo quan sát được zombie.

Nhiệm vụ 3 — tiến trình để thí nghiệm signal:

# Terminal 1: chay mot tien trinh de kill
sleep 1000 &
echo "sleep PID = $!"
# TODO (Terminal 2): gui SIGTERM roi SIGKILL vao PID nay

💡 Gợi ý

Gợi ý 1 (truy vết): pstree -p -s <PID> in nhánh từ PID đó ngược lên tới gốc — nhanh hơn đọc cả cây. Thử pstree -p -s $$.

Gợi ý 2 (zombie): trong bash, một job con đã chết mà chưa được wait là zombie. Nếu bash tự động reap quá nhanh, dùng bản C ở Lời giải (kiểm soát chính xác hơn). Tìm zombie: ps -el | grep ' Z ' hoặc ps aux | grep defunct.

Gợi ý 3 (dọn zombie): nhớ bài 03 — không kill được zombie. Zombie biến mất khi cha wait nó, hoặc khi cha chết (init reap). Vậy cách dọn nhanh nhất: kết thúc tiến trình cha.

Gợi ý 4 (signal): kill -TERM <pid> (hay kill <pid>) gửi SIGTERM; kill -KILL <pid> (hay kill -9) gửi SIGKILL. Với tiến trình phớt lờ SIGTERM, quan sát: sau kill -TERMvẫn còn trong ps; sau kill -9 nó biến mất.

✅ Lời giải

Nhiệm vụ 1 — Truy vết cây tiến trình

$ echo $$
5321                      # PID cua shell hien tai (vi du bash)

$ pstree -p -s 5321
systemd(1)---systemd(1234)---gnome-terminal(4180)---bash(5321)
   ^PID 1        ^user session   ^terminal emulator   ^shell cua ban

Đọc từ phải sang: bash (PID 5321) có cha là gnome-terminal (4180), cha nó là systemd --user (1234), rồi lên tới systemd (PID 1) — tổ tiên của mọi tiến trình. Kiểm chứng từng mắt xích bằng ps -o pid,ppid,comm -p <pid>:

$ ps -o pid,ppid,comm -p 5321
   PID   PPID  COMMAND
  5321   4180  bash            # PPID cua bash = 4180 (terminal)

Mọi tiến trình đều truy được về PID 1 — đó là ý nghĩa "cây tiến trình": một cây có gốc duy nhất là init/systemd.

Nhiệm vụ 2 — Tạo và dọn zombie

Bản C kiểm soát chính xác (bash đôi khi reap ngầm):

// zombie.c
#include <stdio.h>
#include <unistd.h>

int main(void) {
    pid_t pid = fork();
    if (pid == 0) {
        _exit(0);             // con thoat ngay -> thanh zombie
    }
    printf("Cha PID=%d, con zombie PID=%d\n", getpid(), pid);
    sleep(60);                // cha KHONG wait -> con ket lai zombie 60s
    return 0;
}
# Terminal 1:
$ gcc zombie.c -o zombie && ./zombie
Cha PID=6001, con zombie PID=6002

# Terminal 2 (trong 60 giay):
$ ps -o pid,ppid,stat,comm -p 6002
   PID   PPID STAT COMMAND
  6002   6001 Z    zombie          # STAT = Z, con la zombie cua cha 6001

$ ps aux | grep defunct
user  6002  ... Z  ...  [zombie] <defunct>    # nhan <defunct>

Dọn zombie — không kill zombie (6002) được; kill cha (6001):

# Terminal 2:
$ kill -9 6002          # VO TAC DUNG: zombie da chet roi
$ kill 6001             # kill CHA -> con thanh orphan cua init -> init reap ngay
# Kiem tra: 6002 da bien mat khoi ps

Khi cha 6001 chết, zombie 6002 được init (PID 1) nhận nuôi và reap tức thì — đúng cơ chế bài 03.

Nhiệm vụ 3 — SIGTERM vs SIGKILL

Tiến trình thường (sleep) chết với cả hai:

$ sleep 1000 &
[1] 6100
$ kill -TERM 6100       # SIGTERM -> sleep khong bat, dung default Term -> chet
[1]+  Terminated        sleep 1000

Giờ một tiến trình phớt lờ SIGTERM — chỉ SIGKILL giết được:

// stubborn.c -- bo qua SIGTERM, chi SIGKILL moi giet duoc
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(void) {
    signal(SIGTERM, SIG_IGN);       // bo qua SIGTERM
    printf("PID=%d, thu 'kill %d' -- toi se lo di\n", getpid(), getpid());
    while (1) pause();              // ngu cho signal
    return 0;
}
# Terminal 1:
$ gcc stubborn.c -o stubborn && ./stubborn
PID=6200, thu 'kill 6200' -- toi se lo di

# Terminal 2:
$ kill 6200             # SIGTERM -> bi bo qua, tien trinh VAN SONG
$ ps -p 6200            # van con day
$ kill -9 6200          # SIGKILL -> khong the bo qua -> chet ngay
$ ps -p 6200            # da bien mat

Đây là bằng chứng sống cho luận điểm bài 04: SIGTERM bắt/bỏ qua được, SIGKILL thì không — luôn là van cuối cùng.

🎓 Mở rộng

  • Quan sát orphan reparenting: sửa zombie.c cho con sleep(30) còn cha _exit(0) ngay. Trong 30s, chạy ps -o pid,ppid,comm -p <child> — bạn sẽ thấy PPID của con đổi thành 1 (hoặc PID của subreaper gần nhất trên hệ systemd) khi init nhận nuôi. Đây là orphan tự lành ở bài 03.
  • Đếm zombie hệ thống: ps -el | awk '$2 == "Z"' liệt kê mọi zombie đang có. Trên máy khoẻ, con số này gần 0.
  • Graceful shutdown thật: viết lại stubborn.c nhưng bắt SIGTERM để in "cleaning up..." rồi thoát (pattern bài 04). Gửi kill <pid> và xem nó dừng sạch thay vì bị bỏ qua.
  • /proc là cửa sổ vào PCB: chạy cat /proc/<pid>/status cho một tiến trình của bạn — bạn đang đọc chính các field PCB (State, PPid, VmRSS...) mà bài 01 mô tả.

✨ Điều bạn vừa làm được

Bạn vừa chuyển từ đọc về tiến trình sang thao tác trên chúng:

  • Đọc cây tiến trình thật: truy vết chuỗi cha-con từ shell của bạn lên tới PID 1 — thấy tận mắt "mọi tiến trình có một tổ tiên chung là init".
  • Tạo và dọn zombie: dựng đúng điều kiện sinh zombie (con chết, cha không wait), nhận diện bằng Z/<defunct>, và dọn bằng cách kill cha — không phí công kill xác.
  • Phân biệt SIGTERM/SIGKILL bằng thực nghiệm: chứng minh rằng một tiến trình phớt lờ được SIGTERM nhưng không thể chống SIGKILL.

Kỹ năng này — đọc trạng thái tiến trình và tác động đúng cách — là thứ bạn dùng mỗi lần debug một service treo, một container không chịu tắt, hay một máy đầy zombie trong production.

Bài tiếp theo: Tổng kết module — 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