This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Hiểu cách quản lý tiến trình (process)tín hiệu (signals) là kỹ năng thiết yếu khi làm việc với Bash. Bài học này sẽ giúp bạn nắm vững cách chạy, kiểm soát và dọn dẹp tiến trình một cách chuyên nghiệp — đặc biệt quan trọng khi viết script tự động hóa.

1. Process Basics — Cơ bản về tiến trình

Mỗi chương trình đang chạy trên hệ thống là một process. Mỗi process có một PID (Process ID) duy nhất và một PPID (Parent PID) chỉ ra tiến trình cha đã tạo ra nó.

process_basics.sh
#!/bin/bash

# Xem PID của shell hiện tại
echo "PID của script này: $$"
echo "PPID (parent): $PPID"

# Liệt kê tất cả process
ps aux                    # BSD style — đầy đủ thông tin
ps -ef                    # System V style

# Lọc process theo tên
ps aux | grep nginx
ps aux | grep -v grep | grep nginx    # Loại bỏ chính lệnh grep

# pgrep — tìm PID theo tên (gọn hơn ps | grep)
pgrep -l nginx            # Liệt kê PID và tên
pgrep -u www-data          # Process của user cụ thể

# Process tree — xem quan hệ cha-con
pstree                     # Toàn bộ cây process
pstree -p $$               # Cây từ shell hiện tại (kèm PID)

# top / htop — monitor real-time
top -n 1 -b | head -20    # Chạy 1 lần, lấy 20 dòng đầu
# htop                     # Interactive (cần cài đặt)

# Thông tin chi tiết về 1 process
cat /proc/$$/status        # Linux: thông tin từ procfs
cat /proc/$$/cmdline       # Lệnh đã chạy

Fork & Exec

Khi bạn chạy một lệnh trong Bash, hệ thống thực hiện 2 bước:

  • fork(): tạo một bản sao (child process) của shell hiện tại
  • exec(): thay thế child process bằng chương trình mới

Đây là cơ chế cơ bản của mọi tiến trình trong Unix/Linux.

2. Foreground & Background Jobs

Bash cho phép bạn chạy lệnh ở foreground (chiếm terminal) hoặc background (chạy ngầm). Đây là nền tảng của job control.

jobs_control.sh
#!/bin/bash

# Chạy lệnh ở background với &
sleep 60 &
echo "Sleep đang chạy background, PID: $!"

# Liệt kê jobs hiện tại
jobs            # Hiển thị tất cả jobs
jobs -l         # Kèm PID

# Ctrl+Z: suspend (tạm dừng) job foreground
# → Job chuyển sang trạng thái "Stopped"

# fg: đưa job về foreground
fg %1           # Đưa job 1 về foreground
fg              # Đưa job gần nhất về foreground

# bg: tiếp tục chạy job ở background
bg %1           # Chạy tiếp job 1 ở background

# Ví dụ workflow thực tế:
# 1. Chạy lệnh dài: make -j4
# 2. Ctrl+Z → suspended
# 3. bg → tiếp tục ở background
# 4. Làm việc khác...
# 5. fg → quay lại xem kết quả

nohup, disown & wait

nohup_wait.sh
#!/bin/bash

# nohup — chạy lệnh immune với SIGHUP (đóng terminal không bị kill)
nohup long_running_task.sh &
# Output ghi vào nohup.out

# nohup với redirect output
nohup ./backup.sh > backup.log 2>&1 &
echo "Backup PID: $!"

# disown — bỏ job khỏi job table (terminal đóng không ảnh hưởng)
long_task &
disown %1
# Hoặc disown -h %1 (giữ trong job table nhưng ignore SIGHUP)

# wait — đợi background process hoàn thành
pid1=$!
sleep 5 &
pid2=$!

echo "Đang đợi 2 process..."
wait $pid1 $pid2
echo "Cả 2 đã xong!"

# wait không có argument: đợi TẤT CẢ background jobs
for i in {1..5}; do
    process_data "file_$i.csv" &
done
wait
echo "Tất cả 5 file đã xử lý xong!"

# wait với exit code
some_command &
wait $!
echo "Exit code: $?"

