The Linux Programming Interface Chương 44 · Pipes and FIFOs

Chương 44 · Đường ống và FIFO Chapter 44 · Pipes and FIFOs

Một dòng byte chảy từ tiến trình này sang tiến trình khác Một cái ống dẫn dữ liệu từ chương trình này sang chương trình khác A stream of bytes flowing from one process to another

Kerrisk mở chương bằng nhận xét rằng pipe (đường ống) là phương pháp liên lạc liên tiến trình (IPC) lâu đời nhất trên UNIX, xuất hiện từ bản UNIX phiên bản ba (Third Edition) đầu thập niên 1970.1 Ông trình bày: pipe là một byte stream một chiều, dung lượng có hạn, với bảo đảm ghi nguyên tử cho khối ≤ PIPE_BUF; cách tạo pipe bằng lời gọi pipe(); cách nối cha–con và nối các filter bằng dup2(); cách nói chuyện với một lệnh shell qua popen()/pclose(); FIFO (named pipe) tạo bằng mkfifo() với ngữ nghĩa mở đặc biệt; một ứng dụng client–server dùng FIFO; nhập/xuất không chặn O_NONBLOCK; và ngữ nghĩa read()/write() cùng tín hiệu SIGPIPE/lỗi EPIPE.1

Khi bạn gõ ls | wc -l trong shell, dấu gạch đứng ấy chính là một cái ống: đầu ra của lệnh trái chảy thẳng sang đầu vào của lệnh phải, như nước chảy qua ống nước. Chương này mổ xẻ cái ống đó — nó chở dữ liệu ra sao, ai dựng được nó, và người anh em "có tên gọi" của nó là FIFO. Nhớ nhé: pipe là cái ống dẫn dữ liệu từ chương trình này sang chương trình kia.

Kerrisk opens by noting that the pipe is the oldest method of interprocess communication (IPC) on UNIX, having appeared in Third Edition UNIX in the early 1970s.1 He presents: a pipe as a unidirectional, limited-capacity byte stream with an atomicity guarantee for blocks of ≤ PIPE_BUF bytes; creating a pipe with the pipe() system call; connecting parent and child and chaining filters with dup2(); talking to a shell command via popen()/pclose(); FIFOs (named pipes) created with mkfifo() and their peculiar open semantics; a client–server application using FIFOs; nonblocking I/O with O_NONBLOCK; and the semantics of read()/write() with the SIGPIPE signal and EPIPE error.1

Góc nhìn của nhân: một pipe đơn giản là một vùng đệm duy trì trong bộ nhớ nhân, truy cập qua một cặp bộ mô tả tệp. Chính vì sống trong nhân chứ không có tên trên hệ thống tệp, một pipe không tên chỉ nối được những tiến trình họ hàng — những tiến trình cùng thừa kế cặp bộ mô tả từ một tổ tiên chung đã gọi pipe() trước khi fork(). FIFO khắc phục đúng hạn chế đó: nó có một đường dẫn trên hệ thống tệp nên bất kỳ tiến trình nào có quyền cũng mở được.13The kernel's view: a pipe is simply a buffer maintained in kernel memory, reached through a pair of file descriptors. Because it lives in the kernel with no name on the file system, an unnamed pipe can only connect related processes — ones that inherit the descriptor pair from a common ancestor that called pipe() before fork(). A FIFO fixes exactly that limitation: it has a pathname on the file system, so any process with suitable permissions can open it.13

Luận đề của chương: pipe và FIFO là cùng một ý tưởng — một byte stream một chiều, dung lượng có hạn do nhân giữ — chỉ khác ở chỗ FIFO có tên trên hệ thống tệp nên nối được cả những tiến trình không họ hàng. Nắm được ngữ nghĩa đóng bộ mô tả, trần PIPE_BUF, cách mở FIFO có thể chặn, và cặp SIGPIPE/EPIPE — là nắm được kênh IPC đơn giản nhất mà cả shell lẫn vô số chương trình vẫn dùng mỗi ngày.1

Ý chính: pipe và FIFO đều là một cái "ống" một chiều để chở dữ liệu giữa các chương trình. Pipe thường dùng cho các chương trình có họ hàng; FIFO có tên nên ai cũng gọi được. Hiểu cái ống này là hiểu cách shell nối các lệnh với nhau. Nhớ nhé: pipe là ống không tên cho người nhà, FIFO là ống có tên cho tất cả.

The chapter's thesis: pipes and FIFOs are the same idea — a unidirectional, limited-capacity byte stream held by the kernel — differing only in that a FIFO has a name on the file system, so it can connect unrelated processes too. Grasping the descriptor-closing discipline, the PIPE_BUF limit, the FIFO's blocking open, and the SIGPIPE/EPIPE pair is grasping the simplest IPC channel that the shell and countless programs still use every day.1

Cụm khái niệm 01 Concept cluster 01

Pipe là một dòng byte một chiều, do nhân giữ, với dung lượng hữu hạn Pipe là một cái ống một chiều, chở dữ liệu thành dòng, có lúc đầy A pipe is a unidirectional kernel byte stream with a finite capacity

Trước khi tạo một pipe, cần hiểu nó là gì. Kerrisk nêu một loạt đặc tính nền tảng. Pipe là một byte stream: không có khái niệm thông điệp hay ranh giới thông điệp — bên đọc có thể đọc khối bất kỳ kích thước nào, bất kể bên ghi đã ghi thành từng khối to nhỏ ra sao, và dữ liệu đi qua theo đúng thứ tự được ghi, không thể truy cập ngẫu nhiên bằng lseek().13 Pipe là một chiều: dữ liệu chỉ chảy một hướng, một đầu để ghi và đầu kia để đọc.1 Và pipe có dung lượng giới hạn: nó chỉ là một vùng đệm trong bộ nhớ nhân, đầy rồi thì lời ghi tiếp theo bị chặn cho tới khi bên đọc lấy bớt dữ liệu ra.1

Hãy hình dung một ống nước thật: nước chỉ chảy một chiều, bạn đổ vào một đầu và hứng ở đầu kia. Nước không "chia thành gói" — nó là một dòng liên tục, ai hứng bao nhiêu tùy ca lớn ca nhỏ. Và ống chỉ chứa được chừng ấy nước: đổ nhanh quá mà chưa ai hứng thì phải chờ. Pipe trong máy tính đúng như vậy, chỉ thay nước bằng byte. Nhớ nhé: pipe là ống một chiều, chở byte thành dòng, và có lúc đầy phải chờ.

Before creating a pipe, you need to know what it is. Kerrisk lists several foundational properties. A pipe is a byte stream: there is no concept of messages or message boundaries — the reader may read blocks of any size regardless of how the writer wrote them, and the data passes through in exactly the order it was written, with no random access via lseek().13 A pipe is unidirectional: data flows in only one direction, one end for writing and the other for reading.1 And a pipe has a limited capacity: it is just a buffer in kernel memory, and once full, further writes block until the reader removes some data.1

Định nghĩa. Một pipe là một kênh IPC một chiều: một vùng đệm vòng trong bộ nhớ nhân, truy cập qua hai bộ mô tả tệp — một cho đầu đọc, một cho đầu ghi. Bên đọc một pipe rỗng bị chặn tới khi có ít nhất một byte được ghi; khi đầu ghi đóng lại, bên đọc thấy cuối tệp (read() trả về 0) sau khi đã đọc hết dữ liệu còn lại.12

