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

Trong hệ điều hành Unix và Linux, triết lý thiết kế cốt lõi là "Everything is a file" (Tất cả mọi thứ đều là tập tin). Từ các file văn bản thông thường, thư mục, thiết bị phần cứng (như bàn phím, màn hình), cho đến các cổng kết nối mạng (sockets) — tất cả đều được biểu diễn và thao tác dưới dạng luồng byte (stream of bytes) thông qua một giao diện chung. Việc quản lý và điều hướng các luồng dữ liệu này được thực hiện thông qua hệ thống File Descriptors (FD). Làm chủ các khái niệm này là chìa khóa để viết các script tự động hóa mức hệ thống một cách chuyên nghiệp.

1. Kiến trúc File Descriptors & Cơ chế Nhân hệ điều hành (Kernel)

Để hiểu bản chất của File Descriptor (FD), ta cần đi sâu vào cách Linux Kernel quản lý tài nguyên trong bộ nhớ:

  • File Descriptor Table (Bảng FD cấp Tiến trình): Mỗi tiến trình sở hữu riêng một bảng chứa các chỉ số số nguyên không âm (0, 1, 2, 3...). Mỗi chỉ số này trỏ tới một bản ghi trong bảng Open File Table của Kernel.
  • Open File Table (Bảng File Đang Mở cấp Hệ thống): Chứa các thông tin trạng thái hoạt động của file: chế độ mở (đọc/ghi/append), vị trí con trỏ đọc ghi hiện tại (file offset), và con trỏ trỏ tới bảng chứa inode.
  • Inode Table (Bảng Inode): Biểu diễn đối tượng vật lý thực tế trên ổ cứng (chứa metadata của file, quyền truy cập, kích thước file và vị trí các block dữ liệu thực tế).

Mặc định, khi một shell hoặc tiến trình mới khởi động, Kernel sẽ tự động mở sẵn 3 luồng tiêu chuẩn tương ứng với các chỉ số FD sau:

Số hiệu FD Tên gọi Thiết bị mặc định Mô tả hoạt động của luồng dữ liệu
0 stdin (Standard Input) Keyboard / Terminal Luồng nhập dữ liệu tiêu chuẩn. Nhận dữ liệu đầu vào cho tiến trình.
1 stdout (Standard Output) Screen / Terminal Luồng xuất kết quả tiêu chuẩn. Hiển thị kết quả hoạt động bình thường.
2 stderr (Standard Error) Screen / Terminal Luồng báo lỗi tiêu chuẩn. Đẩy thông điệp chẩn đoán lỗi độc lập với stdout.

Cơ chế hệ thống đằng sau Redirection và hàm hệ thống dup2()

Khi ta viết lệnh chuyển hướng command > file.txt, Kernel không hề thay đổi mã nguồn của chương trình để trỏ vào file. Thay vào đó, Shell (tiến trình cha) thực hiện các bước sau:

  1. Gọi hàm hệ thống fork() để sinh ra tiến trình con.
  2. Tiến trình con gọi hàm open("file.txt", ...) để lấy ra một số hiệu FD mới (ví dụ: FD 3).
  3. Tiến trình con gọi hàm hệ thống dup2(3, 1). Hệ thống sẽ sao chép con trỏ tại vị trí số 3 trong bảng FD ghi đè lên vị trí số 1. Từ lúc này, FD 1 không còn trỏ ra màn hình nữa mà trỏ trực tiếp tới file.txt.
  4. Tiến trình con đóng FD 3 dư thừa bằng close(3).
  5. Tiến trình con gọi hàm hệ thống execve() để chạy chương trình command. Bất kỳ dòng code nào ghi vào FD 1 (stdout) của chương trình con đều tự động đi vào file.

Phân tích cạm bẫy thứ tự: >file 2>&1 so với 2>&1 >file

Shell xử lý các phép toán chuyển hướng tuần tự từ trái qua phải. Sự thay đổi con trỏ FD diễn ra lập tức:

fd_order_analysis.sh
# Cú pháp A (Đúng):
# Bước 1: Shell gặp `> log.txt`. Nó hướng FD 1 sang file log.txt. (Bảng FD: 1 -> log.txt)
# Bước 2: Shell gặp `2>&1`. Nó copy đích trỏ của FD 1 (log.txt) vào FD 2. (Bảng FD: 2 -> log.txt)
# Kết quả: Cả hai luồng 1 và 2 đều đẩy dữ liệu vào log.txt.
command > log.txt 2>&1

