Hệ điều hành & Tiến trình/Signal — SIGTERM, SIGKILL và Ctrl+C thật sự làm gì
12/28
Bài 12 / 28~12 phútTiến trình — sinh ra, sống, chếtMiễn phí lượt xem

Signal — SIGTERM, SIGKILL và Ctrl+C thật sự làm gì

Cơ chế signal: OS gõ cửa tiến trình thế nào, khác nhau giữa SIGTERM (xin dừng), SIGKILL (không cãi được), SIGINT (Ctrl+C), SIGSTOP — và graceful shutdown.

TL;DR: Signal là cách OS (hoặc tiến trình khác) gõ cửa một tiến trình đang chạy để báo một sự kiện: "hãy dừng", "có lỗi", "con của mày vừa chết". Khi signal tới, kernel tạm ngắt tiến trình và chạy handler — hàm phản ứng — nếu tiến trình đã đăng ký; nếu không, dùng default action (thường là kết thúc tiến trình). Điểm mấu chốt: hầu hết signal có thể bắt (catch) để xử lý gọn gàng, nhưng hai signal — SIGKILL (số 9) và SIGSTOP (số 19 trên x86/ARM) — không thể bắt, chặn, hay bỏ qua. Vì vậy SIGTERM (số 15, "xin dừng") cho phép cleanup, còn SIGKILL giết thẳng. Ctrl+C gửi SIGINT (số 2). docker stop gửi SIGTERM rồi SIGKILL sau 10 giây.

Bạn gõ Ctrl+C một script Python đang chạy — nó dừng ngay. Nhưng một service khác thì Ctrl+C hoài không chết, phải kill -9 mới xong. Bạn docker stop một container, đợi đúng 10 giây nó mới tắt hẳn. Ba tình huống này đều là signal — và khác biệt về "chết ngay hay chết từ từ" nằm ở signal nào được gửitiến trình có được phép phản ứng không.

Xong bài, bạn compare được SIGTERM, SIGKILL, SIGINT, SIGSTOP — dự đoán mỗi tiến trình phản ứng ra sao — và áp dụng được pattern graceful shutdown mà mọi service production nên có.

1. Analogy — các kiểu yêu cầu nhân viên rời văn phòng

Hình dung nhiều cách bảo một nhân viên rời chỗ làm:

  • "Anh dọn đồ rồi về nhé" (lịch sự): nhân viên lưu file đang mở, gửi nốt email dở, tắt máy, rồi đi. Đây là SIGTERM — yêu cầu dừng, cho phép chuẩn bị.
  • Bảo vệ lôi thẳng ra cửa, không cho lấy đồ: nhân viên bị đưa ra ngay, file chưa lưu mất. Đây là SIGKILL — không thương lượng, không cleanup.
  • Nhấn chuông báo động chung (Ctrl+C): mọi người ở khu vực đó đứng dậy đi ra. Đây là SIGINT từ terminal.
  • Bấm nút "đóng băng": nhân viên đứng im tại chỗ, không làm gì, chờ lệnh tiếp. Đây là SIGSTOP — tạm dừng, chưa kết thúc.

Điểm khác biệt cốt lõi: với lời nhắc lịch sự, nhân viên được quyền phản ứng (dọn dẹp). Với bảo vệ lôi ra hay nút đóng băng, nhân viên không cãi được — tương ứng SIGKILLSIGSTOP mà tiến trình không thể bắt hay chặn.

Văn phòngSignal
"Dọn đồ rồi về" (được chuẩn bị)SIGTERM — xin dừng, cleanup được
Bảo vệ lôi thẳng ra, không cho lấy đồSIGKILL — giết ngay, không cleanup
Chuông báo động khu vựcSIGINT — Ctrl+C tới nhóm foreground
Nút đóng băng đứng imSIGSTOP — tạm dừng, không kết thúc
Bảo vệ và nút đóng băng: không cãi đượcSIGKILL/SIGSTOP không bắt/chặn được
💡 Cách nhớ

SIGTERMgõ cửa ("làm ơn dừng"); SIGKILLphá cửa ("dừng ngay, khỏi bàn"). Luôn thử SIGTERM trước để tiến trình cleanup; chỉ dùng SIGKILL khi nó không phản hồi.

2. Cơ chế signal — kernel gõ cửa thế nào?

Signal là một thông báo bất đồng bộ (asynchronous) — nó có thể tới bất kỳ lúc nào, giữa hai lệnh bất kỳ của tiến trình. Nguồn gửi: kernel (ví dụ lỗi chia cho 0 → SIGFPE), tiến trình khác (qua system call kill), hay chính terminal (Ctrl+C → SIGINT).