Nói đơn giản. Pipe là cái ống có hai đầu: một đầu đổ vào, một đầu lấy ra. Lấy ở ống rỗng thì phải đứng chờ tới khi có người đổ vào. Khi không còn ai đổ nữa và ống đã cạn, người lấy nhận tín hiệu "hết rồi". Nhớ nhé: ống rỗng thì đợi, hết người đổ thì báo hết.

Definition. A pipe is a unidirectional IPC channel: a circular buffer in kernel memory, reached through two file descriptors — one for the read end, one for the write end. A reader of an empty pipe blocks until at least one byte is written; when the write end is closed, the reader sees end-of-file (read() returns 0) once it has read all remaining data.12

ghi ──► [ b y t e s t r e a m ] ──► đọc
• không ranh giới thông điệp
• đọc đúng thứ tự đã ghi
• không lseek()
• ghi ≤ PIPE_BUF là nguyên tử
• ống đầy ⇒ ghi bị chặn

đổ vào ──► [ ống byte ] ──► lấy ra
• một chiều
• dòng liên tục, không chia gói
• đầy thì phải chờ

write ──► [ b y t e s t r e a m ] ──► read
• no message boundaries
• read in write order
• no lseek()
• writes ≤ PIPE_BUF are atomic
• full pipe ⇒ write blocks

  1. byte stream

    Không có "thông điệp" — chỉ có dòng byte Dữ liệu là một dòng liền, không phải từng gói There are no "messages" — only a byte stream

    Nếu bên ghi gọi write() ba lần, bên đọc không nhất thiết nhận lại thành ba khối tương ứng — nó có thể gộp cả ba vào một lần read(), hoặc đọc nhỏ giọt từng chút. Pipe không ghi nhớ ranh giới giữa các lần ghi. Muốn có khái niệm "thông điệp rời rạc", ứng dụng phải tự dựng lấy; nếu không, tốt hơn nên dùng cơ chế khác như hàng thông điệp hoặc datagram socket.1

    Đổ ba ca nước vào ống, người hứng ở đầu kia có thể hứng gọn vào một ca to, hoặc hứng làm năm lần nhỏ — ống không nhớ bạn đã đổ thành mấy lần. Byte cũng vậy. Nhớ nhé: pipe không chia dữ liệu thành gói, bạn muốn gói thì tự đóng gói lấy.

    If the writer calls write() three times, the reader does not necessarily get three matching blocks back — it may gather all three into one read(), or sip them a little at a time. The pipe keeps no record of boundaries between writes. To get discrete "messages," the application must build that itself; otherwise a mechanism such as a message queue or datagram socket is preferable.1

    Bạn gặp nó ở đâuWhere you meet it Khi gửi các bản ghi có kích thước khác nhau qua pipe mà bên nhận đọc sai ranh giới — đó là lúc bạn cần tự thêm độ dài hay dấu phân cách vào mỗi bản ghi.1 Khi bạn gửi nhiều mẩu tin qua ống mà bên kia đọc lẫn vào nhau — hãy tự đánh dấu đâu là hết một mẩu. When sending variable-sized records through a pipe and the receiver reads the boundaries wrong — that is when you add a length or delimiter to each record yourself.1
  2. unidirectional

    Một chiều — muốn hai chiều thì tạo hai pipe Chỉ chảy một hướng; cần hai hướng thì dùng hai ống One direction — for two-way, make two pipes

    Dữ liệu chỉ đi một hướng qua pipe. Trên vài hiện thực UNIX khác — đáng chú ý là các bản dẫn xuất từ System V Release 4 — pipe là hai chiều (gọi là stream pipe), nhưng tính chất này không có trong bất kỳ chuẩn UNIX nào, nên tốt nhất đừng dựa vào.1 Nếu cần liên lạc hai chiều, giải pháp đơn giản là tạo hai pipe, mỗi cái một hướng; hoặc dùng cặp UNIX domain stream socket qua socketpair(), vốn hai chiều một cách chuẩn mực.1

    Một cái ống nước chỉ cho nước chảy một chiều. Muốn trò chuyện qua lại hai bên thì lắp hai ống: một ống "tôi nói cho bạn", một ống "bạn nói cho tôi". Nhớ nhé: một ống một chiều, cần qua lại thì bắc hai ống.

    Data travels in only one direction through a pipe. On some other UNIX implementations — notably those derived from System V Release 4 — pipes are bidirectional (so-called stream pipes), but this is not specified by any UNIX standard, so it is best not to rely on it.1 If you need two-way communication, the simple answer is to create two pipes, one per direction; or use a UNIX domain stream socket pair via socketpair(), which is bidirectional in a standardized way.1

    Bạn gặp nó ở đâuWhere you meet it Khi dựng hai pipe cho liên lạc qua lại — hãy đề phòng deadlock: cả hai tiến trình cùng chặn vì đang chờ đọc từ ống rỗng hoặc chờ ghi vào ống đã đầy.1 Khi dùng hai ống để nói qua lại — coi chừng cả hai cùng đứng chờ nhau mà không ai nhúc nhích. When wiring two pipes for two-way talk — beware deadlock: both processes blocking because each waits to read an empty pipe or write a full one.1
  3. PIPE_BUF

    Ghi ≤ PIPE_BUF byte được bảo đảm nguyên tử Đổ một lượng vừa đủ nhỏ thì không bị trộn lẫn Writes of ≤ PIPE_BUF bytes are guaranteed atomic

    Nếu nhiều tiến trình cùng ghi vào một pipe, dữ liệu của chúng không bị trộn lẫn miễn là mỗi lần ghi không quá PIPE_BUF byte. SUSv3 đòi PIPE_BUF ít nhất bằng _POSIX_PIPE_BUF (512); giá trị thực khác nhau giữa các hệ — ví dụ 512 byte trên FreeBSD 6.0, 4096 byte trên Tru64 5.1, 5120 byte trên Solaris 8. Trên Linux, PIPE_BUF bằng 4096.13 Khi ghi khối lớn hơn PIPE_BUF, nhân có thể chia thành nhiều mẩu nhỏ và, nếu có nhiều bên ghi, các mẩu đó có thể bị xen kẽ với dữ liệu của tiến trình khác.1

    Tưởng tượng nhiều người cùng đổ nước vào một ống. Nếu mỗi người chỉ đổ một ca nhỏ vừa đủ, nước của mỗi người vào gọn một mạch, không lẫn với người khác. Nhưng nếu ai đó đổ cả xô to, xô ấy có thể bị chia nhỏ và xen giữa những ca của người bên cạnh. Nhớ nhé: đổ ít một lần thì gọn, đổ quá nhiều thì dễ bị trộn.

    If multiple processes write to one pipe, their data is not intermingled as long as each write is no more than PIPE_BUF bytes. SUSv3 requires PIPE_BUF to be at least _POSIX_PIPE_BUF (512); the actual value varies — e.g. 512 bytes on FreeBSD 6.0, 4096 on Tru64 5.1, 5120 on Solaris 8. On Linux, PIPE_BUF is 4096.13 When writing a block larger than PIPE_BUF, the kernel may split it into smaller pieces and, if there are several writers, those pieces may be interleaved with other processes' data.1

    Bạn gặp nó ở đâuWhere you meet it Khi nhiều tiến trình cùng ghi log vào một pipe — giữ mỗi bản ghi ≤ PIPE_BUF byte để các dòng không bị cắt xén chồng lên nhau.7 Khi nhiều chương trình cùng ghi vào một ống — giữ mỗi lần ghi đủ nhỏ để các dòng không lẫn vào nhau. When several processes log into one pipe — keep each record ≤ PIPE_BUF bytes so the lines are not chopped and mixed.7

    Khác biệt giữa các phiên bản nhânA kernel-version difference

    Kerrisk lưu ý: trên Linux 2.2, mọi lời ghi vào pipe — bất kể kích thước — đều nguyên tử, trừ khi bị ngắt bởi một trình xử lý tín hiệu. Từ Linux 2.4 trở đi, bất kỳ lời ghi lớn hơn PIPE_BUF byte đều có thể bị xen kẽ với lời ghi của tiến trình khác, vì phần mã hiện thực pipe đã thay đổi lớn giữa hai dòng nhân 2.2 và 2.4.1Kerrisk notes: on Linux 2.2, pipe writes of any size were atomic unless interrupted by a signal handler. From Linux 2.4 onward, any write larger than PIPE_BUF bytes may be interleaved with other processes' writes, because the pipe implementation code changed substantially between the 2.2 and 2.4 kernels.1

    Trần ảnh hưởng tới lúc nào dữ liệu được chuyểnThe limit shapes when data moves

    Khi ghi tối đa PIPE_BUF byte, write() sẽ chặn nếu cần cho tới khi có đủ chỗ để hoàn tất nguyên tử. Khi ghi nhiều hơn PIPE_BUF, write() đổ được chừng nào hay chừng ấy rồi chặn chờ bên đọc lấy bớt; nếu bị tín hiệu ngắt giữa chừng, nó trả về số byte đã chuyển (một "partial write").1When writing up to PIPE_BUF bytes, write() blocks if needed until there is enough room to complete atomically. When writing more than PIPE_BUF, write() transfers as much as it can, then blocks until the reader drains some; if a signal interrupts it midway, it returns the count of bytes transferred (a "partial write").1