3. Signals — Tín hiệu

Signals là cơ chế giao tiếp giữa các process trong Unix. Khi bạn nhấn Ctrl+C, thực ra bạn đang gửi signal SIGINT đến process foreground.

signals.sh
#!/bin/bash

# Các signal quan trọng:
# SIGHUP  (1)  — Terminal đóng / reload config
# SIGINT  (2)  — Ctrl+C, yêu cầu dừng
# SIGQUIT (3)  — Ctrl+\, dừng + core dump
# SIGKILL (9)  — Buộc dừng ngay (KHÔNG THỂ bắt/bỏ qua)
# SIGTERM (15) — Yêu cầu dừng lịch sự (mặc định của kill)
# SIGSTOP (19) — Tạm dừng (KHÔNG THỂ bắt/bỏ qua)
# SIGCONT (18) — Tiếp tục sau khi bị stop

# kill — gửi signal đến process
kill 12345              # Gửi SIGTERM (mặc định)
kill -15 12345          # Tương tự — SIGTERM
kill -9 12345           # SIGKILL — buộc dừng (biện pháp cuối)
kill -SIGTERM 12345     # Dùng tên signal

# killall — kill theo tên process
killall nginx           # Kill tất cả process tên "nginx"
killall -9 python3      # Force kill tất cả python3

# pkill — kill theo pattern (linh hoạt hơn killall)
pkill -f "python.*server"    # Kill process có cmdline khớp pattern
pkill -u john                # Kill tất cả process của user john
pkill -SIGHUP nginx          # Gửi SIGHUP (thường dùng reload config)

# Liệt kê tất cả signal
kill -l

# Gửi signal cho chính mình
kill -0 $$              # Test xem process còn sống không (exit code 0 = có)

4. trap — Bắt và xử lý Signal

trap là lệnh quan trọng nhất trong bài. Nó cho phép script của bạn bắt signal và thực thi code xử lý — thường dùng để dọn dẹp tài nguyên (cleanup) khi script kết thúc.

trap_basics.sh
#!/bin/bash

# Cú pháp: trap 'commands' SIGNAL [SIGNAL...]

# Cleanup khi script kết thúc (bất kể lý do)
cleanup() {
    echo "Đang dọn dẹp..."
    rm -f /tmp/my_script_*.tmp
    echo "Dọn dẹp xong!"
}
trap cleanup EXIT

# Bắt Ctrl+C
trap 'echo "Bạn nhấn Ctrl+C! Đang thoát..."; exit 1' INT

# Bắt nhiều signal cùng lúc
trap 'echo "Nhận được signal dừng"; exit 1' INT TERM HUP

# Bắt lỗi — chạy khi có lệnh trả về exit code != 0
trap 'echo "LỖI ở dòng $LINENO, exit code: $?"' ERR

# Bỏ qua signal (ví dụ: không cho Ctrl+C dừng script)
trap '' INT              # Chuỗi rỗng = bỏ qua
echo "Bạn không thể dừng tôi bằng Ctrl+C!"
sleep 10
trap - INT               # Reset về hành vi mặc định

Ví dụ thực tế: Script với lockfile và cleanup

lockfile_example.sh
#!/bin/bash
# Script an toàn với lockfile — chỉ cho phép 1 instance chạy

LOCKFILE="/tmp/my_deploy.lock"
LOGFILE="/var/log/deploy.log"
TMPDIR=$(mktemp -d)

# Hàm cleanup — luôn chạy khi script kết thúc
cleanup() {
    local exit_code=$?
    echo "[$(date)] Script kết thúc với code: $exit_code" >> "$LOGFILE"
    rm -f "$LOCKFILE"
    rm -rf "$TMPDIR"
    echo "Đã dọn dẹp lockfile và thư mục tạm."
}
trap cleanup EXIT

# Hàm xử lý khi bị interrupt
on_interrupt() {
    echo ""
    echo "⚠ Script bị interrupt! Đang rollback..."
    # Thực hiện rollback nếu cần
    exit 130    # 128 + signal number (SIGINT=2)
}
trap on_interrupt INT TERM

