Bài 03
Tương tác với hệ thống: redirect, pipe, signal, process, trap
Bài 03 — Tương tác với hệ thống: redirect, pipe, signal, process, trap
Script đơn giản print ra terminal là xong. Production script cần: log ra file, lọc output bằng pipe, xử lý Ctrl+C khi đang chạy, biết khi nào service crash, graceful shutdown trong container.
Bài này là phần "thấu hiểu OS" — không chỉ học cú pháp, mà hiểu shell tương tác với kernel ra sao: file descriptor là gì ở mức process table, fork/exec/wait là chuỗi syscall gì, daemon được tạo bằng cách nào, signal là cơ chế IPC như thế nào.
Shell Redirection
Shell Redirection là một tính năng mạnh mẽ trong các shell của Unix/Linux. Nó cho phép kiểm soát cách dữ liệu được gửi vào hoặc ra khỏi chương trình, file, hoặc thiết bị. Đặc biệt hữu ích khi muốn lưu output, xử lý dữ liệu, hoặc tương tác với file mà không cần giao diện trực tiếp.
3 stream chuẩn (file descriptor / FD):
0 - Đầu vào - Bàn phím (stdin)
1 - Đầu ra - Màn hình (stdout)
2 - Lỗi - Màn hình (stderr)Đây là số POSIX chuẩn, còn được gọi là mô tả tệp (FD). Mỗi lệnh Linux ít nhất mở các stream trên để giao tiếp với user hoặc chương trình hệ thống khác.
Input — chuyển hướng đầu vào
Cấu trúc:
command < filenameCòn được gọi là stdin. Đầu vào chuẩn mặc định là bàn phím. < là ký hiệu chuyển hướng đầu vào.
Output — chuyển hướng đầu ra
Ghi dữ liệu output của chương trình vào file thay vì hiển thị trên màn hình (stdout):
command > output.file # ghi đè
command >> output.file # append (ghi tiếp vào cuối)
echo "Hello" > out.txt # tạo file out.txt với "Hello"
echo "World" >> out.txt # thêm "World" vào cuốiError — chuyển hướng lỗi
Chuyển đầu ra lỗi (stderr) vào file:
command 2> errors.txt # ghi đè
command 2>> errors.txt # appendTạo file rỗng
Cấu trúc:
> filenameRedirect không có command → tạo file rỗng (hoặc truncate file đang có). Hữu ích để clear log file đang được app ghi tiếp (không phải xoá rồi tạo lại — file inode khác, app vẫn ghi vào inode cũ).
/dev/null — loại bỏ output không mong muốn
/dev/null là một thiết bị đặc biệt trong Unix/Linux. Hoạt động như một "hố đen" — mọi dữ liệu gửi vào đều bị loại bỏ mà không để lại dấu vết. Rất hữu ích khi muốn loại bỏ output không mong muốn.
Cấu trúc:
command > /dev/null # bỏ stdout
command 2> /dev/null # bỏ stderr
command > /dev/null 2>&1 # bỏ cả stdout và stderr
command &> /dev/null # shortcut, tương đương trênDùng khi chỉ quan tâm exit status:
if grep -q "ERROR" /var/log/app.log > /dev/null 2>&1; then
alert
fiStdout vs stderr — phân biệt khi viết script
Trong script, viết stderr cho error message:
echo "Backup OK" # info → stdout
echo "ERROR: ..." >&2 # error → stderrHabit này quan trọng khi script bị pipe — error vẫn ra terminal, stdout đi vào pipe sạch.
File descriptor — góc nhìn OS
Tài liệu hay lướt qua "FD là số 0/1/2". Thực tế ở mức kernel, FD là integer index vào bảng file mở của process.
Mỗi process có một file descriptor table — array nhỏ map FD number → kernel file object. Khi process gọi open("/tmp/log"), kernel:
- Tìm slot trống nhỏ nhất trong bảng FD (thường 3, vì 0/1/2 đã dùng).
- Tạo kernel file object.
- Trỏ slot 3 → file object đó.
- Trả về số 3 cho process.
Process sau đó dùng số 3 cho read(), write(), close().
Kiểm tra FD đang mở của process:
ls -l /proc/<PID>/fd/
# 0 -> /dev/pts/0
# 1 -> /dev/pts/0
# 2 -> /dev/pts/0
# 3 -> /var/log/myapp.log/proc/<PID>/fd/ là window xem file descriptor table thật của process — debug "process này đang mở file/socket nào".
Here Document (Heredoc)
Here Document là phương pháp trong shell scripting của Unix/Linux để truyền nhiều dòng văn bản làm đầu vào cho một lệnh hoặc chương trình. Đây là cách thay thế hiệu quả cho việc nhập liệu trực tiếp hoặc sử dụng file tạm.
Chủ yếu dùng EOF làm marker. Cấu trúc:
command <<EOF
nội_dung
EOFVí dụ tạo config file:
cat <<EOF > config.conf
[server]
address=127.0.0.1
port=8080
EOFVariable trong heredoc được expand. Không muốn expand → quote EOF:
cat <<'EOF' > script.sh
echo "$VAR" # giữ nguyên $VAR, không expand
EOFUse case kinh điển:
mysql -uroot -p mydb <<EOF
CREATE TABLE users (id INT, name VARCHAR(50));
INSERT INTO users VALUES (1, 'admin');
EOF
ssh server <<'EOF'
cd /var/www
git pull origin main
systemctl reload nginx
EOFHere Strings
Here Strings là tính năng trong shell (như Bash) dùng để cung cấp một chuỗi văn bản ngắn gọn làm đầu vào cho một lệnh. Cách thay thế ngắn gọn hơn so với Here Document khi chỉ cần truyền một dòng.
Cấu trúc:
command <<< "chuỗi_văn_bản"Ví dụ:
cat <<< "Hello, World!"
# đọc dữ liệu đầu vào và in ra màn hình
grep "admin" <<< "admin user added"
# Tương đương: echo "admin user added" | grep "admin"File descriptor tuỳ ý với exec
Bash hỗ trợ mở FD số bất kỳ (≥ 3) cho file:
exec fd> output.txt
# fd >= 3Ví dụ:
#!/bin/bash
# Gán fd 3 cho output file
exec 3> /tmp/output.txt
# Ghi vào fd 3
echo "This is a test" >&3
# Ghi date output vào fd 3
date >&3
# Đóng fd 3
exec 3<&-3 mode: read, write, read+write
Bash hỗ trợ cú pháp sau để mở file đọc/ghi vào file descriptor:
exec fd<>fileNameGiải thích:
exec fd< filename— mở file ở chế độ ĐỌC.exec fd> filename— mở file ở chế độ GHI.exec fd<>fileName— mở file ở chế độ ĐỌC và GHI.
Ví dụ:
#!/bin/bash
FILENAME="/tmp/out.txt"
# Mở fd 3 cho /tmp/out.txt — đọc và ghi
exec 3<>$FILENAME
# Ghi
echo "Today is $(date)" >&3
echo "Fear is the path to the dark side." >&3
echo "--- Yoda" >&3
# Đóng fd 3
exec 3>&-read -u — đọc từ FD tuỳ ý
Sử dụng read -u cho phép đọc dữ liệu từ một file descriptor (FD) tùy chỉnh thay vì từ stdin. Hữu ích khi cần đọc từ file hoặc luồng dữ liệu khác ngoài bàn phím hoặc đầu vào mặc định.
Cấu trúc:
read -u fd var1 var2 ... varNVí dụ:
#!/bin/bash
# Mở fd 3 ở chế độ đọc
exec 3< /etc/resolv.conf
# Mở fd 4 ở chế độ ghi
exec 4> /tmp/output.txt
# Đọc dữ liệu từ fd 3, gán vào biến a, b
read -u 3 a b
# Hiển thị
echo "Data read from fd 3:"
echo "$a $b"
# Ghi cùng dữ liệu vào fd 4
echo "Field #1 - $a" >&4
echo "Field #2 - $b" >&4
# Đóng fd 3 và 4
exec 3<&-
exec 4<&-Pipes và Filters
Trong lập trình shell, pipes và filters là 2 khái niệm cơ bản giúp kết nối các lệnh với nhau, cho phép xử lý dữ liệu theo chuỗi mà không cần lưu trữ tạm vào file. Đây là cách tiếp cận liên kết lệnh (command chaining) mạnh mẽ trong shell.
Pipe |
Pipes trong shell được sử dụng để chuyển dữ liệu từ lệnh này sang lệnh khác. Dữ liệu được chuyển qua pipe giữa các lệnh dưới dạng dòng (stream) mà không cần lưu vào file.
Cú pháp:
command1 | command2command1: lệnh đầu tiên, xuất ra dữ liệu (stdout).command2: lệnh thứ hai, nhận dữ liệu từ stdin của lệnh đầu.
Ví dụ:
ps aux | grep nginx | wc -l # đếm process nginx
cat /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10Pipe ở mức kernel: shell tạo pipe (cặp FD đọc + ghi), fork 2 subprocess, child A redirect stdout vào đầu ghi, child B redirect stdin vào đầu đọc. Hai child chạy song song.
⚠️ Pipe chỉ pipe stdout. Stderr không qua pipe:
make 2>&1 | grep error # cần 2>&1 để stderr cũng vào pipeFilters
Filters là các lệnh hoặc chương trình có chức năng xử lý, chuyển đổi hoặc lọc dữ liệu đầu vào và xuất ra kết quả đã xử lý. Filters thường làm việc với dữ liệu từ stdin và gửi kết quả ra stdout.
Các lệnh phổ biến dùng làm filters:
grep: tìm kiếm chuỗi trong dữ liệu.awk: xử lý và phân tích dữ liệu văn bản theo cột.sed: thay thế hoặc chỉnh sửa văn bản.sort: sắp xếp dữ liệu.cut: cắt bỏ hoặc lựa chọn cột từ dữ liệu.
Ví dụ phổ biến:
grep "pattern" file # tìm dòng match
grep -v "pattern" # invert (không match)
grep -i "PATTERN" # case insensitive
grep -E "p1|p2" # regex extended
awk '{print $1, $3}' file # cột 1 và 3
awk -F: '{print $1}' /etc/passwd # delimiter là :
sed 's/old/new/g' file # thay thế (in stdout)
sed -i 's/old/new/g' file # in-place
cut -d: -f1 /etc/passwd # cột 1, delimiter :
sort | uniq -c | sort -rn # đếm + sắp xếp ngược
head -n 10 # 10 dòng đầu
tail -f log.txt # follow file (real-time)
wc -l # đếm dòngSử dụng pipes với multiple commands
Có thể kết hợp nhiều lệnh với pipe để tạo chuỗi xử lý phức tạp hơn:
cat file.txt | grep "pattern" | sort | uniqPipe cho stderr (2>&1)
Dùng để kết hợp đầu ra lỗi (stderr) với đầu ra chuẩn (stdout):
ls /nonexistent_directory 2>&1 | grep "No such file or directory"
# 2>&1: chuyển stderr sang stdout
# | grep: lọc thông báo lỗiPipe với process substitution <() >()
<(command): tạo "file ảo" chứa output của command, lệnh khác đọc được như file.>(command): tạo "file ảo" mà lệnh khác có thể ghi vào.
Ví dụ:
diff <(echo "apple") <(echo "orange")
# So sánh output 2 lệnh
# Xem khác biệt config 2 server
diff <(ssh server1 cat /etc/nginx/nginx.conf) \
<(ssh server2 cat /etc/nginx/nginx.conf)Ở mức OS, bash tạo named pipe (FIFO) dưới /dev/fd/<n>, command đọc/ghi pipe đó như file thường.
Ví dụ thực tế
# Tìm các dòng lỗi trong file log
cat /var/log/syslog | grep "error" | sort | uniqSignals
Linux hỗ trợ cả tín hiệu đáng tin cậy POSIX ("tín hiệu chuẩn") và tín hiệu thời gian thực POSIX. Tín hiệu được gửi đến một tiến trình hoặc lệnh để thông báo sự kiện đã xảy ra.
Đây là một dạng inter-process communication (IPC) — process gửi signal, kernel deliver, process nhận xử lý.
Khi process nhận signal, kernel pause execution, gọi handler tương ứng (mặc định hoặc custom set bằng signal()/sigaction() syscall), sau đó tiếp tục.
Kiểm tra các tín hiệu được hỗ trợ:
kill -lHoặc đọc header file kernel:
more /usr/include/linux/signal.h(Tuỳ distro path có thể là /usr/include/asm/signal.h hoặc /usr/include/asm-generic/signal.h.)
Process
Linux là hệ điều hành đa người dùng (nhiều người dùng có thể đăng nhập và chia sẻ tài nguyên của nó) và đa nhiệm. Nghĩa là bạn có thể chạy nhiều lệnh và thực hiện nhiều tác vụ cùng lúc.
Trong Linux, khi bạn bắt đầu một quy trình, nó được cấp một số duy nhất gọi là PID hoặc process-id.
- PID range thường từ 1 đến 65535 (modern Linux có thể cao hơn, xem
/proc/sys/kernel/pid_max). - PID 1 luôn được gán cho quy trình init, đây là quy trình đầu tiên được bắt đầu khi khởi động (trên distro modern là systemd).
Mỗi process có:
- PID — số định danh duy nhất.
- PPID — PID của parent process.
- State — Running (R), Sleeping (S), Stopped (T), Zombie (Z).
- UID/GID — quyền sở hữu.
- Memory map, open files (FD table), signal handlers, priority.
Parent — Child
Tiến trình cha là tiến trình Linux đã tạo ra một hoặc nhiều tiến trình con. Tiến trình có thể phân nhánh tiến trình con, tức là tạo một tiến trình con.
Hàm fork()
Hàm fork() tạo một bản sao của tiến trình hiện tại. Tiến trình cha tiếp tục chạy, và tiến trình con được tạo ra từ cùng một điểm gọi fork().
- Trong tiến trình cha,
fork()trả về PID của tiến trình con. - Trong tiến trình con,
fork()trả về giá trị 0.
pid_t pid = fork();
if (pid == 0) {
// Đây là CHILD
printf("I am child, PID = %d\n", getpid());
} else if (pid > 0) {
// Đây là PARENT
printf("I am parent, child PID = %d\n", pid);
}Sau khi gọi fork(), cả tiến trình cha và con chạy song song.
getpid() và getppid()
getpid(): trả về PID của tiến trình hiện tại.getppid(): trả về PID của tiến trình cha.
Trong bash:
echo $$ # PID của shell hiện tại — bash dùng getpid()
echo $PPID # PID của parent — bash dùng getppid()exec() — thay process
Sau fork(), child thường gọi exec() để load chương trình mới vào chính process đó. Memory cũ bị thay, nhưng PID và FD table giữ nguyên.
Đó là cách shell chạy command:
shell (PID 1234)
↓ fork()
shell (1234) + child clone (1235)
↓ exec("/bin/ls")
ls (1235) — vẫn PID 1235, nhưng giờ chạy code của lswait() — parent thu dọn
Khi child kết thúc, kernel giữ entry trong process table cho đến khi parent gọi wait() để thu thập. Sau wait(), entry mới được xoá hoàn toàn. wait() là chức năng để tiến trình cha chờ và thu thập thông tin về tiến trình con của mình.
Bash tự gọi wait() cho mọi command foreground. Background command (cmd &) cần explicit wait:
sleep 100 &
PID=$!
wait $PIDKiểm tra process khi chương trình chạy
ps -ef | grep <tên chương trình>Tiến trình mồ côi (Orphan) và Tiến trình zombie (Zombie)
Orphan Process
Nếu tiến trình cha thoát trước khi tiến trình con hoàn thành, tiến trình con sẽ trở thành mồ côi.
Hệ thống sẽ gán tiến trình con cho init (PID 1) làm cha mới. Init nhận trách nhiệm wait() cho orphan khi nó xong.
Bình thường không phải vấn đề. Daemon thực ra dùng cơ chế này.
Zombie Process
Khi tiến trình con hoàn thành nhưng tiến trình cha chưa gọi wait() để thu dọn tài nguyên, tiến trình con sẽ chuyển thành zombie.
Trạng thái zombie giữ lại thông tin của tiến trình con trong bảng tiến trình.
Một zombie không hại. Nhưng nếu app cha có bug không handle SIGCHLD đúng → zombie tích tụ → process table đầy → kernel không fork được nữa → toàn hệ thống "hết chỗ tạo process".
Xem zombie:
ps aux | awk '$8=="Z"'
ps -ef | grep defunctDevOps thấy zombie tích tụ → không kill được zombie (đã chết rồi). Phải:
- Tìm parent:
ps -o ppid <zombie-pid>. - Kill parent → init adopt zombie → init
wait()→ zombie biến mất. - Hoặc fix bug ở app cha (handle SIGCHLD).
Hệ thống xử lý tiến trình cơ bản
Mọi chương trình chạy trên Linux đều là một process. Process có thể là:
- Tiến trình hệ thống (daemon): chạy nền cung cấp dịch vụ như
sshd,cron,nginx. - Tiến trình người dùng: ứng dụng do user chạy như
vim,bash.
Quản lý tiến trình cha-con
Khi chương trình mới chạy, Linux tạo tiến trình con từ tiến trình cha hiện có:
- Tiến trình cha gọi
fork()để tạo con. - Tiến trình con thực hiện công việc hoặc thay thế bằng chương trình khác qua
exec().
Ví dụ thực tế: tiến trình init hoặc systemd (tiến trình đầu tiên trên hệ thống) là cha của hầu hết các tiến trình khác.
Daemon process — quản lý trên máy chủ Linux
Các dịch vụ như web server (nginx), DB (mysql), SSH (sshd) hoạt động trong nền dưới dạng tiến trình daemon. Daemon được quản lý bởi systemd hoặc init.
Hoạt động thực tế:
- Khi máy chủ khởi động, systemd khởi chạy các tiến trình nền cần thiết.
- Mỗi dịch vụ chạy dưới dạng một tiến trình độc lập.
- Các tiến trình nền sinh thêm tiến trình con để xử lý công việc cụ thể.
Ví dụ: nginx tạo tiến trình con để xử lý các request HTTP từ client.
systemd không cần gọi fork() để tạo nginx; thay vào đó, systemd dùng tệp binary (như /usr/sbin/nginx) để khởi chạy nginx. Sau khi nginx được khởi động, nginx sẽ dùng fork() để tạo các tiến trình con (worker processes) xử lý request HTTP.
Khi tiến trình con hoàn thành, tiến trình con gửi tín hiệu để tiến trình cha gọi wait() và thu dọn.
Cách daemon truyền thống được tạo (double-fork)
Daemon C truyền thống dùng pattern double-fork để tách khỏi terminal:
- Fork lần 1. Parent thoát ngay. Child mồ côi → init nhận làm cha (PPID=1).
setsid()— child tách khỏi controlling terminal, trở thành leader của session mới.- Fork lần 2. Để đảm bảo daemon không thể tự reclaim terminal.
- Đổi working directory về
/. umask(0)— reset umask.- Đóng FD 0, 1, 2 hoặc redirect vào
/dev/null. - Bắt đầu logic daemon.
Modern Linux thường KHÔNG dùng pattern này — systemd quản lý daemon thay (Type=simple, forking, notify trong unit file). Systemd exec() trực tiếp binary, daemon không cần tự fork tách terminal.
Trạng thái tiến trình
Tiến trình trong Linux có nhiều trạng thái:
- Running: đang thực thi.
- Sleeping: chờ tài nguyên hoặc sự kiện.
- Stopped: bị dừng tạm thời.
- Zombie: đã kết thúc nhưng chưa được dọn dẹp bởi cha.
Công cụ quản lý process
Hiển thị tất cả tiến trình:
ps aux
ps
ps aux | less
ps aux | grep "process-name"
ps aux | grep "httpd"
ps alx | grep "mysqld"Theo dõi real-time:
top
htopQuan hệ cha-con:
pstree
pstree -p # kèm PIDQuản lý tiến trình:
kill <PID> # Kết thúc một tiến trình cụ thể (gửi SIGTERM)
nice / renice # Điều chỉnh độ ưu tiên của tiến trìnhnice / renice — process priority
Linux scheduler dùng giá trị nice (-20 đến +19) quyết định ưu tiên CPU. Nice thấp = ưu tiên cao. Mặc định nice = 0.
# Chạy command với nice +10 (priority thấp hơn)
nice -n 10 ./heavy-job.sh
# Đổi nice của process đang chạy
renice -n 5 -p <PID>
# Chỉ root mới đặt nice âm
sudo nice -n -10 ./critical-taskUse case DevOps: backup job đêm nên nice -n 19 để không tranh CPU với app:
0 2 * * * nice -n 19 /scripts/backup.shDanh sách các tín hiệu thường dùng
| Signal | Ý nghĩa | Ứng dụng | Catch được? |
|---|---|---|---|
| SIGHUP (1) | Yêu cầu reload config hoặc khởi động lại | Thay đổi cấu hình mà không tắt service (nginx, apache) | Có |
| SIGINT (2) | Tín hiệu ngắt — Ctrl+C trong terminal | Ngừng tiến trình đang chạy | Có |
| SIGQUIT (3) | Yêu cầu dừng + tạo core dump | Lỗi nghiêm trọng cần debug | Có |
| SIGKILL (9) | Buộc tiến trình dừng ngay lập tức | Khi tiến trình không phản hồi | Không |
| SIGSEGV (11) | Báo lỗi phân vùng bộ nhớ (segfault) | Truy cập bộ nhớ không hợp lệ | Có (hạn chế) |
| SIGTERM (15) | Yêu cầu kết thúc lịch sự (default kill) |
Yêu cầu kết thúc bình thường | Có |
| SIGCHLD (17) | Tiến trình con kết thúc hoặc đổi trạng thái | Cha thu dọn tài nguyên con | Có |
| SIGCONT (18) | Tiếp tục tiến trình đã tạm dừng | Sau SIGSTOP | Có |
| SIGSTOP (19) | Yêu cầu dừng ngay lập tức | Dừng không hủy bỏ | Không |
| SIGTSTP (20) | Tạm dừng (Ctrl+Z trong terminal) | Dừng tiến trình mà không kết thúc | Có |
| SIGUSR1, SIGUSR2 | Tín hiệu user-defined | App tự define ý nghĩa | Có |
Tín hiệu đặc biệt:
- SIGKILL và SIGTERM là 2 tín hiệu quan trọng nhất để dừng tiến trình. SIGKILL mạnh nhất và không thể từ chối.
- SIGHUP, SIGUSR1, SIGUSR2 thường được dùng để thông báo và thực hiện hành động tùy chỉnh.
(Số signal có thể khác trên architecture khác — ARM, PowerPC. Kiểm tra cụ thể bằng kill -l.)
Gửi tín hiệu
kill -<số tín hiệu> <PID>
kill -9 <PID> # SIGKILL
kill -HUP <PID> # SIGHUP (reload)
kill -TERM <PID> # SIGTERM rõ ràng
killall nginx
pkill -f "python app.py"Quy tắc dùng signal
- SIGTERM trước, SIGKILL sau. SIGTERM cho app cleanup (đóng connection, flush buffer, lưu state). Sau vài giây không respond, mới SIGKILL.
kill -9không phải reflex. App handle SIGTERM đúng → graceful shutdown. SIGKILL = data corruption nguy cơ.- SIGHUP để reload config thay restart. Không downtime:
kill -HUP $(pidof nginx) - SIGKILL và SIGSTOP không catch được — đặc tính kernel, đảm bảo admin luôn có cách kill/freeze process.
trap
Lệnh trap trong Linux được sử dụng để xử lý các tín hiệu (signals) trong shell script. Cho phép chỉ định các hành động mà shell sẽ thực hiện khi nhận một tín hiệu nhất định, giúp kiểm soát và xử lý các sự kiện bất ngờ hoặc tín hiệu như SIGINT, SIGTERM, SIGKILL, v.v.
Cú pháp:
trap 'command' signalVí dụ đơn giản:
#!/bin/bash
# Bắt tín hiệu EXIT (0)
trap 'echo "Exit signal detected..."' 0
echo "This is a test"
exit 0Pattern production — script chạy lâu với cleanup:
#!/bin/bash
set -euo pipefail
LOCK="/tmp/myscript.lock"
cleanup() {
echo "Cleaning up..."
rm -f "$LOCK"
}
# Bắt EXIT (chạy cleanup khi script kết thúc, dù OK hay error)
# Bắt SIGINT (Ctrl+C) và SIGTERM
trap cleanup EXIT INT TERM
if [[ -f "$LOCK" ]]; then
echo "Already running" >&2
exit 1
fi
touch "$LOCK"
echo "Working..."
sleep 100trap cleanup EXIT ở đầu script = cleanup chắc chắn chạy. Defensive programming pattern. Đưa vào template script production.
Subshell
Subshell (tiến trình con) là một phiên bản con của shell, được tạo ra khi một lệnh hoặc một tập lệnh được thực thi trong một shell khác biệt. Subshell cho phép thực thi các lệnh mà không ảnh hưởng đến môi trường của shell cha (shell gốc).
Tạo subshell bằng dấu ngoặc đơn ( ... ):
(echo "This is a subshell"; ls)Đặc điểm subshell
- Tách biệt môi trường — biến, working directory không leak ra shell cha.
- Các lệnh trong subshell độc lập.
- Subshell là tiến trình con thực sự, có thể tạo signal hoặc thay đổi thông tin tiến trình (qua
kill,wait, v.v.).
Ví dụ:
( cd /tmp && echo "Current directory is $(pwd)" )
echo "Current directory is $(pwd)"Lệnh cd vào /tmp và echo chạy trong subshell. Môi trường trong subshell thay đổi nhưng KHÔNG ảnh hưởng shell cha. Sau khi subshell kết thúc, shell cha vẫn ở thư mục ban đầu.
Subshell có PID riêng:
echo $$ # PID parent shell
( echo $BASHPID ) # PID subshell — khác $$Use case: thay đổi env tạm thời, set trap riêng, tránh leak biến:
(
export DEBUG=1
export LOG_LEVEL=verbose
./run-test.sh
)
# Sau ngoặc, env quay về như cũCompound command — lệnh hợp thành
Compound command là cách kết hợp nhiều lệnh lại với nhau để thực thi chúng trong một cách có tổ chức.
Dùng dấu chấm phẩy ;
Các lệnh được thực thi tuần tự, bất kể lệnh trước có thành công hay không:
command1; command2; command3
echo "Hello"; echo "World"; lsDùng && (AND), || (OR)
cmd1 && cmd2 # cmd2 chạy NẾU cmd1 OK
cmd1 || cmd2 # cmd2 chạy NẾU cmd1 failDùng & cho parallel
cmd1 & # chạy nền, không đợi
cmd1 & cmd2 & wait # parallel rồi wait tất cảPattern parallel:
backup_db &
backup_files &
backup_logs &
wait # đợi cả 3 xong
echo "All backups done"Lưu PID để kill sau:
long_task &
TASK_PID=$!
echo "Started: $TASK_PID"
kill "$TASK_PID"exec
Trong Linux và Unix, exec là một lệnh trong shell có tác dụng thay thế tiến trình hiện tại bằng một tiến trình mới. Khi exec được sử dụng, tiến trình hiện tại (chạy lệnh exec) sẽ bị thay thế hoàn toàn bằng tiến trình mới, và tiến trình này sẽ chạy trong cùng một không gian bộ nhớ và PID như tiến trình gốc.
Điều này có nghĩa là khi exec được gọi, các lệnh hoặc chương trình mới sẽ được thực thi mà KHÔNG tạo ra tiến trình con.
Cú pháp:
exec <command> [arguments...]Ví dụ:
exec /usr/bin/python /app/main.py
# Lệnh sau exec không bao giờ chạy (process đã bị thay)Use case quan trọng: wrapper script trong Docker entrypoint.
#!/bin/bash
# entrypoint.sh
export DB_HOST="prod-db"
export DB_PORT=5432
exec python /app/main.py "$@"
# python thay bash, giữ PID 1, signal forward đúngCâu chuyện thực tế — signal forwarding trong Docker
Service Python deploy production trong Docker. Container chạy bình thường, nhưng docker stop mất 10 giây chờ rồi force kill. Log không có "graceful shutdown".
Entrypoint script:
#!/bin/bash
export DB_URL="..."
python /app/main.pyVấn đề ở mức OS:
- Container start → bash chạy → bash
fork() + exec()python. - Bash giờ là PID 1 trong namespace container, python là PID 2.
docker stopgửi SIGTERM tới PID 1 = bash.- Bash đang ở
wait()chờ python — không có handler SIGTERM nên không forward. - Python không nhận SIGTERM → không cleanup.
- 10 giây sau Docker gửi SIGKILL tới PID 1 — bash chết, kernel cũng kill PID 2 (python).
- Connection DB bị reset, request đang xử lý dở mất data.
Fix dùng exec:
#!/bin/bash
export DB_URL="..."
exec python /app/main.py # exec → python THAY bash, trở thành PID 1Bây giờ:
- Container start → bash chạy.
exec python→ kernel giữ PID 1 nhưng load code python — bash biến mất, python là PID 1.docker stopgửi SIGTERM tới PID 1 = python trực tiếp.- Python handle SIGTERM, graceful shutdown trong 1-2 giây.
Bài học:
- Trong container, luôn
execlệnh cuối trong entrypoint script. - Hiểu process model (fork/exec/PID) để debug "tại sao app không nhận SIGTERM".
- Hoặc dùng
tini/dumb-initlàm PID 1:ENTRYPOINT ["tini", "--", "/app/entrypoint.sh"]
Cùng nguyên lý: K8s gửi SIGTERM → pod's PID 1 phải forward → app graceful shutdown trong terminationGracePeriodSeconds.
Lab
mkdir /tmp/sys-lab && cd /tmp/sys-lab
# Test redirect & pipe
cat > analyze.sh <<'EOF'
#!/bin/bash
set -euo pipefail
echo "=== Top 5 CPU ==="
ps aux --sort=-%cpu | head -6
echo
echo "=== Disk usage (>50%) ==="
df -h | awk 'NR==1 || $5+0 > 50' | grep -v tmpfs
EOF
chmod +x analyze.sh
./analyze.sh > report.txt 2> err.log
cat report.txt
# Tạo file rỗng
> /tmp/empty.log
ls -la /tmp/empty.log
# File descriptor tuỳ ý
exec 3> /tmp/myfd.log
echo "Line 1" >&3
echo "Line 2" >&3
exec 3>&-
cat /tmp/myfd.log
# read từ FD tuỳ ý
exec 4< /etc/hostname
read -u 4 host
echo "Hostname từ FD 4: $host"
exec 4<&-
# Trap signal
cat > robust.sh <<'EOF'
#!/bin/bash
set -euo pipefail
LOCK="/tmp/robust.lock"
cleanup() {
echo "Cleaning up..."
rm -f "$LOCK"
}
trap cleanup EXIT INT TERM
[[ -f "$LOCK" ]] && { echo "Already running"; exit 1; }
touch "$LOCK"
echo "Working... (Ctrl+C để test trap)"
for i in {1..20}; do
echo "Step $i"
sleep 1
done
EOF
chmod +x robust.sh
./robust.sh
# Nhấn Ctrl+C → "Cleaning up..." chạy → lock xoá
# Khám phá process với /proc
echo "Bash hiện tại PID: $$"
ls -l /proc/$$/fd/
# Tìm zombie (nếu có)
ps aux | awk '$8=="Z"'
# Nice + renice
sleep 1000 &
PID=$!
ps -p $PID -o pid,ni,cmd # nice = 0
renice -n 10 -p $PID
ps -p $PID -o pid,ni,cmd # nice = 10
kill $PID
# Subshell — biến không leak
VAR_OUT="parent"
( VAR_OUT="subshell"; echo "Inside: $VAR_OUT" )
echo "Outside: $VAR_OUT" # vẫn "parent"
# Demo exec — script bị thay
cat > exec-demo.sh <<'EOF'
#!/bin/bash
echo "Trước exec, PID = $$"
exec sleep 5
echo "Dòng này không bao giờ chạy"
EOF
chmod +x exec-demo.sh
./exec-demo.shPitfalls
>ghi đè file, lỡoutput > inputthayoutput > newfile→ mất data.- Pipe chỉ pipe stdout. Cần stderr →
2>&1trước|. kill -9reflex → app không cleanup, data corruption. SIGTERM trước, SIGKILL chỉ khi cần.- Bash PID 1 trong container không forward signal → app không nhận SIGTERM. Dùng
exechoặctini. - Heredoc
<<EOF(không quote) expand variable. Quote<<'EOF'để giữ nguyên. - Zombie tích tụ = bug app cha không wait child. Không kill được — kill parent hoặc fix code.
./script &không lưu$!→ khó kill sau../script & PID=$!.- Trap không bắt SIGKILL và SIGSTOP — đặc tính kernel.
renicepriority cao (âm) cần root. User thường chỉ làm priority thấp hơn.
Tóm tắt
- 3 stream stdin/stdout/stderr (FD 0/1/2). FD là index vào file table của process —
/proc/<PID>/fd/xem được. - Pipe
|chỉ pipe stdout. Stderr cần2>&1. exec FD<>filecho FD tuỳ ý.read -u FDđọc từ đó.- Process tạo bằng
fork()(clone parent),exec()(load code mới),wait()(parent thu dọn). PID 1 = init/systemd. - Zombie = child chết, parent chưa wait. Orphan = child sống, parent chết → init nhận.
- Daemon truyền thống: double-fork + setsid. Modern dùng systemd.
- SIGTERM trước, SIGKILL sau. SIGKILL/SIGSTOP không catch được.
trap cleanup EXITở đầu script production. Cleanup luôn chạy.exectrong entrypoint container để app nhận signal đúng (PID 1).nice/renicepriority — backup job nên nice cao (19) để không tranh CPU.
🎓 Tổng kết — 3 bài Bash
Đi qua:
- Bài 01: Shell cơ bản — Linux/shell/bash, builtins vs external (ranh giới shell-OS), shell script là gì, script đầu tiên, biến, login vs non-login shell, customize bashrc.
- Bài 02: Logic — exit status, test command, if/elif/else, case, AND/OR/NOT, positional parameters, loops (for/while/until/select/break/continue), function với local/global var,
set -euo pipefail, ShellCheck. - Bài 03: Hệ thống — redirect, /dev/null, heredoc/here string, file descriptor tuỳ ý, pipes/filters, signals, process internals (fork/exec/wait/getpid), orphan/zombie, daemon lifecycle, trap, subshell, compound command, exec.
Đủ viết script production thật + hiểu OS từ phía shell.
Lúc chưa rõ thì đọc thêm:
- Array (
declare -a,${arr[@]}) — quản lý list. - Associative array (
declare -A) — Bash 4+, key-value. - Regex sâu: PCRE, sed, awk regex.
coproc, named pipe (mkfifo) — IPC giữa script.- Bash debug:
bash -x script.sh,set -x,PS4. - The Linux Programming Interface (Michael Kerrisk) — định nghĩa kernel của syscall, fd, signal, process. Đọc khi muốn hiểu kernel side.
Lời khuyên cuối:
Mọi script production:
#!/bin/bash set -euo pipefail trap cleanup EXITQuote biến luôn. ShellCheck báo chỗ thiếu.
Hiểu OS phía dưới = debug nhanh gấp 10 lần. Khi gặp "app không respond SIGTERM", "process zombie tích tụ", "FD leak" — bạn không panic mà biết đào đâu.
Bash quá rối/dài (>500 dòng) → cân nhắc Python/Go. Bash giỏi nhất ở thao tác file/process.
Khi rối, đọc kỹ error message của bash. Đa phần nói rõ dòng nào, lý do gì.
— Minh Hưng