Cụm khái niệm 02 Concept cluster 02

Dựng ống: pipe(), nối cha–con, nối filter, và popen() Tự tay lắp ống: tạo, nối cha–con, nối lệnh, nói với shell Plumbing: pipe(), parent–child, connecting filters, and popen()

Lời gọi pipe(int filedes[2]) tạo một pipe mới và trả về hai bộ mô tả tệp đang mở trong mảng filedes: filedes[0] cho đầu đọc và filedes[1] cho đầu ghi.12 Một pipe gần như vô dụng trong phạm vi một tiến trình đơn lẻ; sức mạnh của nó lộ ra khi phối hợp với fork(): tiến trình gọi pipe() rồi fork(), và vì con thừa kế bản sao các bộ mô tả của cha, hai bên lập tức có chung một pipe nối liền. Ngay sau đó, mỗi bên đóng đầu không dùng: bên đọc đóng filedes[1], bên ghi đóng filedes[0].1

Hàm pipe() giao cho bạn hai đầu ống: một đầu để lấy ra (số 0), một đầu để đổ vào (số 1). Một mình thì ống chẳng để làm gì; nhưng khi một tiến trình đẻ ra tiến trình con, đứa con được chép lại hai đầu ống ấy — thế là hai cha con nối với nhau. Việc đầu tiên mỗi người làm là bịt cái đầu mình không dùng tới. Nhớ nhé: tạo ống, sinh con, rồi ai cũng bịt bớt đầu thừa.

The call pipe(int filedes[2]) creates a new pipe and returns two open file descriptors in the array filedes: filedes[0] for the read end and filedes[1] for the write end.12 A pipe is nearly useless within a single process; its power appears when combined with fork(): a process calls pipe() then fork(), and because the child inherits copies of the parent's descriptors, both sides immediately share one connected pipe. Right afterward, each side closes the end it does not use: the reader closes filedes[1], the writer closes filedes[0].1

Định nghĩa. Đóng bộ mô tả không dùng không chỉ để khỏi cạn hạn mức bộ mô tả — nó thiết yếu cho sự đúng đắn. Bên đọc phải đóng đầu ghi để, khi bên kia ghi xong và đóng đầu ghi của họ, nó mới thấy cuối tệp; nếu không, read() sẽ chặn mãi vì nhân biết vẫn còn một đầu ghi đang mở. Bên ghi phải đóng đầu đọc để nhận được SIGPIPE/EPIPE khi không còn ai đọc.1

Nói đơn giản. Phải bịt đầu ống mình không dùng, không phải cho gọn mà vì nếu không mọi thứ sẽ kẹt. Người lấy nước mà không bịt đầu đổ của chính mình thì sẽ đứng chờ mãi, tưởng vẫn còn người sắp đổ. Nhớ nhé: không bịt đầu thừa thì chương trình treo.

Definition. Closing unused descriptors is not merely about not exhausting the descriptor limit — it is essential for correctness. The reader must close the write end so that, when the other side finishes and closes its write end, the reader sees end-of-file; otherwise read() blocks forever because the kernel knows a write end is still open. The writer must close the read end to receive SIGPIPE/EPIPE when no reader remains.1

#include <unistd.h>
int pipe(int filedes[2]);
/* filedes[0] = đầu đọc */
/* filedes[1] = đầu ghi */
/* trả 0 nếu ok, -1 nếu lỗi */

pipe(filedes)
filedes[0] --> lấy ra
filedes[1] --> đổ vào
rồi fork() để chia cho con