Khi một signal được gửi tới tiến trình, kernel tạm ngắt luồng chạy bình thường và chọn một trong ba disposition:

  1. Handler tự đăng ký: nếu tiến trình đã gọi signal()/sigaction() để đăng ký một hàm cho signal đó, kernel chạy hàm ấy (rồi thường quay lại chỗ bị ngắt). Đây là "bắt" (catch) signal.
  2. Default action: nếu chưa đăng ký, kernel dùng hành vi mặc định của signal đó. Theo signal(7), default có thể là Term (kết thúc), Core (kết thúc + dump), Stop (tạm dừng), Cont (chạy tiếp), hay Ign (bỏ qua).
  3. Bỏ qua: tiến trình đặt disposition thành SIG_IGN — signal tới cũng như không.

Khi kernel gửi một signal tới tiến trình: nếu tiến trình có đăng ký handler thì chạy handler rồi quay lại chỗ bị ngắt; nếu không thì dùng hành vi mặc định là kết thúc, tạm dừng hoặc bỏ qua; riêng SIGKILL và SIGSTOP đi thẳng, kernel thực thi ngay không hỏi handler và không cho chặn

Ngoại lệ tối quan trọng (mũi tên đứt trong sơ đồ): theo signal(7), "SIGKILLSIGSTOP không thể bị catch, block, hay ignore". Với hai signal này, kernel bỏ qua mọi handler và thực thi trực tiếp — SIGKILL giết tiến trình ngay, SIGSTOP đóng băng ngay. Đây là "van an toàn" của OS: luôn có cách giết/dừng một tiến trình lì, kể cả khi code nó cố chống.

Năm nấc của graceful shutdown: docker stop gửi SIGTERM để xin dừng; tiến trình chạy handler đóng kết nối, flush log và lưu trạng thái; nếu tự thoát trước hạn thì dừng sạch và xong sớm; hết 10 giây mà chưa thoát thì docker gửi SIGKILL; kernel giết ngay, không cleanup và dữ liệu đang dở có thể mất

3. Bốn signal bạn gặp mỗi ngày

SignalSốDefaultBắt được?Ý nghĩa / khi nào
SIGINT2TermCtrl+C tới nhóm tiến trình foreground — "ngắt, tôi muốn dừng"
SIGTERM15TermYêu cầu kết thúc lịch sự; kill <pid> mặc định gửi cái này
SIGKILL9TermKhôngGiết ngay, không cleanup; kill -9; van cuối cùng
SIGSTOP19StopKhôngĐóng băng tiến trình (không kết thúc); số 19 tuỳ kiến trúc; Ctrl+Z gửi SIGTSTP họ hàng (bắt được)

Vài hệ quả thực tế:

  • kill <pid> mặc định gửi SIGTERM, không phải SIGKILL. Nhiều người tưởng kill là "giết chết" — thực ra nó chỉ gửi signal, và mặc định là lời mời dừng lịch sự.
  • Ctrl+C = SIGINT: signal tới cả nhóm tiến trình foreground (process group — tập tiến trình shell gom lại, ví dụ cả pipeline a | b | c, để nhận signal cùng lúc). Chương trình bắt được nên có thể dừng gọn (lưu trạng thái, in "Bye"). Chương trình phớt lờ SIGINT (đăng ký handler rỗng) thì Ctrl+C vô hiệu — đó là lý do đôi khi Ctrl+C không ăn.
  • SIGKILL là lựa chọn cuối: dùng khi tiến trình không phản hồi SIGTERM. Vì không cleanup, nó có thể để lại file tạm, lock, dữ liệu ghi dở.
Vì sao SIGKILL và SIGSTOP phải không bắt được

Nếu mọi signal đều bắt/chặn được, một tiến trình lỗi (hoặc mã độc) có thể đăng ký handler rỗng cho tất cả và trở nên bất tử — không cách nào dừng. OS cần một cơ chế không thể bị vô hiệu hoá để giữ quyền kiểm soát cuối cùng. Vì thế đúng hai signal — SIGKILL (giết) và SIGSTOP (dừng) — được kernel thực thi trực tiếp, không đi qua code của tiến trình. Đây là ranh giới quyền lực kernel/user (đã học ở Module 1): user code chạy trong không gian của nó, nhưng kernel luôn có nút tắt cứng.

4. Graceful shutdown — bắt SIGTERM để dừng sạch

Đây là pattern quan trọng nhất của bài, dùng trong mọi service production. Ý tưởng: bắt SIGTERM, và thay vì chết ngay, làm thủ tục dừng có trật tự:

  1. Ngừng nhận yêu cầu/việc mới.
  2. Hoàn tất các việc đang dở (request đang xử lý).
  3. Đóng tài nguyên: flush log, đóng kết nối DB, nhả lock, xoá file tạm.
  4. Thoát với exit status hợp lý.

