The Linux Programming Interface Chương 26 · Monitoring Child Processes

Chương 26 · Giám sát tiến trình con Chapter 26 · Monitoring Child Processes

Làm sao một tiến trình cha biết con mình đã kết thúc ra sao Làm sao cha biết đứa con của mình đã xong việc và xong thế nào How a parent learns when and how its child ended

Kerrisk mở chương bằng một nhu cầu rất thực tế: trong nhiều thiết kế, tiến trình cha cần biết khi nào một tiến trình con đổi trạng thái — khi con kết thúc, hoặc bị một tín hiệu dừng lại.1 Chương trình bày hai kỹ thuật giám sát con: lời gọi wait() (cùng các biến thể waitpid(), waitid(), wait3(), wait4()) và tín hiệu SIGCHLD.1 Dọc đường, nó giải thích giá trị trạng thái (wait status) và các macro để bóc tách nó, chuyện tiến trình mồ côi và zombie, cùng những cái bẫy khi xử lý nhiều tín hiệu SIGCHLD đến dồn dập.1

Hãy hình dung cha mẹ giao cho con một việc rồi đi làm chuyện khác. Lúc nào đó họ cần biết: con đã làm xong chưa, và làm xong êm đẹp hay gặp sự cố? Trong Linux, tiến trình cha cũng vậy: nó cần "ngóng tin" các tiến trình con. Chương này dạy hai cách ngóng tin đó — chủ động hỏi (wait()) hoặc nhận một tiếng chuông báo (SIGCHLD) — và cách đọc "giấy báo kết quả" mà con để lại. Nhớ nhé: cha phải thu dọn tin của con, nếu không con chết sẽ thành "ma" lảng vảng trong hệ thống.

Kerrisk opens with a very practical need: in many designs, a parent process needs to know when a child changes state — when the child terminates, or is stopped by a signal.1 The chapter presents two techniques for monitoring children: the wait() system call (with its variants waitpid(), waitid(), wait3(), and wait4()) and the SIGCHLD signal.1 Along the way it explains the wait status value and the macros that dissect it, the matter of orphans and zombies, and the traps of handling many SIGCHLD signals arriving in quick succession.1

Vì sao chương này quan trọng: kết thúc của một con là một sự kiện không đồng bộ — cha không đoán được khi nào nó xảy ra. Nhân giữ lại một mẩu thông tin về mỗi con đã chết cho tới khi cha "thu nhận" nó bằng một lời gọi họ wait(); nếu cha không bao giờ thu nhận, mẩu đó đọng lại dưới dạng một zombie trong bảng tiến trình. Do đó, giám sát con không chỉ để biết kết quả, mà còn là trách nhiệm dọn dẹp bắt buộc với mọi chương trình sống lâu.15Why this chapter matters: a child's death is an asynchronous event — the parent cannot predict when it happens. The kernel retains a scrap of information about each dead child until the parent "reaps" it with a call in the wait() family; if the parent never does, that scrap lingers as a zombie in the process table. So monitoring children is not only about learning the outcome, but a mandatory cleanup duty for every long-lived program.15

Luận đề của chương: một tiến trình cha giám sát con bằng họ lời gọi wait() và/hoặc tín hiệu SIGCHLD; nó phải đọc giá trị trạng thái qua các macro chuẩn (chứ không mò bit), và phải thu dọn mọi con đã chết để chúng không đọng thành zombie làm tắc bảng tiến trình.1

Ý chính: cha có hai cách ngóng tin con (hỏi bằng wait() hoặc nghe chuông SIGCHLD), phải đọc kết quả đúng cách, và phải dọn xác con đã chết. Nhớ nhé: con chết mà cha không dọn thì thành zombie làm nghẽn hệ thống.

The chapter's thesis: a parent monitors its children with the wait() family and/or the SIGCHLD signal; it must read the status value through the standard macros (not by poking at bits), and must reap every dead child so none lingers as a zombie clogging the process table.1

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

Chờ con bằng wait() — và những gì waitpid() làm tốt hơn Cha hỏi "con xong chưa?" bằng wait(), và cách hỏi khéo hơn Awaiting a child with wait() — and what waitpid() does better

Lời gọi wait(&status) chờ một con bất kỳ của tiến trình gọi kết thúc, rồi trả về PID của con đó và đặt trạng thái kết thúc vào ô mà status trỏ tới.12 Nếu chưa có con nào (chưa được chờ trước đó) kết thúc, lời gọi chặn cho tới khi có; nếu đã có một con chết sẵn, nó trả về ngay. Khi tiến trình không còn con nào để chờ, wait() trả về −1 với errno bằng ECHILD — nhờ đó ta có vòng lặp dọn hết con: while ((pid = wait(NULL)) != -1) ;.12

Cách đơn giản nhất để cha ngóng con là gọi wait(). Nó như câu "để ba đợi một đứa nào đó làm xong đã": cha đứng chờ cho tới khi một đứa con bất kỳ hoàn thành, rồi nhận về số của đứa đó và kết quả nó làm. Khi không còn đứa con nào nữa, wait() báo "hết con rồi". Nhớ nhé: wait() bắt cha đứng chờ cho tới khi có một đứa con xong việc.

The call wait(&status) waits for any one child of the calling process to terminate, then returns that child's PID and places its termination status in the location pointed to by status.12 If no (previously unwaited-for) child has terminated, the call blocks until one does; if a child has already died, it returns immediately. When the process has no more children to wait for, wait() returns −1 with errno set to ECHILD — giving the reap-them-all loop while ((pid = wait(NULL)) != -1) ;.12

Định nghĩa. Thuật ngữ wait status bao trùm mọi sự kiện mà cha có thể nhận biết về con: con kết thúc bình thường qua _exit(), bị một tín hiệu giết, bị một tín hiệu dừng, hoặc được tiếp tục bằng SIGCONT. Hai trường hợp đầu còn được gọi riêng là termination status (trạng thái kết thúc).12

Nói đơn giản. "Trạng thái" mà cha nhận được cho biết chuyện gì đã xảy ra với con: tự xong việc, bị giết, bị tạm dừng, hay được chạy tiếp. Nhớ nhé: wait status là bản tin tóm tắt số phận của đứa con.