#include <unistd.h>
int pipe(int filedes[2]);
/* filedes[0] = read end */
/* filedes[1] = write end */
/* returns 0 on success, -1 on error */

  1. close()

    Đóng đầu không dùng — quy tắc sống còn của pipe Bịt đầu ống mình không dùng, nếu không sẽ kẹt Close the unused end — the pipe's survival rule

    Thông thường một pipe chỉ nên có một bên ghi. Lý do đóng đầu đọc ở bên ghi: khi một tiến trình ghi vào pipe mà không còn tiến trình nào mở đầu đọc, nhân gửi tín hiệu SIGPIPE (mặc định giết tiến trình); nếu bắt hoặc bỏ qua tín hiệu, write() thất bại với lỗi EPIPE ("broken pipe").17 Còn khi tất cả bộ mô tả trỏ tới pipe trong mọi tiến trình đều đóng, pipe mới bị hủy và tài nguyên được giải phóng — mọi dữ liệu chưa đọc bị mất.1

    Nếu người lấy nước quên bịt đầu đổ của chính mình, họ sẽ chờ mãi vì nghĩ "biết đâu lát nữa có người đổ" — kể cả khi người đổ thật đã bỏ đi. Còn nếu đổ nước vào ống mà đầu kia chẳng còn ai hứng, hệ thống "đập vỡ ống" báo lỗi. Nhớ nhé: bịt đầu thừa để thấy khi nào hết, và để biết khi nào không còn ai nghe.

    Normally a pipe should have only one writer. Why the writer closes the read end: when a process writes to a pipe with no process holding the read end open, the kernel sends SIGPIPE (which by default kills the process); if you catch or ignore it, write() fails with EPIPE ("broken pipe").17 And only when all descriptors referring to the pipe in every process are closed is the pipe destroyed and its resources freed — any unread data is lost.1

    Bạn gặp nó ở đâuWhere you meet it Mỗi khi một chương trình "treo" ở read() trên pipe dù bên ghi đã xong — gần như chắc chắn là một đầu ghi còn mở đâu đó chưa được đóng.1 Khi chương trình đứng im mãi lúc đọc ống dù bên kia đã xong — thường là vì còn một đầu đổ chưa bịt. Whenever a program "hangs" on read() though the writer has finished — almost always a write end is still open somewhere, unclosed.1

    pipe() và pipe2()pipe() and pipe2()

    Từ nhân 2.6.27, Linux hỗ trợ lời gọi phi chuẩn pipe2(), giống pipe() nhưng thêm đối số flags. Cờ O_CLOEXEC bật close-on-exec cho hai bộ mô tả mới; cờ O_NONBLOCK đánh dấu chúng là không chặn, tiết kiệm các lời gọi fcntl() về sau.12Since kernel 2.6.27, Linux supports the nonstandard pipe2(), like pipe() but with an extra flags argument. The O_CLOEXEC flag sets close-on-exec on the two new descriptors; O_NONBLOCK marks them nonblocking, saving later fcntl() calls.12

    Tiến trình họ hàng, không chỉ cha–conRelated processes, not just parent–child

    Pipe dùng được giữa bất kỳ hai (hay nhiều) tiến trình họ hàng, miễn pipe được tạo bởi một tổ tiên chung trước chuỗi fork(). Ví dụ quen thuộc nhất là hai tiến trình anh em — cha tạo pipe rồi tạo hai con; đó chính là cách shell dựng một pipeline.1A pipe works between any two (or more) related processes, as long as it was created by a common ancestor before the series of fork() calls. The most familiar case is two sibling processes — a parent creates the pipe then creates two children; that is exactly how the shell builds a pipeline.1

  2. dup2()

    Nối filter: bẻ stdin/stdout vào pipe bằng dup2() Nối hai lệnh: cắm đầu ra lệnh này vào đầu vào lệnh kia Connecting filters: bend stdin/stdout into a pipe with dup2()

    Để tái tạo ls | wc -l, shell phải làm cho đầu ra chuẩn của ls chảy vào đầu ghi của pipe và đầu vào chuẩn của wc lấy từ đầu đọc — mà không sửa mã của chính hai lệnh đó. Thủ thuật là nhân bản bộ mô tả: dup2(pfd[1], STDOUT_FILENO) làm bộ mô tả 1 trỏ tới đầu ghi của pipe, rồi đóng bản thừa.110 Kerrisk minh họa bằng chương trình nối ls với wc -l; chạy nó cho ra cùng con số với khi gõ ls | wc -l trực tiếp trong shell.1

    Cái hay của shell là nó khiến ls tưởng mình vẫn in ra màn hình, còn wc tưởng mình vẫn đọc từ bàn phím — trong khi thực ra chúng đang nói chuyện qua một cái ống giấu kín. Mẹo nằm ở chỗ "tráo" đầu ra chuẩn và đầu vào chuẩn sang hai đầu ống. Nhớ nhé: nối lệnh là lừa cho mỗi lệnh tưởng nó vẫn nói với màn hình và bàn phím.

    To recreate ls | wc -l, the shell must make ls's standard output flow into the pipe's write end and wc's standard input come from the read end — without touching the two commands' code. The trick is descriptor duplication: dup2(pfd[1], STDOUT_FILENO) makes descriptor 1 refer to the pipe's write end, then closes the surplus copy.110 Kerrisk illustrates this with a program wiring ls to wc -l; running it produces the same count as typing ls | wc -l directly in the shell.1

    Bạn gặp nó ở đâuWhere you meet it Mỗi dấu | bạn gõ trong shell là một lần dàn dựng đúng chuỗi pipe() → fork() → dup2() → exec() này cho bạn.1 Mỗi dấu gạch đứng trong shell chính là lúc nó âm thầm lắp và nối ống giúp bạn. Every | you type in the shell stages exactly this pipe() → fork() → dup2() → exec() dance for you.1

    Vì sao dùng dup2() thay close()+dup()Why dup2() over close()+dup()

    Cách cũ là close(STDOUT_FILENO) rồi dup(pfd[1]) — dựa vào việc dup() dùng bộ mô tả rảnh nhỏ nhất. Nhưng nếu bộ mô tả 1 vốn đã đóng, ta có thể vô tình gán nhầm. dup2(pfd[1], STDOUT_FILENO) nói rõ đích, tránh được bẫy đó; sau đó nên bọc lời gọi trong if (pfd[1] != STDOUT_FILENO) để phòng trường hợp chúng vốn trùng nhau.1The old way is close(STDOUT_FILENO) then dup(pfd[1]) — relying on dup() using the lowest free descriptor. But if descriptor 1 was already closed, you can bind the wrong thing. dup2(pfd[1], STDOUT_FILENO) names the target explicitly, avoiding that trap; afterwards bracket the calls in if (pfd[1] != STDOUT_FILENO) in case they already coincide.1

    FIFO + tee(1) tạo pipeline rẽ nhánhFIFO + tee(1) forks a pipeline

    Pipeline shell vốn tuyến tính, nhưng dùng một FIFO cùng lệnh tee ta có thể rẽ đôi luồng: ls -l | tee myfifo | sort -k5n gửi một bản sao đầu ra qua FIFO cho một tiến trình khác đọc song song.1A shell pipeline is linear, but with a FIFO plus the tee command you can fork the stream: ls -l | tee myfifo | sort -k5n sends a duplicate of the output through the FIFO for another process to read in parallel.1

  3. popen()

    popen()/pclose(): nói chuyện với một lệnh shell popen(): chạy một lệnh shell rồi đọc hoặc gửi dữ liệu popen()/pclose(): talk to a shell command

    Một việc thường gặp là chạy một lệnh shell rồi đọc đầu ra hoặc gửi đầu vào cho nó. popen(command, mode) làm gọn: nó tạo một pipe, fork() một con để exec() một shell chạy chuỗi command, rồi trả về một luồng FILE *. Nếu mode là "r", lời gọi đọc đầu ra chuẩn của lệnh; nếu là "w", nó ghi vào đầu vào chuẩn của lệnh. Vì pipe một chiều, không thể giao tiếp hai chiều. Kết thúc bằng pclose(), hàm này đợi shell chạy xong và trả về trạng thái kết thúc.16

    popen() giống như bạn nhờ shell chạy giùm một lệnh rồi đưa cho bạn một cái phễu: phễu "r" để hứng những gì lệnh in ra, phễu "w" để rót dữ liệu cho lệnh ăn. Xong việc thì gọi pclose() để dọn dẹp. Nhớ nhé: popen là nhờ shell chạy lệnh và trao cho bạn một cái phễu đọc hoặc ghi.

    A common need is to run a shell command and read its output or feed it input. popen(command, mode) makes this tidy: it creates a pipe, fork()s a child to exec() a shell running command, and returns a FILE * stream. If mode is "r", the caller reads the command's standard output; if "w", it writes to the command's standard input. Because pipes are unidirectional, two-way talk is impossible. Finish with pclose(), which waits for the shell to finish and returns its termination status.16

    Bạn gặp nó ở đâuWhere you meet it Nguy hiểm thật sự: nếu command được dựng từ đầu vào người dùng, một chuỗi như ; rm * có thể bị shell thực thi — luôn phải kiểm tra kỹ đầu vào trước khi đưa vào popen() hay system().16 Coi chừng: nếu lệnh được ghép từ chữ người dùng gõ, kẻ xấu có thể lén chèn lệnh phá hoại. Luôn kiểm tra đầu vào trước. The real danger: if command is built from user input, a string like ; rm * may be executed by the shell — always check input carefully before passing it to popen() or system().16