# Kiểm tra lockfile — tránh chạy đồng thời
if [ -f "$LOCKFILE" ]; then
    existing_pid=$(cat "$LOCKFILE")
    if kill -0 "$existing_pid" 2>/dev/null; then
        echo "Script đang chạy (PID: $existing_pid). Thoát."
        exit 1
    else
        echo "Lockfile cũ tồn tại nhưng process đã chết. Tiếp tục..."
        rm -f "$LOCKFILE"
    fi
fi

# Tạo lockfile
echo $$ > "$LOCKFILE"
echo "[$(date)] Deploy bắt đầu (PID: $$)" >> "$LOGFILE"

# === Logic chính ===
echo "Đang deploy..."
sleep 5    # Giả lập công việc
echo "Deploy thành công!"
# cleanup sẽ tự động chạy nhờ trap EXIT

5. Subshells & Biến đặc biệt Process

Subshell là một child process chạy bản sao của shell hiện tại. Các thay đổi biến trong subshell không ảnh hưởng đến shell cha.

subshell.sh
#!/bin/bash

# Subshell với ()
x=10
(
    x=20
    echo "Trong subshell: x=$x"    # x=20
)
echo "Ngoài subshell: x=$x"        # x=10 (không bị ảnh hưởng!)

# Ứng dụng: chạy lệnh trong thư mục khác mà không ảnh hưởng cwd
(cd /tmp && ls)        # cd chỉ ảnh hưởng trong subshell
pwd                     # Vẫn ở thư mục cũ

# Các biến đặc biệt về process
echo "PID hiện tại:        $$"
echo "PID thực (Bash 4+):  $BASHPID"
echo "PID parent:          $PPID"
echo "PID background gần nhất: $!"

# $$ vs $BASHPID trong subshell
echo "Shell: $$ / $BASHPID"
( echo "Subshell: $$ / $BASHPID" )
# $$ giữ nguyên PID shell cha, $BASHPID thay đổi

# PIPESTATUS — exit code của từng lệnh trong pipeline
false | true | false
echo "PIPESTATUS: ${PIPESTATUS[@]}"    # 1 0 1
# So sánh: $? chỉ cho exit code của lệnh CUỐI CÙNG

Process Substitution — <() và >()

process_substitution.sh
#!/bin/bash

# Process Substitution: <(command) tạo file descriptor tạm
# cho phép dùng output của lệnh như thể nó là file

# So sánh 2 thư mục
diff <(ls dir1/) <(ls dir2/)

# So sánh output của 2 lệnh
diff <(sort file1.txt) <(sort file2.txt)

# Đọc vào biến mà không mất biến do subshell (khác với pipe)
# SAI — biến bị mất do pipe tạo subshell:
count=0
cat file.txt | while read line; do
    ((count++))
done
echo "Count: $count"    # Luôn = 0!

# ĐÚNG — dùng process substitution:
count=0
while read line; do
    ((count++))
done < <(cat file.txt)
echo "Count: $count"    # Giá trị chính xác!

# >() — ghi output vào process
tee >(grep "error" > errors.log) >(grep "warn" > warnings.log) < app.log

# Ví dụ thực tế: paste 2 output song song
paste <(cut -d',' -f1 data.csv) <(cut -d',' -f3 data.csv)

6. Câu hỏi trắc nghiệm ôn tập

Trắc nghiệm: trap & Signals

Lệnh trap cleanup EXIT sẽ chạy hàm cleanup khi nào?

Trắc nghiệm: Subshell & Process

Biến x có giá trị gì sau đoạn code: x=5; (x=10); echo $x?

Tải file code thực hành minh họa bài học

File script tổng hợp ví dụ về process management, signals, trap, subshells và process substitution.

Tải về process_signals.sh

Related Articles

Bài viết liên quan trong series

Lesson 7: Real-World Bash: Backup, Monitor & Deploy Scripts Bài 7: Thực Chiến Bash: Backup, Monitor & Deploy Scripts Lesson 5: Text Processing in Bash: Pipeline, grep, sed, awk & Redirection Bài 5: Xử Lý Text trong Bash: Pipeline, grep, sed, awk & Redirection Back to Bash Series Overview Quay lại Lộ trình Bash Series