Definition. The term wait status encompasses every event the parent can learn about a child: the child terminated normally via _exit(), was killed by a signal, was stopped by a signal, or was resumed by SIGCONT. The first two cases are also called the termination status.12

#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
/* trả về PID con, hoặc −1 khi lỗi */
/* ECHILD: không còn con để chờ */

wait(&status) --> chờ một đứa con bất kỳ
waitpid(pid, &status, options)
  --> chờ đúng đứa mình chọn
(trả về số của con, hoặc −1)

#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
/* return child PID, or −1 on error */
/* ECHILD: no more children to wait for */

  1. wait()

    wait() chờ một con bất kỳ — ba giới hạn của nó wait() chờ một đứa bất kỳ, và nó hơi "cứng" wait() takes any child — and its three limits

    Dù đơn giản, wait() có ba hạn chế mà Kerrisk nêu rõ: (1) không thể chờ một con cụ thể, chỉ chờ con kế tiếp kết thúc; (2) nếu chưa con nào chết, nó luôn chặn — không có kiểu chờ không-chặn; (3) chỉ phát hiện con đã kết thúc, không báo khi con bị dừng hay được tiếp tục.1 Ngoài ra, SUSv3 để không xác định thứ tự thu các con khi nhiều con cùng kết thúc tại một thời điểm — và hành vi này khác nhau ngay cả giữa các phiên bản nhân Linux.1

    wait() tiện nhưng hơi cứng: cha không chọn được chờ đứa nào, phải đứng im chờ, và chỉ biết khi con "xong hẳn" chứ không biết lúc con bị tạm dừng. Nhớ nhé: wait() chỉ hợp khi cha sẵn lòng đứng chờ bất kỳ đứa con nào.

    Simple as it is, wait() has three limits Kerrisk spells out: (1) you cannot wait for a specific child, only the next to terminate; (2) if no child has died, it always blocks — there is no nonblocking wait; (3) it detects only a terminated child, not one stopped or resumed.1 Moreover, SUSv3 leaves unspecified the order in which children are reaped when several terminate at once — and this behaviour even varies across Linux kernel versions.1

    Bạn gặp nó ở đâuWhere you meet it Trong một chương trình tạo vài con rồi muốn chờ tất cả xong mới tiếp tục — vòng lặp while (wait(NULL) != -1) là đủ, và kết thúc gọn khi gặp ECHILD.1 Khi cha chỉ cần chờ cho hết mọi đứa con làm xong rồi mới đi tiếp — lặp wait() tới khi báo "hết con". In a program that spawns a few children and wants to wait for them all before proceeding — the loop while (wait(NULL) != -1) suffices, ending cleanly on ECHILD.1
  2. waitpid()

    waitpid(): chọn con, chờ không-chặn, thấy cả con bị dừng waitpid(): hỏi khéo hơn — chọn đứa, khỏi đứng chờ waitpid(): pick a child, poll, and see stops too

    waitpid(pid, &status, options) sinh ra để gỡ đúng ba hạn chế trên. Đối số pid chọn con cần chờ: pid > 0 là một con cụ thể; pid == 0 là con bất kỳ cùng nhóm tiến trình với cha; pid < -1 là con bất kỳ trong nhóm tiến trình có mã bằng trị tuyệt đối của pid; còn pid == -1 là con bất kỳ — khi đó waitpid(-1, &status, 0) tương đương hệt wait(&status).12 Trường options là mặt nạ bit gồm WUNTRACED (cũng báo khi con bị dừng), WCONTINUED (báo khi con được SIGCONT cho chạy tiếp; có từ Linux 2.6.10) và WNOHANG (thăm dò, trả về 0 ngay nếu chưa con nào đổi trạng thái).12

    waitpid() là bản "hỏi khéo" của wait(): cha chọn đúng đứa con muốn chờ, hoặc chỉ liếc một cái rồi đi luôn nếu chưa đứa nào xong (nhờ cờ WNOHANG), và còn biết được khi con bị tạm dừng. Nhớ nhé: cần chọn đứa hay cần "liếc rồi đi" thì dùng waitpid, không dùng wait.

    waitpid(pid, &status, options) was designed to remove exactly those three limits. The pid argument selects the child: pid > 0 is one specific child; pid == 0 is any child in the caller's own process group; pid < -1 is any child in the process group whose ID equals the absolute value of pid; and pid == -1 is any child — so waitpid(-1, &status, 0) is exactly equivalent to wait(&status).12 The options bit mask holds WUNTRACED (also report a stopped child), WCONTINUED (report a child resumed by SIGCONT; since Linux 2.6.10), and WNOHANG (poll, returning 0 at once if no child has yet changed state).12

    Bạn gặp nó ở đâuWhere you meet it Trong một máy chủ hay shell cần tiếp tục làm việc khác mà vẫn thỉnh thoảng thu con đã chết — gọi waitpid(-1, &status, WNOHANG) định kỳ để "thăm dò" mà không bị treo.1 Khi cha đang bận việc khác nhưng vẫn muốn dọn con đã xong — liếc bằng WNOHANG rồi đi tiếp. In a server or shell that must keep doing other work while occasionally reaping dead children — call waitpid(-1, &status, WNOHANG) periodically to "poll" without blocking.1

    Tên WUNTRACED có gốc lịch sửThe name WUNTRACED is historical

    SUSv3 ghi rằng cái tên WUNTRACED là một di sản từ BSD, nơi một tiến trình có thể bị dừng theo hai cách: do bị ptrace() theo dõi, hoặc bị dừng mà không bị theo dõi. Với một con đang bị ptrace() theo dõi, việc chuyển bất kỳ tín hiệu nào (trừ SIGKILL) khiến con dừng và một SIGCHLD được gửi cho cha.1SUSv3 notes that the name WUNTRACED is a BSD-era artifact, where a process could be stopped in two ways: by being traced with ptrace(), or by being stopped while not traced. For a child being traced with ptrace(), delivering any signal (other than SIGKILL) stops the child and sends a SIGCHLD to the parent.1

    WNOHANG trả về 0, không phải −1WNOHANG returns 0, not −1

    Khi dùng WNOHANG mà chưa con nào (khớp với pid) đổi trạng thái, waitpid() trả về 0 (chứ không phải −1). Nếu tiến trình gọi hoàn toàn không có con nào khớp, nó mới trả về −1 với errno là ECHILD.12With WNOHANG set and no matching child having changed state, waitpid() returns 0 (not −1). Only if the caller has no matching children at all does it return −1 with errno set to ECHILD.12

  3. waitid()

    waitid(): điều khiển sự kiện chính xác hơn, qua siginfo_t waitid(): hỏi chi tiết hơn, nhận cả "hồ sơ" của con waitid(): finer event control via siginfo_t

    waitid(idtype, id, &info, options) cũng trả về trạng thái con, nhưng cho khả năng mà waitpid() không có.12 Lời gọi này đến từ System V, nay được SUSv3 quy định, và được thêm vào Linux ở nhân 2.6.9.1 Cặp idtype/id chọn con (P_ALL, P_PID, P_PGID). Khác biệt lớn nhất: ta chọn chính xác loại sự kiện bằng cách OR các cờ WEXITED, WSTOPPED, WCONTINUED — thay vì "gói gọn" như ở waitpid(). Khi thành công, nó điền cấu trúc siginfo_t: si_code cho biết loại sự kiện (CLD_EXITED, CLD_KILLED, CLD_STOPPED, CLD_CONTINUED), cùng si_pid, si_signo (luôn là SIGCHLD), si_status và si_uid.13

    waitid() là bản "hỏi tỉ mỉ" nhất: cha nói rõ chỉ muốn nghe loại tin nào (con chết? con dừng? con chạy lại?), và nhận về cả một tấm hồ sơ nhỏ ghi rõ chuyện gì xảy ra với đứa nào. Nhớ nhé: cần phân biệt rạch ròi từng loại sự kiện thì dùng waitid.

    waitid(idtype, id, &info, options) also returns child status, but with power waitpid() lacks.12 It derives from System V, is now specified by SUSv3, and was added to Linux in kernel 2.6.9.1 The idtype/id pair selects the child (P_ALL, P_PID, P_PGID). The key difference: you select the precise event type by ORing WEXITED, WSTOPPED, WCONTINUED — rather than the bundled behaviour of waitpid(). On success it fills a siginfo_t: si_code gives the event type (CLD_EXITED, CLD_KILLED, CLD_STOPPED, CLD_CONTINUED), along with si_pid, si_signo (always SIGCHLD), si_status, and si_uid.13

    Bạn gặp nó ở đâuWhere you meet it Cờ WNOWAIT: bình thường chờ xong một con là "tiêu thụ" sự kiện đó; với WNOWAIT, con vẫn ở trạng thái chờ được, nên sau này có thể chờ lại để lấy đúng thông tin ấy.13 Có cờ WNOWAIT để "xem trước mà chưa dọn" — đọc tin con nhưng để dành dọn sau. The WNOWAIT flag: normally waiting on a child consumes that event; with WNOWAIT the child stays waitable, so you can wait for it again later to retrieve the same information.13

    Bẫy với WNOHANG và trả về 0A trap with WNOHANG and a 0 return

    Khi dùng WNOHANG, một giá trị trả về 0 từ waitid() có thể nghĩa hai điều: hoặc một con đã đổi trạng thái (thông tin nằm trong info), hoặc không con nào đổi trạng thái. Cách khả chuyển để phân biệt là memset() cấu trúc siginfo_t về 0 trước khi gọi, rồi kiểm tra info.si_pid sau đó.13With WNOHANG, a 0 return from waitid() can mean two things: either a child changed state (details in info), or no child changed state. The portable way to tell them apart is to memset() the siginfo_t to zero before the call, then test info.si_pid afterward.13

    Không có "0 nghĩa là nhóm của tôi"No "0 means my group"

    Khác waitpid(), không thể đặt id bằng 0 để nói "nhóm tiến trình của tôi"; với P_PGID phải ghi rõ mã nhóm, lấy từ getpgrp().1Unlike waitpid(), you cannot set id to 0 to mean "my own process group"; with P_PGID you must give the group ID explicitly, from getpgrp().1

  4. wait3() / wait4()

    wait3()/wait4(): giống waitpid() nhưng kèm thống kê tài nguyên wait3()/wait4(): còn cho biết con đã tốn bao nhiêu sức wait3()/wait4(): like waitpid(), plus resource usage

    Hai lời gọi wait3() và wait4() làm công việc tương tự waitpid(), khác biệt chính là chúng trả thêm thông tin sử dụng tài nguyên của con đã kết thúc trong một cấu trúc struct rusage — gồm thời gian CPU đã dùng và các số liệu quản lý bộ nhớ.14 Bỏ qua phần rusage, wait3() tương đương waitpid(-1, &status, options) (chờ con bất kỳ), còn wait4() tương đương waitpid(pid, &status, options) (chọn được con). Tên của chúng phản ánh số đối số mỗi lời gọi nhận.1

    Hai lời gọi này giống waitpid() nhưng kèm một tờ "hóa đơn" cho biết đứa con đã ngốn bao nhiêu thời gian máy và bộ nhớ. Nhớ nhé: chỉ dùng wait3/wait4 khi thật sự cần biết con đã tốn bao nhiêu tài nguyên.

    The wait3() and wait4() calls do a task similar to waitpid(), the principal difference being that they also return resource-usage information about the terminated child in a struct rusage — including CPU time used and memory-management statistics.14 Excluding the rusage argument, wait3() is equivalent to waitpid(-1, &status, options) (any child), and wait4() to waitpid(pid, &status, options) (a selectable child). Their names reflect the number of arguments each takes.1

    Bạn gặp nó ở đâuWhere you meet it Hiếm khi cần: Kerrisk nói sách thường tránh dùng wait3()/wait4() vì ít khi cần số liệu thêm, và vì thiếu chuẩn hóa làm giảm tính khả chuyển.1 Ít khi cần tới: phần lớn chương trình không quan tâm con tốn bao nhiêu, nên dùng wait/waitpid là đủ. Rarely needed: Kerrisk says the book usually avoids wait3()/wait4() because the extra figures are seldom required, and their lack of standardization limits portability.1

    Nguồn gốc BSD, không trong SUSv3BSD origin, absent from SUSv3

    Cả hai bắt nguồn từ BSD và nay có trên hầu hết hệ UNIX, nhưng không được chuẩn hóa trong SUSv3. SUSv2 từng quy định wait3() nhưng đánh dấu nó là LEGACY (lỗi thời).1Both originated in BSD and are now on most UNIX systems, but neither is standardized in SUSv3. SUSv2 did specify wait3() but marked it LEGACY.1

    Thu tài nguyên của con bị dừngUsage for stopped children

    Trên một số hệ UNIX, wait3()/wait4() chỉ trả tài nguyên cho con đã kết thúc; trên Linux, còn lấy được cho con bị dừng nếu truyền cờ WUNTRACED.14On some UNIX systems, wait3()/wait4() return usage only for terminated children; on Linux it can also be retrieved for stopped children if the WUNTRACED flag is passed.14

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