Cụm khái niệm 03 Concept cluster 03

FIFO: một pipe có tên trên hệ thống tệp — và một ứng dụng client–server FIFO: cái ống có tên, ai cũng gọi được, dùng cho client–server FIFOs: a pipe with a name on the file system — and a client–server app

Về ngữ nghĩa, FIFO giống pipe; khác biệt chính là FIFO có một tên trong hệ thống tệp và được mở như một tệp thường. Nhờ đó, FIFO nối được cả những tiến trình không họ hàng — chẳng hạn một client và một server.14 Tên "FIFO" đến từ "first in, first out": dữ liệu được đọc đúng thứ tự được ghi; FIFO cũng thường được gọi là named pipe. Tạo FIFO từ shell bằng lệnh mkfifo, hoặc trong chương trình bằng hàm mkfifo(pathname, mode).15

FIFO cũng là cái ống một chiều như pipe, chỉ khác là nó có tên và nằm trên đĩa như một tệp. Vì có tên, hai chương trình chẳng quen biết gì nhau cũng có thể hẹn "gặp nhau ở cái ống tên này". Tên FIFO nghĩa là "vào trước, ra trước" — đổ vào trước thì lấy ra trước. Nhớ nhé: FIFO là pipe có tên, nên người lạ cũng gặp nhau được.

Semantically a FIFO is like a pipe; the chief difference is that a FIFO has a name in the file system and is opened like a regular file. This lets a FIFO connect even unrelated processes — such as a client and a server.14 The name "FIFO" comes from "first in, first out": data is read in the order it was written; FIFOs are also called named pipes. Create one from the shell with the mkfifo command, or in a program with mkfifo(pathname, mode).15

Định nghĩa. Mở một FIFO có ngữ nghĩa khác thường. Vì cách dùng hợp lý duy nhất là có một bên đọc và một bên ghi ở hai đầu, nên mặc định mở FIFO để đọc (O_RDONLY) sẽ chặn cho tới khi một tiến trình khác mở nó để ghi (O_WRONLY), và ngược lại. Nói cách khác, việc mở FIFO đồng bộ hóa hai bên. Nếu đầu kia đã mở sẵn, open() thành công ngay.14

Nói đơn giản. Khi bạn mở một đầu FIFO, bạn phải đứng chờ tới khi có người mở đầu kia — như hai người hẹn nhau ở quán, ai tới trước thì ngồi đợi. Chỉ khi đủ cả hai, cuộc nói chuyện mới bắt đầu. Nhớ nhé: mở FIFO một mình thì phải đợi bạn mình tới.

Definition. Opening a FIFO has unusual semantics. Since the only sensible use is a reader on one end and a writer on the other, by default opening a FIFO for reading (O_RDONLY) blocks until another process opens it for writing (O_WRONLY), and vice versa. In other words, opening a FIFO synchronizes the two sides. If the opposite end is already open, open() succeeds immediately.14

$ mkfifo myfifo  # tạo từ shell

#include <sys/stat.h>
int mkfifo(const char *path,
             mode_t mode);
# ls -l: kiểu 'p'; ls -F: thêm '|'

$ mkfifo myfifo
mkfifo("đường/dẫn", quyền)
ls -l thấy chữ 'p' ở đầu dòng

$ mkfifo myfifo  # from the shell

#include <sys/stat.h>
int mkfifo(const char *path,
             mode_t mode);