# Cú pháp B (Sai):
# Bước 1: Shell gặp `2>&1`. Nó copy đích trỏ của FD 1 (lúc này đang là Màn hình) vào FD 2. (Bảng FD: 2 -> Màn hình)
# Bước 2: Shell gặp `> log.txt`. Nó hướng FD 1 sang file log.txt. (Bảng FD: 1 -> log.txt, FD 2 -> Màn hình)
# Kết quả: stdout ghi vào log.txt, nhưng stderr (FD 2) vẫn ghi ra màn hình terminal!
command 2>&1 > log.txt

2. exec & Quản lý File Descriptor tùy biến (FD 3-9)

Trong lập trình shell, ta có thể tự mở thêm các số hiệu FD từ 3 tới 9 bằng lệnh built-in exec.

Bảng so sánh phương thức điều hướng I/O

Cú pháp Tên gọi Bản chất hoạt động Ưu điểm & Trường hợp sử dụng Nhược điểm & Cạm bẫy
cmd > file Output Redirection Ghi đè nội dung file bằng stdout của cmd. Nhanh, đơn giản để ghi đè log/file mới. Xóa sạch dữ liệu cũ của file mà không báo trước.
cmd >> file Append Redirection Ghi nối đuôi dữ liệu vào cuối file. An toàn, bảo toàn lịch sử log ứng dụng. Làm phình kích thước file nếu không có logrotate.
exec 3< file Custom FD Read Mở một FD cố định trỏ tới file để đọc dần bằng read -u 3. Không chiếm dụng stdin (FD 0), tránh xung đột vòng lặp. Bắt buộc phải đóng thủ công qua exec 3<&- để tránh leak FD.
cmd << EOF Here-Document Truyền nhiều dòng text trực tiếp vào stdin của cmd. Dễ viết cấu hình file tạm, xuất thông báo nhiều dòng. Tải toàn bộ nội dung vào bộ nhớ (RAM), không phù hợp với file lớn.

Ứng dụng thực tiễn: Đọc cấu hình song song với tương tác người dùng

Một lỗi kinh điển khi dùng vòng lặp đọc file là nếu bạn dùng while read line thông thường (redirect ở cuối block < file), toàn bộ stdin (FD 0) của vòng lặp bị file chiếm dụng. Bất kỳ lệnh nhập liệu nào bên trong vòng lặp như read -p từ bàn phím sẽ đọc nhầm dữ liệu của dòng tiếp theo trong file cấu hình thay vì đợi người dùng gõ phím.

Giải pháp phòng thủ là mở một File Descriptor số 3 độc lập:

isolated_read.sh
# Mở FD 3 trỏ tới file config
exec 3< server_list.txt

while read -u 3 server_ip; do
    echo "--- Kiểm tra máy chủ: $server_ip ---"
    # Lệnh read dưới đây vẫn nhận dữ liệu từ bàn phím bình thường vì stdin (FD 0) hoàn toàn tự do!
    read -p "Bạn có muốn deploy sang IP này không? (y/n): " choice
    if [ "$choice" = "y" ]; then
         echo "Đang khởi động SSH deploy tới $server_ip..."
    fi
done

# Đóng FD 3 giải phóng tài nguyên
exec 3<&-

3. Giao tiếp giữa các tiến trình bằng Named Pipes (FIFOs)

Khi dùng ống dẫn thông thường (Anonymous Pipe) như cat file.txt | grep "error", hệ thống tự động gọi hàm pipe() tạo ra một buffer trong RAM và chỉ kết nối giữa hai tiến trình cha-con chạy đồng thời, sau đó biến mất khi kết thúc lệnh.

Named Pipe (FIFO) là một thực thể đặc biệt được đăng ký trên hệ thống tập tin (tạo bằng mkfifo). Nó hoạt động như một đường dẫn trung gian giúp hai tiến trình độc lập hoàn toàn (không có quan hệ cha-con) có thể bắt tay và gửi dữ liệu cho nhau.