Đọc giá trị trạng thái: dùng macro, đừng mò bit Đọc "giấy báo kết quả" của con bằng công cụ sẵn có Reading the status value: use macros, don't poke bits

Giá trị trạng thái tuy khai báo là int nhưng thực tế chỉ dùng hai byte thấp; cách sắp xếp hai byte này tùy sự kiện đã xảy ra (Figure 26-1 trong sách minh họa bố cục trên Linux/x86-32).1 SUSv3 không quy định bố cục này — thậm chí không buộc nó nằm ở hai byte thấp — nên chương trình khả chuyển phải luôn dùng các macro trong <sys/wait.h> để kiểm tra, chứ không soi trực tiếp các bit.12

Con số trạng thái giống một tờ phiếu mã hóa: bên trong có ghi "con tự xong", "con bị giết bởi tín hiệu số mấy", hay "con đang bị tạm dừng". Đừng tự tay cạy tờ phiếu ra xem từng chữ — đã có sẵn những "chiếc kính lúp" (macro) để đọc đúng từng mục. Nhớ nhé: luôn đọc trạng thái bằng macro, đừng tự giải mã bit.

The status value, though declared int, really uses only its bottom two bytes; how those bytes are laid out depends on the event (the book's Figure 26-1 shows the layout on Linux/x86-32).1 SUSv3 does not specify this layout — it does not even require it to be in the bottom two bytes — so portable programs must always use the macros in <sys/wait.h> to inspect it, never read the bits directly.12

Định nghĩa. Khi áp các macro lên một giá trị status, đúng một macro trong nhóm chính sẽ trả về true, cho biết thuộc trường hợp nào. Lưu ý quan trọng: tuy tên đối số cũng là status, các macro này nhận một số nguyên thuần, chứ không phải con trỏ như wait()/waitpid() đòi hỏi.12

Nói đơn giản. Trong bốn chiếc kính lúp "con tự xong / bị giết / bị dừng / được chạy tiếp", chỉ đúng một chiếc sáng đèn cho mỗi tờ phiếu. Nhớ nhé: với một trạng thái, chỉ một macro WIF… đúng.

Definition. Applied to a status value, exactly one of the main macros returns true, telling you which case occurred. One important note: although its argument is also called status, these macros take a plain integer, not the pointer that wait()/waitpid() require.12

if (WIFEXITED(status))
  n = WEXITSTATUS(status); /* 0–255 */
else if (WIFSIGNALED(status))
  sig = WTERMSIG(status);
else if (WIFSTOPPED(status))
  sig = WSTOPSIG(status);
else if (WIFCONTINUED(status))
  /* được SIGCONT cho chạy tiếp */ ;

WIFEXITED --> con tự xong?
WEXITSTATUS --> mã kết thúc
WIFSIGNALED --> bị tín hiệu giết?
WTERMSIG --> tín hiệu số mấy
WIFSTOPPED --> đang bị dừng?

if (WIFEXITED(status))
  n = WEXITSTATUS(status); /* 0–255 */
else if (WIFSIGNALED(status))
  sig = WTERMSIG(status);
else if (WIFSTOPPED(status))
  sig = WSTOPSIG(status);
else if (WIFCONTINUED(status))
  /* resumed by SIGCONT */ ;

  1. WIFEXITED

    Kết thúc bình thường: WIFEXITED và WEXITSTATUS Con tự làm xong: đọc mã kết thúc Normal exit: WIFEXITED and WEXITSTATUS

    WIFEXITED(status) trả về true nếu con kết thúc bình thường (gọi _exit() hoặc exit()). Khi đó WEXITSTATUS(status) cho mã kết thúc — nhưng chỉ byte thấp nhất của giá trị con truyền cho _exit() là đến được cha, nên dải hợp lệ là 0–255.12 Ví dụ trong sách: con thoát với mã 23 cho status là 0x1700, tách ra thành hai byte (23, 0).1

    Nếu con tự chạy hết việc rồi dừng, WIFEXITED báo "đúng, con tự xong", và WEXITSTATUS cho con số kết quả (0 thường là "ổn"). Con số này chỉ nằm trong khoảng 0 đến 255. Nhớ nhé: mã kết thúc chỉ giữ lại byte cuối, nên luôn trong 0–255.

    WIFEXITED(status) is true if the child terminated normally (via _exit() or exit()). Then WEXITSTATUS(status) gives the exit status — but only the least-significant byte of the value the child passed to _exit() reaches the parent, so the valid range is 0–255.12 The book's example: a child exiting with status 23 gives a status of 0x1700, splitting into the two bytes (23, 0).1

    Bạn gặp nó ở đâuWhere you meet it Trong shell, biến $? chính là mã kết thúc của lệnh vừa chạy — đúng giá trị mà WEXITSTATUS trả về cho tiến trình cha là shell.1 Trong terminal, gõ echo $? ngay sau một lệnh là thấy mã kết thúc của nó. In the shell, the $? variable is the exit status of the last command — exactly the value WEXITSTATUS returns to the parent shell.1
  2. WIFSIGNALED

    Bị giết bởi tín hiệu: WTERMSIG và WCOREDUMP Con bị "tuýt còi" dừng hẳn: tín hiệu nào? Killed by a signal: WTERMSIG and WCOREDUMP

    WIFSIGNALED(status) true nếu con bị một tín hiệu giết. Khi đó WTERMSIG(status) cho số hiệu tín hiệu đã giết nó, và WCOREDUMP(status) true nếu con còn tạo ra một tệp core dump.12 Ví dụ trong sách: gửi SIGABRT (số 6) cho con khi core dump đang bị tắt cho status 0x0006 (0, 6); khi bật core dump, status là 0x0086 (0, 134) — bit 0x80 báo đã có core dump.1

    Nếu con không tự dừng mà bị một "tiếng còi" (tín hiệu) bắt dừng hẳn, WIFSIGNALED báo điều đó, và WTERMSIG cho biết là tiếng còi số mấy. Đôi khi con còn để lại một "ảnh chụp hiện trường" gọi là core dump. Nhớ nhé: WTERMSIG cho biết tín hiệu nào đã kết liễu đứa con.

    WIFSIGNALED(status) is true if the child was killed by a signal. Then WTERMSIG(status) gives the number of that signal, and WCOREDUMP(status) is true if the child also produced a core dump file.12 The book's example: sending SIGABRT (signal 6) to a child with core dumps disabled gives a status of 0x0006 (0, 6); with core dumps enabled, status is 0x0086 (0, 134) — the 0x80 bit flags that a core was dumped.1

    Bạn gặp nó ở đâuWhere you meet it Khi một tiến trình con "biến mất" mà không in gì — kiểm tra WIFSIGNALED/WTERMSIG ở cha thường lộ ra nó bị SIGSEGV, SIGKILL hay SIGABRT.1 Khi một tiến trình con tự dưng "mất tích" — hỏi cha xem nó có bị tín hiệu nào giết không. When a child "vanishes" printing nothing — checking WIFSIGNALED/WTERMSIG in the parent often reveals it was hit by SIGSEGV, SIGKILL, or SIGABRT.1

    WCOREDUMP không thuộc SUSv3WCOREDUMP is not in SUSv3

    Nguồn có chỗ khác nhau về phạm vi macro: Kerrisk ghi rõ WCOREDUMP() không được SUSv3 quy định, dù có mặt trên hầu hết hệ UNIX; theo đúng sách, chương trình khả chuyển nên bọc nó trong #ifdef WCOREDUMP.12Sources differ on macro coverage: Kerrisk is explicit that WCOREDUMP() is not specified by SUSv3, though present on most UNIX systems; following the book, portable programs should guard it with #ifdef WCOREDUMP.12

    An toàn tín hiệu khi in trạng tháiSignal safety when printing status

    Hàm printWaitStatus() (Listing 26-2) dùng printf(), vốn không an toàn với tín hiệu không đồng bộ (async-signal-safe); do đó không nên gọi nó từ bên trong một trình xử lý SIGCHLD.1The printWaitStatus() helper (Listing 26-2) uses printf(), which is not async-signal-safe; so it should not be called from inside a SIGCHLD handler.1

  3. WIFSTOPPED

    Bị dừng hoặc được tiếp tục: WIFSTOPPED, WIFCONTINUED Con tạm nghỉ hay chạy lại: hai macro nữa Stopped or resumed: WIFSTOPPED, WIFCONTINUED

    WIFSTOPPED(status) true nếu con bị một tín hiệu dừng; khi đó WSTOPSIG(status) cho số hiệu tín hiệu đã dừng nó. WIFCONTINUED(status) true nếu con được tiếp tục bằng SIGCONT — macro này có từ Linux 2.6.10.12 Ví dụ trong sách: gửi SIGSTOP (số 19) cho con cho status 0x137f (19, 127) — byte thấp 0x7f là dấu hiệu "bị dừng"; gửi SIGCONT cho status 0xffff (255, 255).1

    Con không phải lúc nào cũng "xong" hay "bị giết" — đôi khi nó chỉ tạm nghỉ rồi lại chạy tiếp. Hai macro này cho cha biết hai tình huống đó. Nhớ nhé: dừng và tiếp tục cũng là tin đáng theo dõi, không chỉ chuyện sống chết.

    WIFSTOPPED(status) is true if the child was stopped by a signal; then WSTOPSIG(status) gives the number of that stopping signal. WIFCONTINUED(status) is true if the child was resumed by SIGCONT — this macro exists since Linux 2.6.10.12 The book's example: sending SIGSTOP (signal 19) gives a status of 0x137f (19, 127) — the low byte 0x7f marks "stopped"; sending SIGCONT gives 0xffff (255, 255).1

    Bạn gặp nó ở đâuWhere you meet it Để nhận các sự kiện dừng/tiếp tục qua waitpid(), phải truyền WUNTRACED và/hoặc WCONTINUED trong options — nếu không, lời gọi chỉ báo khi con đã kết thúc.12 Muốn nghe tin "con tạm nghỉ / chạy lại" thì phải bật cờ WUNTRACED/WCONTINUED khi gọi waitpid. To receive stop/continue events through waitpid(), you must pass WUNTRACED and/or WCONTINUED in options — otherwise the call reports only a terminated child.12

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

Mồ côi, zombie, và tín hiệu SIGCHLD: trách nhiệm dọn dẹp Con mồ côi, con "ma", và chuông báo con chết Orphans, zombies, and SIGCHLD: the duty to clean up

Vòng đời của cha và con thường không trùng nhau — bên này có thể sống lâu hơn bên kia, làm nảy ra hai câu hỏi.1 Nếu cha chết trước, con thành mồ côi và được init (tổ tiên của mọi tiến trình, PID 1) nhận nuôi; từ đó getppid() của con trả về 1.1 Nếu con chết trước khi cha kịp wait(), nhân biến con thành zombie: hầu hết tài nguyên được trả lại hệ thống, chỉ còn một mục trong bảng tiến trình ghi PID, trạng thái kết thúc và thống kê tài nguyên — chờ cha đến thu.15

Cha và con hiếm khi "ra đi" cùng lúc, nên có hai cảnh. Cảnh một: cha mất trước, con thành mồ côi và được ông tổ init (số 1) nhận nuôi. Cảnh hai: con mất trước mà cha chưa kịp dọn, con biến thành ma (zombie) — thân xác tan gần hết, chỉ còn một tờ hồ sơ nằm đó chờ cha ký nhận. Nhớ nhé: mồ côi được init nhận nuôi; con chết chưa ai dọn thì thành zombie.

The lifetimes of parent and child usually differ — either may outlive the other, raising two questions.1 If the parent dies first, the child becomes an orphan and is adopted by init (the ancestor of all processes, PID 1); thereafter the child's getppid() returns 1.1 If the child dies before the parent can wait(), the kernel turns the child into a zombie: most of its resources are returned to the system, leaving only a process-table entry recording the PID, termination status, and resource-usage statistics — awaiting the parent's reap.15

Định nghĩa. Kết thúc của một con là một sự kiện không đồng bộ: cha không đoán được khi nào. Thay vì chặn ở wait() hoặc thăm dò lặp đi lặp lại bằng WNOHANG (cả hai đều bất tiện), cha có thể cài một trình xử lý cho tín hiệu SIGCHLD — tín hiệu nhân gửi cho cha mỗi khi một con kết thúc (và, nếu muốn, khi con bị dừng).15

Nói đơn giản. Cha không biết trước lúc nào con xong. Thay vì đứng chờ hoài hay cứ chốc chốc ngó một cái, cha có thể lắp một "chuông cửa": hễ con chết là chuông SIGCHLD reo, cha chạy ra dọn. Nhớ nhé: SIGCHLD là chuông báo "có con vừa chết, ra dọn đi".

Definition. A child's termination is an asynchronous event: the parent cannot predict when it happens. Rather than blocking in wait() or polling repeatedly with WNOHANG (both inconvenient), the parent can install a handler for the SIGCHLD signal — which the kernel sends to the parent whenever a child terminates (and, if requested, when a child is stopped).15

/* thân một handler SIGCHLD điển hình */
while (waitpid(-1, NULL, WNOHANG) > 0)
  continue;
/* lặp tới khi trả về 0 (hết con chết)
  hoặc −1 (ECHILD: hết con) */

Trong chuông báo (handler):
while (waitpid(-1,NULL,WNOHANG) > 0)
  ; /* dọn hết mọi con đã chết */

/* body of a typical SIGCHLD handler */
while (waitpid(-1, NULL, WNOHANG) > 0)
  continue;
/* loop until it returns 0 (no more dead)
  or −1 (ECHILD: no more children) */

  1. orphan → init

    Mồ côi: con mất cha được init nhận nuôi Con mất cha thì ai lo? Ông tổ init Orphans: a parentless child is adopted by init

    Khi cha ruột kết thúc trước, con không bị bỏ rơi: nó được init (PID 1) nhận nuôi. Vì vậy, một lời gọi getppid() trả về 1 có thể dùng để biết cha ruột của một con có còn sống không (với giả định con này không do chính init tạo ra).1 init cũng gánh việc wait() cho những con mồ côi này khi chúng chết, nên chúng không đọng lại thành zombie.1

    Cha mất trước thì con không "bơ vơ": ông tổ init (số 1) đứng ra nhận nuôi. Nhờ vậy, thấy một tiến trình có "cha" là số 1 nghĩa là cha ruột của nó đã đi rồi. Nhớ nhé: không có cha thì init nhận nuôi, và init cũng lo dọn khi con mồ côi chết.

    When the real parent terminates first, the child is not abandoned: it is adopted by init (PID 1). So a getppid() returning 1 can tell you whether a child's real parent is still alive (assuming the child was not created by init itself).1 init also takes on the job of wait()-ing for these orphans when they die, so they never linger as zombies.1

    Bạn gặp nó ở đâuWhere you meet it Thủ thuật tạo "daemon": một tiến trình cố ý fork() rồi để cha thoát, khiến con thành mồ côi được init nhận nuôi và chạy nền tách khỏi terminal.1 Mẹo tạo tiến trình nền: cho cha thoát để con thành mồ côi, được init nuôi và chạy âm thầm. The daemon trick: a process deliberately fork()s and lets the parent exit, so the child becomes an orphan adopted by init and runs in the background, detached from the terminal.1

    PR_SET_PDEATHSIGPR_SET_PDEATHSIG

    Với thao tác PR_SET_PDEATHSIG của lời gọi prctl() (riêng Linux), một tiến trình có thể yêu cầu được nhận một tín hiệu định sẵn vào lúc nó trở thành mồ côi — hữu ích khi con muốn tự dọn nếu cha đột ngột chết.1With the PR_SET_PDEATHSIG operation of the Linux-specific prctl() call, a process can arrange to receive a given signal when it becomes an orphan — useful when a child wants to clean up if its parent dies suddenly.1

  2. zombie

    Zombie: không giết nổi, và làm tắc bảng tiến trình Con "ma": không giết được, để lâu thì nghẽn máy Zombies: unkillable, and they clog the process table

    Kerrisk nói đùa rằng UNIX bắt chước phim: một zombie không thể bị giết bằng tín hiệu — kể cả "viên đạn bạc" SIGKILL — để bảo đảm cha luôn có cơ hội wait() lấy trạng thái.1 Khi cha wait(), nhân xóa zombie. Nếu cha chết mà chưa wait(), init nhận nuôi và tự thu. Nhưng nếu cha sống mà quên thu, mục zombie tồn tại vô thời hạn; tạo nhiều con kiểu đó sẽ dần làm đầy bảng tiến trình của nhân, chặn việc tạo tiến trình mới. Lệnh ps(1) hiển thị zombie bằng chuỗi <defunct>.1

    Con "ma" có một điểm lạ: bắn kiểu gì cũng không chết thêm được nữa — ngay cả lệnh giết mạnh nhất. Lý do: hệ thống giữ nó lại để chờ cha đến "ký nhận". Nếu cha cứ sống mà quên ký, lũ ma chất đống làm nghẽn cả máy. Lệnh ps ghi chúng là <defunct>. Nhớ nhé: zombie vô hại một mình nhưng để đầy sẽ làm máy hết chỗ tạo tiến trình mới.

    Kerrisk jokes that UNIX imitates the movies: a zombie cannot be killed by a signal — not even the "silver bullet" SIGKILL — so the parent always gets a chance to wait() for its status.1 When the parent wait()s, the kernel removes the zombie. If the parent dies without waiting, init adopts and reaps it. But if the parent lives yet forgets to reap, the zombie entry persists indefinitely; creating many such children gradually fills the kernel's process table, blocking new process creation. The ps(1) command shows a zombie with the string <defunct>.1

    Bạn gặp nó ở đâuWhere you meet it Trong máy chủ mạng hay shell sống lâu, tạo nhiều con: phải wait() đều đặn, nếu không zombie tích tụ và cuối cùng không fork() được nữa.1 Trong chương trình chạy mãi và sinh nhiều con, phải dọn con đều, kẻo một ngày máy báo "hết chỗ". In a long-lived network server or shell that spawns many children: you must wait() regularly, or zombies pile up until you can no longer fork().1

    Cách duy nhất "giết" một zombieThe only way to "kill" a zombie

    Vì zombie không chết bởi tín hiệu, cách duy nhất để xóa một đống zombie của một cha cẩu thả là giết chính cha đó (hoặc chờ cha thoát): khi ấy lũ zombie được init nhận nuôi và thu dọn.1Since zombies ignore signals, the only way to clear a careless parent's pile of zombies is to kill that parent (or wait for it to exit): the zombies are then adopted and reaped by init.1

    Minh họa: make_zombieDemo: make_zombie

    Listing 26-4 trong sách tạo một zombie rồi gửi SIGKILL cho nó; ảnh chụp ps trước và sau cho thấy tiến trình vẫn ở trạng thái <defunct> — bằng chứng rằng SIGKILL không giết được zombie.1The book's Listing 26-4 creates a zombie and sends it SIGKILL; the ps snapshots before and after show it still in the <defunct> state — proof that SIGKILL cannot kill a zombie.1

  3. SIGCHLD handler

    Handler SIGCHLD: cái bẫy tín hiệu không xếp hàng Chuông SIGCHLD reo thiếu: phải dọn theo vòng lặp The SIGCHLD handler: the un-queued-signal trap

    Cài handler SIGCHLD rồi gọi wait() bên trong là cách thu zombie gọn gàng. Nhưng có một cái bẫy: SIGCHLD là tín hiệu chuẩn, mà tín hiệu chuẩn không xếp hàng. Khi handler đang chạy, SIGCHLD bị chặn tạm thời; nếu hai, ba con cùng chết dồn dập trong lúc đó, nhân chỉ sinh SIGCHLD một lần. Một handler chỉ gọi wait() đúng một lần mỗi lần được gọi sẽ bỏ sót zombie.15 Lời giải: lặp trong handler — while (waitpid(-1, NULL, WNOHANG) > 0) ; — cho đến khi hết con chết.1

    Chuông SIGCHLD có tật: nếu ba đứa con cùng chết gần như một lúc, chuông chỉ reo một tiếng chứ không reo ba. Nếu mỗi lần nghe chuông cha chỉ dọn một đứa, hai đứa kia bị bỏ quên thành ma. Cách chữa: mỗi lần nghe chuông, cha dọn liên tục tới khi không còn đứa nào. Nhớ nhé: nghe chuông SIGCHLD một tiếng, phải dọn tới khi hết con chết, không dọn đúng một đứa.

    Installing a SIGCHLD handler and calling wait() inside it is a tidy way to reap zombies. But there is a trap: SIGCHLD is a standard signal, and standard signals are not queued. While the handler runs, SIGCHLD is temporarily blocked; if two or three children die in quick succession then, the kernel generates SIGCHLD only once. A handler that calls wait() just once per invocation will miss zombies.15 The fix: loop inside the handler — while (waitpid(-1, NULL, WNOHANG) > 0) ; — until no dead children remain.1

    Bạn gặp nó ở đâuWhere you meet it Reentrancy: vì waitpid() trong handler có thể đổi errno, nên lưu errno vào biến cục bộ ở đầu handler và khôi phục trước khi trả về, tránh phá giá trị errno mà chương trình chính đang dùng.1 Trong chuông báo, nhớ cất lại số lỗi (errno) rồi trả về đúng như cũ, kẻo làm rối chương trình chính. Reentrancy: because waitpid() in the handler may change errno, save errno into a local at the handler's start and restore it before returning, so you do not corrupt the errno the main program relies on.1

    Cài handler trước khi tạo conInstall the handler before creating children

    SUSv3 để không xác định liệu nhân có sinh SIGCHLD cho một con đã chết sẵn vào lúc ta mới cài handler hay không; một số hệ System V thì có, Linux thì không. Ứng dụng khả chuyển nên cài handler trước khi tạo bất kỳ con nào.15SUSv3 leaves unspecified whether the kernel generates a SIGCHLD for a child that had already died when the handler is installed; some System V systems do, Linux does not. Portable applications should install the handler before creating any children.15

    Bỏ qua con đã chết: SIG_IGNIgnoring dead children: SIG_IGN

    Đặt tường minh disposition của SIGCHLD thành SIG_IGN khiến con kết thúc được xóa ngay thay vì thành zombie — nhưng khi đó trạng thái bị vứt bỏ, wait() không lấy lại được gì. Đây là điểm đặc biệt: dù mặc định SIGCHLD vốn đã bị bỏ qua, việc đặt tường minh SIG_IGN lại cho hành vi khác — SIGCHLD là tín hiệu duy nhất có nét riêng này. Cờ SA_NOCLDWAIT của sigaction() cho hiệu ứng tương tự.156Explicitly setting the disposition of SIGCHLD to SIG_IGN makes terminating children be removed at once instead of becoming zombies — but then the status is discarded and wait() can retrieve nothing. This is peculiar: although SIGCHLD is ignored by default anyway, explicitly setting SIG_IGN causes different behaviour — SIGCHLD is the only signal with this quirk. The sigaction() SA_NOCLDWAIT flag gives a similar effect.156

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

“wait() cho tôi chờ đúng đứa con tôi muốn” “wait() lets me wait for the specific child I want”

Ngộ nhận: gọi wait() là chờ được một con cụ thể.

Thực tế: wait() chỉ chờ con kế tiếp bất kỳ. Muốn chờ đúng một con, phải dùng waitpid() với pid dương; wait() cũng luôn chặn và không báo con bị dừng.12

Myth: calling wait() lets you wait for a particular child.

Reality: wait() waits for any next child only. To wait for a specific one, use waitpid() with a positive pid; wait() also always blocks and never reports a stopped child.12

“Một zombie vẫn đang chạy, cứ SIGKILL là xong” “A zombie is still running; just SIGKILL it”

Ngộ nhận: zombie là tiến trình còn sống và có thể bị giết bằng SIGKILL.

Thực tế: zombie gần như đã "chết hẳn" — chỉ còn một mục trong bảng tiến trình; không tín hiệu nào, kể cả SIGKILL, xóa được nó. Chỉ có wait() từ cha (hoặc cha thoát để init thu) mới dọn được.1

Myth: a zombie is a live process you can kill with SIGKILL.

Reality: a zombie is nearly fully dead — just a process-table entry; no signal, not even SIGKILL, removes it. Only a wait() by the parent (or the parent exiting so init reaps it) clears it.1

“Mỗi con chết là có đúng một SIGCHLD” “Each dead child gives exactly one SIGCHLD”

Ngộ nhận: cha nhận một SIGCHLD cho mỗi con kết thúc, nên dọn một con mỗi lần là đủ.

Thực tế: SIGCHLD là tín hiệu chuẩn, không xếp hàng; nhiều con chết dồn khi handler đang chạy chỉ cho một SIGCHLD. Phải lặp waitpid(..., WNOHANG) tới khi hết.15

Myth: the parent gets one SIGCHLD per terminated child, so reaping one child each time suffices.

Reality: SIGCHLD is a standard signal and is not queued; several children dying while the handler runs yield only one SIGCHLD. You must loop waitpid(..., WNOHANG) until none remain.15

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm cách giám sát tiến trình con Năm câu hỏi kiểm tra nhanh Five questions to check you grasped monitoring child processes

Trả lời được những câu này nghĩa là bạn đã nắm cách cha theo dõi con, đọc trạng thái, và dọn con đã chết — nền cho các chương về thực thi chương trình và tín hiệu.

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

Answering these means you grasp how a parent tracks its children, reads their status, and reaps the dead — the groundwork for the chapters on program execution and signals.

  1. Nêu ba hạn chế của wait() mà waitpid() khắc phục. Giá trị pid bằng 0, −1, và nhỏ hơn −1 chọn những con nào?

    Gợi ý sâu hơn: với WNOHANG, waitpid() trả về gì khi chưa con nào đổi trạng thái, và khác với trường hợp ECHILD ra sao?

  2. Vì sao phải dùng macro (WIFEXITED…) để đọc giá trị trạng thái thay vì tự giải mã bit? Macro nhận con trỏ hay số nguyên?

    Gợi ý sâu hơn: vì sao mã kết thúc qua WEXITSTATUS luôn nằm trong 0–255?

  3. Phân biệt WIFEXITED, WIFSIGNALED, WIFSTOPPED và WIFCONTINUED. Mỗi macro đi kèm macro phụ nào để lấy chi tiết?

    Gợi ý sâu hơn: waitid() cho khả năng gì mà waitpid() không có, và cấu trúc siginfo_t mang những trường nào?

  4. Zombie là gì và vì sao nó phát sinh? Vì sao không thể giết một zombie bằng SIGKILL, và một con mồ côi được ai nhận nuôi?

    Gợi ý sâu hơn: điều gì xảy ra với zombie khi tiến trình cha quên wait() nhưng vẫn sống rất lâu?

  5. Vì sao một handler SIGCHLD chỉ gọi wait() một lần có thể bỏ sót con đã chết? Viết thân handler đúng.

    Gợi ý sâu hơn: vì sao nên lưu và khôi phục errno trong handler, và vì sao nên cài handler trước khi tạo con?

  1. Vì sao waitpid() tiện hơn wait()?

  2. Vì sao nên dùng macro để đọc kết quả con để lại?

  3. Con có thể "xong", "bị giết", "bị dừng", "chạy lại" — macro nào cho biết từng cái?

  4. Con "ma" (zombie) là gì, và ai dọn con mồ côi?

  5. Vì sao trong chuông SIGCHLD phải dọn theo vòng lặp?

  1. Name three limits of wait() that waitpid() removes. Which children do pid values of 0, −1, and less than −1 select?

  2. Why use the macros (WIFEXITED…) to read the status value rather than decode bits yourself? Do the macros take a pointer or an integer?

  3. Distinguish WIFEXITED, WIFSIGNALED, WIFSTOPPED, and WIFCONTINUED. Which companion macro gives each one's detail?

  4. What is a zombie and why does it arise? Why can't you kill a zombie with SIGKILL, and who adopts an orphan?

  5. Why can a SIGCHLD handler that calls wait() only once miss dead children? Write the correct handler body.

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

Giám sát tiến trình con là vừa lắng nghe vừa dọn dẹp: cha dùng họ wait() hoặc tín hiệu SIGCHLD để biết khi nào và vì sao con đổi trạng thái, đọc kết quả bằng macro chuẩn, và thu dọn mọi con đã chết để không zombie nào làm tắc hệ thống. Nắm được điều này, bạn đã sẵn sàng cho câu hỏi kế — làm sao một con thay da đổi thịt thành một chương trình mới qua exec().1

Giám sát con là vừa nghe ngóng vừa dọn dẹp: biết khi nào con xong, đọc đúng kết quả, và dọn xác con để không có con ma nào làm nghẽn máy. Hiểu rồi thì đi tiếp sang chuyện con biến thành một chương trình mới. Nhớ nhé: nghe tin con và dọn con là một cặp không thể tách rời.

Monitoring children is listening and cleaning up at once: the parent uses the wait() family or the SIGCHLD signal to learn when and why a child changes state, reads the result with the standard macros, and reaps every dead child so no zombie clogs the system. With this in hand you are ready for the next question — how a child transforms into a new program via exec().1