# ls -l: type 'p'; ls -F: appends '|'

  1. mkfifo()

    mkfifo() tạo một FIFO với quyền như một tệp mkfifo() tạo ống có tên, có quyền như tệp thường mkfifo() creates a FIFO with file-like permissions

    Hàm mkfifo(pathname, mode) tạo một FIFO mới với quyền cho bởi mode (đã lọc qua umask). Khi dùng stat() trên một FIFO, kiểu tệp trả về là S_IFIFO; khi liệt kê bằng ls -l, FIFO hiện chữ p ở cột đầu, còn ls -F thêm ký hiệu ống | vào sau tên.15 Về lịch sử, FIFO được tạo bằng mknod(pathname, S_IFIFO, 0); POSIX.1-1990 đưa ra mkfifo() như một API gọn hơn, tránh sự tổng quát của mknod().19

    Gõ mkfifo myfifo là bạn đã tạo một cái ống có tên nằm ngay trong thư mục, giống như tạo một tệp. Khi liệt kê thư mục, nó hiện chữ p ở đầu để báo "đây là một ống, không phải tệp thường". Nhớ nhé: mkfifo đặt một cái ống có tên lên đĩa cho mọi người dùng chung.

    The function mkfifo(pathname, mode) creates a new FIFO with permissions given by mode (masked by the umask). Running stat() on a FIFO returns file type S_IFIFO; listing with ls -l shows a p in the first column, and ls -F appends the pipe symbol | to the name.15 Historically FIFOs were made with mknod(pathname, S_IFIFO, 0); POSIX.1-1990 introduced mkfifo() as a simpler API avoiding the generality of mknod().19

    Bạn gặp nó ở đâuWhere you meet it Khi cần một kênh IPC đơn giản giữa các chương trình khởi chạy độc lập mà không chia sẻ tổ tiên — một FIFO đặt ở đường dẫn ai cũng biết là đủ.4 Khi hai chương trình chạy riêng cần trao dữ liệu — hẹn nhau ở một FIFO có tên cố định là xong. When you need a simple IPC channel between independently launched programs with no shared ancestor — a FIFO at a known path is enough.4
  2. open()

    Mở FIFO chặn tới khi đủ cả hai đầu Mở một đầu phải chờ đầu kia có người mở Opening a FIFO blocks until both ends are present

    Đây là điểm dễ gây bất ngờ nhất với người mới. Mở một FIFO để đọc sẽ đứng im ở lời gọi open() cho tới khi một tiến trình khác mở nó để ghi — và đối xứng cho chiều ngược lại. Kerrisk tóm tắt bốn trường hợp trong Bảng 44-1. Hành vi chặn này chính là một cơ chế đồng bộ tiện lợi: hai tiến trình "gặp nhau" tại FIFO.14

    Nếu bạn là người tới quán trước, bạn ngồi đợi; cửa chỉ "mở thông" khi cả hai người cùng có mặt. FIFO cũng thế: mở một đầu thì chờ, tới khi đầu kia có người mở mới bắt đầu chạy tiếp. Nhớ nhé: mở FIFO là một cuộc hẹn — ai tới trước thì đợi.

    This is the point that most surprises newcomers. Opening a FIFO for reading stalls at the open() call until another process opens it for writing — and symmetrically the other way. Kerrisk summarizes the four cases in Table 44-1. This blocking is a handy synchronization mechanism: two processes "meet" at the FIFO.14

    Bạn gặp nó ở đâuWhere you meet it Khi một chương trình "treo" ngay tại open() trên FIFO — nó không hỏng, chỉ đang kiên nhẫn chờ đầu kia xuất hiện.1 Khi chương trình đứng im lúc mở FIFO — yên tâm, nó đang đợi người ở đầu kia. When a program "hangs" right at open() on a FIFO — it is not broken, just patiently waiting for the other end to appear.1

    Cái bẫy O_RDWRThe O_RDWR trap

    Trên hầu hết hiện thực (gồm Linux), có thể lách chặn bằng cách mở FIFO với cờ O_RDWR, nhưng SUSv3 nói rõ hành vi này là không xác định — nên tránh để bảo đảm khả chuyển. Muốn không chặn một cách chuẩn mực, hãy dùng O_NONBLOCK.14On most implementations (including Linux) you can circumvent the blocking by opening the FIFO with O_RDWR, but SUSv3 explicitly calls this unspecified — so avoid it for portability. For a standardized non-blocking open, use O_NONBLOCK.14

    Dữ liệu còn lại bị bỏLeftover data is discarded

    Như với pipe, khi mọi bộ mô tả trỏ tới một FIFO đều đóng, dữ liệu chưa đọc còn lại bị bỏ đi. FIFO vẫn tồn tại như một mục trên hệ thống tệp, nhưng nội dung trong "ống" thì mất.1As with pipes, when all descriptors referring to a FIFO are closed, any outstanding unread data is discarded. The FIFO still exists as a file-system entry, but the contents in the "pipe" are gone.1

  3. client-server

    Một server, nhiều client: tên quen biết và FIFO riêng Một người phục vụ, nhiều người gọi, ai cũng có ống riêng One server, many clients: a well-known name and per-client FIFOs

    Kerrisk dựng một ứng dụng client–server minh họa: server cấp các số thứ tự duy nhất cho mỗi client. Mọi client gửi yêu cầu qua một server FIFO tên cố định (/tmp/seqnum_sv) mà ai cũng biết. Nhưng không thể dùng chung một FIFO cho hồi đáp, vì các client sẽ tranh nhau đọc và có thể đọc nhầm hồi đáp của nhau; vậy nên mỗi client tạo một FIFO riêng, tên dựng từ PID của nó, để server trả lời đúng địa chỉ.1

    Hãy hình dung một quầy phát số thứ tự. Ai cũng bỏ phiếu yêu cầu vào một hộp chung đặt ở chỗ quen thuộc (server FIFO). Nhưng khi nhận kết quả, mỗi người phải có hộp thư riêng mang tên mình, kẻo lấy nhầm số của người khác. Nhớ nhé: hỏi thì dùng hộp chung, trả lời thì mỗi người một hộp riêng.

    Kerrisk builds an illustrative client–server application: a server hands out unique sequence numbers to each client. Every client sends its request through one server FIFO at a fixed, well-known name (/tmp/seqnum_sv). But a single FIFO cannot carry the replies, because clients would race to read and might read each other's responses; so each client creates its own FIFO, named from its PID, for the server to reply to the right address.1

    Bạn gặp nó ở đâuWhere you meet it Mẫu "địa chỉ quen biết + kênh phản hồi riêng cho mỗi client" lặp lại trong thiết kế server — từ FIFO tới socket; học nó ở đây là nền cho các chương về socket sau này.1 Kiểu "một chỗ chung để hỏi, mỗi người một chỗ riêng để nghe trả lời" sẽ gặp lại hoài trong lập trình mạng. The "well-known address + a private reply channel per client" pattern recurs in server design — from FIFOs to sockets; learning it here grounds the later socket chapters.1

Cụm khái niệm 04 Concept cluster 04

Không chặn, và ngữ nghĩa chính xác của read() và write() Chế độ không chờ, và luật đọc/ghi trên pipe và FIFO Nonblocking, and the exact semantics of read() and write()

Khi mở một FIFO, mặc định tiến trình bị chặn nếu đầu kia chưa được mở. Muốn không chặn, truyền cờ O_NONBLOCK cho open(). Cờ này bất đối xứng: nếu mở để đọc mà chưa ai mở đầu ghi, open() vẫn thành công ngay; nhưng nếu mở để ghi mà chưa ai mở đầu đọc, open() thất bại với errno đặt thành ENXIO.14 Sự bất đối xứng ấy hợp lý: đọc khi chưa có người ghi thì chỉ là không có dữ liệu, còn ghi khi chưa có người đọc sẽ sinh SIGPIPE và lỗi EPIPE.1

Mặc định, mở FIFO là phải đứng chờ đầu kia. Thêm cờ O_NONBLOCK nghĩa là "đừng bắt tôi chờ". Nhưng có một điểm lạ: mở để đọc thì được vào ngay dù chưa ai ghi; còn mở để ghi mà chưa ai đọc thì bị từ chối luôn. Nhớ nhé: không chờ thì người đọc được vào ngay, người ghi thì bị đuổi nếu chưa có ai nghe.

When opening a FIFO, a process blocks by default if the other end is not yet open. To avoid blocking, pass the O_NONBLOCK flag to open(). The flag is asymmetric: opening for reading with no writer yet succeeds immediately; but opening for writing with no reader yet fails with errno set to ENXIO.14 The asymmetry makes sense: reading with no writer just yields no data, while writing with no reader would raise SIGPIPE and the EPIPE error.1

Định nghĩa. Cờ O_NONBLOCK không chỉ đổi ngữ nghĩa của open() mà — vì nó bám theo open file description — còn đổi ngữ nghĩa các read()/write() sau đó. Khác biệt duy nhất khi đọc là: khi pipe rỗng mà đầu ghi vẫn mở, read() thường sẽ chặn, còn read() không chặn thất bại ngay với lỗi EAGAIN.1

Nói đơn giản. Khi đã bật chế độ "không chờ", việc đọc và ghi về sau cũng không chờ nữa. Đọc ống rỗng mà vẫn còn người ghi: bình thường thì đứng đợi, còn ở chế độ không chờ thì bị báo "chưa có gì, thử lại sau". Nhớ nhé: không chờ nghĩa là chưa sẵn thì báo lỗi ngay chứ không đứng đợi.

Definition. The O_NONBLOCK flag changes not only open() but — because it stays set on the open file description — the semantics of later read()/write() too. The only difference when reading is: with an empty pipe and the write end open, a normal read() blocks, while a nonblocking read() fails at once with EAGAIN.1

open("fifo", O_RDONLY | O_NONBLOCK)
mở để đọc, chưa ai ghi → OK ngay
mở để ghi, chưa ai đọc → lỗi ENXIO