Cơ chế hoạt động hệ thống và Giới hạn bộ đệm

  • Blocking Mode (Chế độ chặn): Khi một tiến trình mở FIFO để ghi (Writer), hệ thống sẽ chặn tiến trình đó lại (treo dòng) cho đến khi có một tiến trình khác mở FIFO để đọc (Reader). Cơ chế này giúp đồng bộ hóa các ứng dụng cực kỳ tốt mà không gây tốn tài nguyên CPU vì tiến trình bị rơi vào trạng thái ngủ (Sleep) chờ ngắt từ kernel.
  • Giới hạn Buffer: Named Pipe lưu trữ dữ liệu hoàn toàn trên bộ đệm bộ nhớ của Kernel chứ không hề ghi xuống ổ cứng vật lý. Trên hệ điều hành Linux hiện đại, kích thước mặc định của bộ đệm pipe này là 64 KB (quy định tại /proc/sys/fs/pipe-max-size). Nếu Writer ghi vượt quá dung lượng này mà Reader chưa kịp đọc hết, Writer sẽ bị chặn (block) cho đến khi Reader giải phóng bộ đệm.
fifo_demo.sh
# Tạo Named Pipe
mkfifo /tmp/log_pipe

# Tiến trình Reader (Chạy ngầm): Đợi nhận log và xử lý ghi file có tag time
while read -r log_msg; do
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $log_msg" >> /var/log/app_events.log
done < /tmp/log_pipe &

# Tiến trình Writer (Chạy từ ứng dụng khác): Đẩy log vào pipe
echo "User admin logged in" > /tmp/log_pipe
echo "Database migration complete" > /tmp/log_pipe

4. Lập trình Socket trực tiếp trên Bash qua /dev/tcp/dev/udp

Đây là một tính năng ẩn cực kỳ mạnh mẽ của Bash được biên dịch thông qua tùy chọn cấu hình flag --enable-net-redirections. Khi bạn thực hiện chuyển hướng vào đường dẫn ảo /dev/tcp/host/port, trình biên dịch Bash sẽ tự động bắt lấy chuỗi này và gọi hàm thư viện socket C tiêu chuẩn:

c_socket_analogy.c
// Bản chất nội tại của Bash tương đương với đoạn mã C:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

So sánh giữa /dev/tcp với các công cụ mạng tiêu chuẩn

Công cụ Cơ chế hoạt động Ưu điểm Nhược điểm & Hạn chế
/dev/tcp (Bash) Built-in trong trình biên dịch shell. Không phụ thuộc bất kỳ phần mềm ngoài nào. Chạy được trên các hệ thống tối giản (Docker container, hệ thống nhúng). Chỉ hỗ trợ Bash (không hỗ trợ sh, zsh, dash). Không có cơ chế SSL/TLS tích hợp sẵn (không dùng trực tiếp được cho HTTPS).
curl / wget Tiến trình ngoài chuyên dụng. Hỗ trợ đầy đủ giao thức HTTP, HTTPS, tự động giải quyết SSL, chuyển hướng (redirect), headers. Không có sẵn trong các Docker image tối giản (Alpine/Slim), gây overhead khởi tạo tiến trình.
nc (Netcat) Utility kết nối socket thô. Mạnh mẽ, hỗ trợ lắng nghe cổng (Listen mode - nc -l) và UDP. Cú pháp không đồng nhất giữa các phiên bản (BSD Netcat vs GNU Netcat). Thường bị quản trị viên gỡ bỏ vì lý do bảo mật.

Ứng dụng: Gửi gói tin HTTP GET thô và lấy nội dung

Đoạn script minh họa giao tiếp socket thô theo đúng chuẩn RFC 7230 để lấy dữ liệu:

http_raw_request.sh
# Mở socket TCP kết nối hai chiều đọc ghi trên FD 5
exec 5<> /dev/tcp/httpbin.org/80

# Gửi HTTP Request thô (Mỗi dòng kết thúc bằng ký tự carriage return \r\n)
echo -e "GET /ip HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n" >&5

# Đọc kết quả trả về từ FD 5
while read -r -u 5 line; do
    echo "Phản hồi: $line"
done

# Đóng FD 5 sau khi hoàn thành
exec 5>&-
exec 5<&-

5. Trắc nghiệm ôn tập

Trắc nghiệm 1: Phép điều hướng I/O

Đoạn lệnh nào sau đây sẽ chuyển hướng cả output bình thường (stdout) và thông điệp lỗi (stderr) vào tập tin output.log đúng cách?

Trắc nghiệm 2: Named Pipes

Lệnh nào dùng để tạo ra một Named Pipe trong Linux?

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

Tập tin script tổng hợp các kỹ thuật điều hướng nâng cao và socket scripting trong bài học:

Tải về io_redirection.sh