Dưới đây là khung graceful shutdown còn hai chỗ trống — dựa vào bốn bước trên, tự điền trước khi xem đáp án.

// graceful.c -- bat SIGTERM va SIGINT de dung sach
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t stop = 0;   // co bao "can dung"

void on_signal(int sig) {
    stop = 1;                     // chi dat co -- KHONG lam viec nang trong handler
}

int main(void) {
    signal(SIGTERM, /* TODO 1: dang ky ham nao cho SIGTERM? */);
    signal(SIGINT,  /* TODO 1: cung ham do cho Ctrl+C */);

    while (/* TODO 2: lap chung nao co chua bat? */) {
        // ... xu ly cong viec binh thuong ...
        sleep(1);
    }

    // Toi day nghia la da nhan SIGTERM/SIGINT -> cleanup
    printf("Nhan tin hieu dung, dang cleanup roi thoat...\n");
    // dong DB, flush log, xoa file tam o day
    return 0;
}
Tự điền trước khi xem đáp án

TODO 1: đăng ký hàm nào để khi SIGTERM (và Ctrl+C) tới thì cờ stop được bật? TODO 2: vòng lặp chính nên chạy chừng nào cờ stop còn ở giá trị nào? Nhớ: handler chỉ đặt cờ, còn main thấy cờ mới cleanup.

Đáp án: TODO 1 = on_signal (hàm handler đặt stop = 1); TODO 2 = !stop (lặp chừng nào cờ chưa bật, thoát vòng khi handler đã bật cờ). Khớp lại: signal(SIGTERM, on_signal);while (!stop) { ... }.

Thử đoán

Trong handler on_signal, ta chỉ đặt stop = 1 chứ không in log hay đóng DB ngay. Vì sao không làm hết việc cleanup trong handler cho gọn? (Gợi ý: handler chạy bất đồng bộ, chen vào giữa code chính.)

Lý do handler chỉ đặt cờ: signal handler chạy bất đồng bộ, chen vào giữa bất kỳ lệnh nào của chương trình chính — kể cả giữa lúc chương trình đang ở trong malloc hay printf. Một hàm async-signal-safe là hàm được phép gọi an toàn bên trong handler — nó reentrant (gọi lồng/ngắt giữa chừng vẫn đúng), không đụng tới lock, malloc hay buffer dùng chung với luồng chính (danh sách đầy đủ ở signal-safety(7)). Gọi hàm không async-signal-safe trong handler (như printf) có thể gây deadlock hoặc hỏng dữ liệu. Pattern an toàn: handler chỉ đặt một cờ kiểu volatile sig_atomic_t. Từ khoá volatile buộc compiler đọc lại biến từ bộ nhớ mỗi lần thay vì cache trong register — để vòng lặp chính thật sự thấy thay đổi mà handler vừa ghi; còn sig_atomic_t là kiểu mà đọc/ghi một lần là nguyên tử, không bị signal chen ngang giữa chừng. Vòng lặp chính thấy cờ rồi tự cleanup ở ngữ cảnh bình thường.

docker stop = SIGTERM, rồi SIGKILL sau 10 giây

Đây là graceful shutdown ngoài đời. Theo tài liệu Docker, docker stop gửi SIGTERM tới tiến trình chính của container, chờ một khoảng ân hạn (mặc định 10 giây cho Linux container, chỉnh bằng cờ -t/--timeout), rồi nếu tiến trình vẫn chưa thoát mới gửi SIGKILL để giết cứng. Vì vậy nếu app của bạn bắt SIGTERM và cleanup nhanh, container tắt tức thì; nếu app phớt lờ SIGTERM, bạn phải chờ đúng 10 giây rồi nó bị SIGKILL — mất cleanup. Kubernetes dùng cùng mẫu: gửi SIGTERM, chờ terminationGracePeriodSeconds, rồi SIGKILL.

5. Pitfall tổng hợp

Nhầm 1 — tưởng kill luôn là "giết chết ngay":

kill 4821        # gui SIGTERM (15) -- yeu cau dung LICH SU, khong phai giet ngay
kill -9 4821     # day moi la SIGKILL -- giet cung

kill <pid> mặc định gửi SIGTERM, cho tiến trình cơ hội cleanup. Chỉ kill -9 (hoặc kill -KILL) mới là SIGKILL không cưỡng lại được. Thói quen tốt: kill (SIGTERM) trước, chờ vài giây, kill -9 chỉ khi nó lì.

Nhầm 2 — làm việc nặng (I/O, malloc) ngay trong signal handler:

void handler(int sig) {
    printf("Shutting down...\n");   // RUI RO: printf khong async-signal-safe
    save_to_database();            // RAT RUI RO: co the deadlock/hong du lieu
}

✅ Handler chen bất đồng bộ vào giữa code chính, kể cả giữa một lời gọi thư viện. Chỉ làm thao tác async-signal-safe — điển hình là đặt một cờ volatile sig_atomic_t. Cleanup thật để vòng lặp chính làm sau khi thấy cờ.

Nhầm 3 — cố bắt SIGKILL để "chống bị giết":

signal(SIGKILL, my_handler);   // VO ICH: SIGKILL khong the bat

SIGKILLSIGSTOP không thể catch/block/ignore — lời gọi trên bị bỏ qua. Đừng thiết kế logic dựa trên việc "dọn dẹp khi bị SIGKILL": không có cơ hội đó. Mọi cleanup phải gắn vào SIGTERM (bắt được). SIGKILL là điểm cụt.

6. 📚 Deep Dive

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

Nguồn chuẩn:

  • signal(7) — man7.org — bảng đầy đủ mọi signal (số, default action, giải thích), câu chốt "SIGKILLSIGSTOP không thể catch/block/ignore", và khái niệm async-signal-safe.
  • kill(2) — man7.org — system call gửi signal tới tiến trình; nền cho lệnh kill của shell.
  • sigaction(2) — man7.org — cách đăng ký handler hiện đại (thay signal() cũ), với các cờ điều khiển hành vi chính xác.
  • Docker docker stop — SIGTERM, grace period mặc định 10s, rồi SIGKILL; cờ -t/--timeout.
  • OSTEP — chương "Process API" (file cpu-api.pdf) — mục signals trong quan hệ tiến trình.

Ghi chú: Số của các signal cơ bản (SIGINT=2, SIGKILL=9, SIGTERM=15) ổn định trên x86/ARM Linux, nhưng vài signal (SIGSTOP là 19 trên x86/ARM) có số khác nhau theo kiến trúc — nên trong code luôn dùng tên hằng (SIGKILL) thay vì số trần. signal(7) liệt kê cả biến thể theo kiến trúc.

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

  • Zombie & orphan: SIGCHLD là signal kernel gửi cho cha khi con chết — cơ sở để reap không block (bắt SIGCHLD, gọi waitpid với WNOHANG).
  • fork, exec, wait: disposition signal được thừa kế qua fork và (một phần) reset qua exec — vòng đời signal gắn với vòng đời tiến trình.
  • System call là gì: kill, sigaction là system call; và việc SIGKILL không bắt được là biểu hiện của ranh giới quyền lực kernel/user.
  • Mini-challenge — mổ xẻ cây tiến trình: bạn sẽ tự gửi SIGTERM vs SIGKILL lên một tiến trình thật và quan sát khác biệt.

8. Tóm tắt

  • SIGKILL (9)SIGSTOP (19) không thể catch/block/ignore — kernel thực thi trực tiếp; van an toàn đảm bảo OS luôn kiểm soát được tiến trình.
  • Graceful shutdown: handler bắt SIGTERM chỉ đặt cờ (atomic, async-signal-safe — không gọi malloc/printf trong handler); vòng lặp chính đọc cờ và cleanup thật.
  • docker stop: SIGTERM → chờ 10 giây (mặc định, đặt bằng -t) → SIGKILL; K8s terminationGracePeriodSeconds cần lớn hơn timeout cleanup.
  • Dùng tên hằng (SIGKILL, SIGTERM, SIGSTOP) thay vì số trần (9, 15, 19) — số thay đổi theo kiến trúc (ví dụ SIGSTOP là 19 trên x86 nhưng 23 trên MIPS).

9. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao Ctrl+C dừng được script này nhưng service kia phải kill -9? Giải thích theo signal nào được gửi và tiến trình phản ứng ra sao.
  2. Q2
    SIGKILL và SIGSTOP khác mọi signal khác ở điểm gì, và vì sao OS cố tình thiết kế chúng như vậy?
  3. Q3
    Vì sao trong signal handler ta chỉ nên đặt một cờ (stop = 1) thay vì làm cleanup (đóng DB, ghi log) ngay tại đó?
  4. Q4
    Mô tả trình tự docker stop thực hiện để dừng một container, và vì sao app nên bắt SIGTERM.
  5. Q5
    Vì sao code không nên đăng ký handler cho SIGKILL để 'dọn dẹp khi bị giết', và cleanup nên gắn vào signal nào?

Bài tiếp theo: Mini-challenge — mổ xẻ cây tiến trình thật

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

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

Bài tiếp theo

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