read() ống rỗng, đầu ghi mở:
  chặn ↔ không chặn: EAGAIN

O_NONBLOCK = "đừng bắt tôi chờ"
đọc: vào ngay
ghi mà chưa ai đọc: bị từ chối
đọc ống rỗng: báo "thử lại sau"

open("fifo", O_RDONLY | O_NONBLOCK)
open for read, no writer → OK now
open for write, no reader → ENXIO

read() empty pipe, write end open:
  block ↔ nonblock: EAGAIN

  1. O_NONBLOCK

    O_NONBLOCK: không chờ, và một bất đối xứng đọc/ghi O_NONBLOCK: đọc được vào ngay, ghi thì có thể bị đuổi O_NONBLOCK: don't wait, and a read/write asymmetry

    Dùng O_NONBLOCK khi mở FIFO phục vụ hai mục đích chính: cho phép một tiến trình mở cả hai đầu FIFO (mở đọc với O_NONBLOCK trước, rồi mở ghi), và ngăn deadlock giữa các tiến trình cùng mở hai FIFO.1 Nếu đầu kia đã mở sẵn, cờ này không có tác dụng gì với open() — nó mở thành công ngay như thường.1

    Chế độ "không chờ" có hai cái lợi: một mình bạn mở được cả hai đầu ống, và tránh được cảnh hai bên cùng đứng chờ nhau mãi. Nếu đầu kia vốn đã có người mở rồi thì cờ này chẳng thay đổi gì cả. Nhớ nhé: không chờ giúp một mình mở hai đầu và tránh kẹt cứng.

    Using O_NONBLOCK when opening a FIFO serves two main purposes: it lets a single process open both ends of a FIFO (open for reading with O_NONBLOCK first, then open for writing), and it prevents deadlock between processes that each open two FIFOs.1 If the other end is already open, the flag has no effect on open() — it opens successfully at once as usual.1

    Bạn gặp nó ở đâuWhere you meet it Trong một server dùng FIFO: mở đầu đọc với O_NONBLOCK để server khởi động được mà không phải chờ client đầu tiên xuất hiện.1 Trong server dùng FIFO: mở kiểu không chờ để khởi động được ngay, khỏi đợi client đầu tiên. In a FIFO-based server: open the read end with O_NONBLOCK so the server can start without waiting for the first client to appear.1

    Đổi O_NONBLOCK trên bộ mô tả đã mởToggling O_NONBLOCK on an open descriptor

    Với bộ mô tả do pipe() trả về (không qua open(), nên không truyền cờ lúc tạo được), ta bật/tắt chế độ không chặn bằng fcntl(): lấy cờ hiện tại bằng F_GETFL, bật bit O_NONBLOCK, rồi đặt lại bằng F_SETFL.18For descriptors returned by pipe() (not via open(), so no flag can be passed at creation), toggle nonblocking mode with fcntl(): fetch the current flags with F_GETFL, set the O_NONBLOCK bit, then store them back with F_SETFL.18

    Bảng 44-1 trong sáchThe book's Table 44-1

    Kerrisk gom bốn trường hợp mở FIFO vào một bảng: mở đọc/ghi × đầu kia mở/đóng. Chỉ một ô duy nhất là thất bại — mở ghi không chặn khi đầu kia đóng, trả ENXIO; ba ô còn lại hoặc thành công ngay hoặc (ở chế độ chặn) đứng chờ.1Kerrisk collects the four FIFO-open cases into one table: open for reading/writing × other end open/closed. Only one cell fails — a nonblocking open for writing when the other end is closed, returning ENXIO; the other three either succeed at once or (in blocking mode) wait.1

  2. SIGPIPE

    Ghi khi không còn ai đọc: SIGPIPE rồi EPIPE Ghi vào ống không còn ai nghe: bị báo "ống vỡ" Writing with no reader: SIGPIPE then EPIPE

    Khi ghi vào một pipe hay FIFO mà không còn tiến trình nào mở đầu đọc, nhân gửi SIGPIPE cho bên ghi; mặc định tín hiệu này giết tiến trình. Nếu bắt hoặc bỏ qua SIGPIPE, lời write() thất bại với lỗi EPIPE.17 Kerrisk khuyên: cách dễ nhất thường là cho ứng dụng bỏ qua SIGPIPE rồi phát hiện "ống vỡ" qua lỗi EPIPE.1

    Rót nước vào ống mà đầu kia chẳng còn ai hứng thì vô nghĩa — hệ thống "đập vỡ ống" và báo cho người rót biết. Mặc định cú báo ấy giết luôn chương trình; nhưng bạn có thể chọn "bỏ qua tiếng chuông" để tự xử lý lỗi cho êm. Nhớ nhé: ghi mà không ai nghe thì ống vỡ, chương trình có thể chết nếu không lo trước.

    When writing to a pipe or FIFO with no process holding the read end open, the kernel sends SIGPIPE to the writer; by default this signal kills the process. If you catch or ignore SIGPIPE, the write() fails with EPIPE.17 Kerrisk advises: it is usually easiest to have the application ignore SIGPIPE and detect a "broken" pipe via the EPIPE error.1

    Bạn gặp nó ở đâuWhere you meet it Khi yes | head kết thúc êm: head đọc đủ rồi đóng, yes nhận SIGPIPE và dừng — đó là cơ chế này đang vận hành.7 Khi một lệnh trong pipeline tự dừng đúng lúc vì lệnh sau đã đọc đủ — đó là nhờ tín hiệu "ống vỡ". When yes | head ends cleanly: head reads enough then closes, yes gets SIGPIPE and stops — this mechanism at work.7
  3. EAGAIN

    Ngữ nghĩa write(): nguyên tử ≤ PIPE_BUF, nếu không là partial Luật ghi: ít thì trọn vẹn, nhiều thì ghi được chừng nào hay chừng ấy write() semantics: atomic if ≤ PIPE_BUF, else partial

    Kerrisk gom ngữ nghĩa write() vào Bảng 44-3. Ở chế độ chặn: ghi ≤ PIPE_BUF byte là nguyên tử (có thể chặn tới khi đủ chỗ); ghi > PIPE_BUF thì không bảo đảm nguyên tử và có thể bị xen kẽ. Ở chế độ O_NONBLOCK: nếu ghi ≤ PIPE_BUF mà không đủ chỗ, write() thất bại ngay với EAGAIN (vì không thể ghi một phần mà vẫn giữ tính nguyên tử); nếu ghi > PIPE_BUF, nó ghi được bao nhiêu hay bấy nhiêu (partial write) rồi trả về số byte đã chuyển, hoặc EAGAIN nếu không chuyển nổi một byte.17

    Khi ghi: nếu lượng đủ nhỏ (≤ PIPE_BUF) thì hoặc ghi trọn một mạch, hoặc chờ tới khi có chỗ. Nếu lượng lớn thì ghi được tới đâu hay tới đó. Ở chế độ không chờ, không ghi được ngay thì nó báo "thử lại sau" (EAGAIN) chứ không đứng đợi. Nhớ nhé: ít thì trọn vẹn, nhiều thì ghi dần, không chờ thì báo thử lại sau.

    Kerrisk collects write() semantics into Table 44-3. In blocking mode: a write of ≤ PIPE_BUF bytes is atomic (it may block until there is room); a write of > PIPE_BUF is not guaranteed atomic and may be interleaved. In O_NONBLOCK mode: if a ≤ PIPE_BUF write lacks room, write() fails at once with EAGAIN (it cannot do a partial write while keeping atomicity); if > PIPE_BUF, it writes as much as it can (a partial write) and returns the byte count, or EAGAIN if it cannot move even one byte.17

    Bạn gặp nó ở đâuWhere you meet it Khi code không chặn trả EAGAIN — đó không phải lỗi thật sự, chỉ là "chưa sẵn, hãy thử lại", thường được dùng cùng một vòng lặp sự kiện (ví dụ select()/poll()).1 Khi thấy EAGAIN ở chế độ không chờ — đừng hoảng, nó chỉ bảo "lát nữa thử lại nhé". When nonblocking code returns EAGAIN — that is not a real error, just "not ready, try again," usually paired with an event loop (e.g. select()/poll()).1

Ba ngộ nhận thường gặp Three common misconceptions

“Mỗi write() tương ứng một read() ở đầu kia” “Each write() matches one read() at the other end”

Ngộ nhận: ghi ba lần thì đọc được đúng ba khối tương ứng.

Thực tế: pipe là byte stream, không giữ ranh giới thông điệp — ba lần ghi có thể gộp thành một lần đọc, hoặc tách thành nhiều lần. Muốn có "gói", ứng dụng phải tự đóng khung.1

Myth: three writes yield exactly three matching blocks when read.

Reality: a pipe is a byte stream with no message boundaries — three writes may merge into one read, or split into several. For "messages," the application must frame them itself.1

“Ghi bao nhiêu vào pipe cũng là một thao tác nguyên tử” “Any write to a pipe is atomic, whatever its size”

Ngộ nhận: mọi write() vào pipe đều không bị trộn với bên ghi khác.

Thực tế: chỉ ghi ≤ PIPE_BUF (trên Linux là 4096) mới được bảo đảm nguyên tử; ghi lớn hơn có thể bị chia nhỏ và xen kẽ khi có nhiều bên ghi.13

Myth: every write() to a pipe is immune to interleaving with other writers.

Reality: only writes of ≤ PIPE_BUF (4096 on Linux) are guaranteed atomic; larger writes may be split and interleaved when there are several writers.13

“Mở một FIFO giống hệt mở một tệp thường” “Opening a FIFO is just like opening a regular file”

Ngộ nhận: open() trên FIFO trả về ngay như với tệp thường.

Thực tế: mặc định mở để đọc chặn tới khi có bên mở để ghi (và ngược lại) — việc mở FIFO đồng bộ hai bên; muốn không chặn phải dùng O_NONBLOCK.14

Myth: open() on a FIFO returns at once like a regular file.

Reality: by default opening for reading blocks until a writer opens it (and vice versa) — opening a FIFO synchronizes both sides; to avoid blocking, use O_NONBLOCK.14

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm pipe và FIFO Năm câu hỏi kiểm tra nhanh Five questions to check you grasped pipes and FIFOs

Trả lời được những câu này nghĩa là bạn đã nắm bản chất của pipe, cách dựng và nối nó, FIFO có tên, và ngữ nghĩa đọc/ghi — nền cho các chương IPC về sau.

Nếu trả lời được, nghĩa là bạn đã hiểu chương này.

Answering these means you grasp what a pipe is, how to build and connect one, named FIFOs, and the read/write semantics — the groundwork for the later IPC chapters.

  1. Nêu bốn đặc tính của một pipe: byte stream, một chiều, dung lượng hữu hạn, và bảo đảm nguyên tử. Trên Linux PIPE_BUF bằng bao nhiêu?

    Gợi ý sâu hơn: điều gì thay đổi về tính nguyên tử của write() giữa nhân Linux 2.2 và 2.4?

  2. pipe() trả về gì trong filedes? Vì sao đóng bộ mô tả không dùng là thiết yếu chứ không chỉ để gọn?

    Gợi ý sâu hơn: vì sao nếu bên đọc không đóng đầu ghi thì nó không bao giờ thấy cuối tệp?

  3. Làm sao nối đầu ra của một lệnh vào đầu vào của lệnh khác mà không sửa mã chúng? Vai trò của dup2() là gì?

    Gợi ý sâu hơn: popen() làm giúp bạn những bước nào, và vì sao kiểm tra đầu vào lại quan trọng với nó?

  4. FIFO khác pipe ở điểm nào? Mô tả ngữ nghĩa chặn khi mở một FIFO.

    Gợi ý sâu hơn: trong ví dụ client–server, vì sao không thể dùng chung một FIFO cho mọi hồi đáp?

  5. O_NONBLOCK đổi hành vi open() ra sao khi mở để đọc so với khi mở để ghi? Khi nào bên ghi nhận SIGPIPE/EPIPE?

    Gợi ý sâu hơn: ở chế độ không chặn, vì sao một lời ghi ≤ PIPE_BUF lại thất bại với EAGAIN thay vì ghi một phần?

  1. Pipe là cái gì, và dữ liệu chảy theo mấy chiều?

  2. Vì sao phải bịt đầu ống mình không dùng tới?

  3. Dấu | trong shell làm việc gì bên dưới?

  4. FIFO khác pipe ở chỗ nào?

  5. Ghi vào một ống mà không còn ai đọc thì chuyện gì xảy ra?

  1. What is a pipe, and in how many directions does data flow?

  2. Why must you close the pipe end you do not use?

  3. What does the shell's | do under the hood?

  4. How does a FIFO differ from a pipe?

  5. What happens when you write to a pipe with no reader left?

Khép lại chương Closing the chapter

Pipe và FIFO là kênh IPC đơn giản nhất của UNIX: một byte stream một chiều, dung lượng có hạn do nhân giữ, với ghi nguyên tử cho khối ≤ PIPE_BUF. Pipe nối người nhà, FIFO có tên nên nối cả người lạ; cả hai đòi kỷ luật đóng bộ mô tả để thấy cuối tệp và nhận SIGPIPE/EPIPE. Nắm được cái ống này, bạn đã sẵn sàng cho những kênh IPC mạnh hơn — hàng thông điệp, bộ nhớ chia sẻ, và socket.1

Pipe và FIFO là cái ống đơn giản nhất để các chương trình trao dữ liệu cho nhau: một chiều, chở byte thành dòng. Pipe cho người cùng nhà, FIFO có tên cho tất cả. Hiểu cái ống này là bước đệm sang các kênh liên lạc lớn hơn. Nhớ nhé: nắm được cái ống nhỏ này là mở đường tới mọi kênh liên lạc lớn hơn.

Pipes and FIFOs are UNIX's simplest IPC channel: a unidirectional, limited-capacity byte stream held by the kernel, with atomic writes for blocks of ≤ PIPE_BUF. Pipes join relatives; a named FIFO joins strangers too; both demand the descriptor-closing discipline to see end-of-file and receive SIGPIPE/EPIPE. Grasp this little pipe and you are ready for the stronger IPC channels — message queues, shared memory, and sockets.1