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 */
-
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.1wait()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.1Bạ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ặpwhile (wait(NULL) != -1)là đủ, và kết thúc gọn khi gặpECHILD.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 loopwhile (wait(NULL) != -1)suffices, ending cleanly onECHILD.1 -
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ốpidchọn con cần chờ:pid > 0là một con cụ thể;pid == 0là con bất kỳ cùng nhóm tiến trình với cha;pid < -1là con bất kỳ trong nhóm tiến trình có mã bằng trị tuyệt đối củapid; cònpid == -1là con bất kỳ — khi đówaitpid(-1, &status, 0)tương đương hệtwait(&status).12 Trườngoptionslà mặt nạ bit gồmWUNTRACED(cũng báo khi con bị dừng),WCONTINUED(báo khi con đượcSIGCONTcho 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).12waitpid()là bản "hỏi khéo" củawait(): 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. Thepidargument selects the child:pid > 0is one specific child;pid == 0is any child in the caller's own process group;pid < -1is any child in the process group whose ID equals the absolute value ofpid; andpid == -1is any child — sowaitpid(-1, &status, 0)is exactly equivalent towait(&status).12 Theoptionsbit mask holdsWUNTRACED(also report a stopped child),WCONTINUED(report a child resumed bySIGCONT; since Linux 2.6.10), andWNOHANG(poll, returning 0 at once if no child has yet changed state).12Bạ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ọiwaitpid(-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 — callwaitpid(-1, &status, WNOHANG)periodically to "poll" without blocking.1Tên WUNTRACED có gốc lịch sửThe name WUNTRACED is historical
SUSv3 ghi rằng cái tên
WUNTRACEDlà 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ộtSIGCHLDđược gửi cho cha.1SUSv3 notes that the nameWUNTRACEDis a BSD-era artifact, where a process could be stopped in two ways: by being traced withptrace(), or by being stopped while not traced. For a child being traced withptrace(), delivering any signal (other thanSIGKILL) stops the child and sends aSIGCHLDto the parent.1WNOHANG trả về 0, không phải −1WNOHANG returns 0, not −1
Khi dùng
WNOHANGmà chưa con nào (khớp vớipid) đổ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ớierrnolàECHILD.12WithWNOHANGset 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 witherrnoset toECHILD.12 -
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ặpidtype/idchọ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úcsiginfo_t:si_codecho biết loại sự kiện (CLD_EXITED,CLD_KILLED,CLD_STOPPED,CLD_CONTINUED), cùngsi_pid,si_signo(luôn làSIGCHLD),si_statusvàsi_uid.13waitid()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 powerwaitpid()lacks.12 It derives from System V, is now specified by SUSv3, and was added to Linux in kernel 2.6.9.1 Theidtype/idpair selects the child (P_ALL,P_PID,P_PGID). The key difference: you select the precise event type by ORingWEXITED,WSTOPPED,WCONTINUED— rather than the bundled behaviour ofwaitpid(). On success it fills asiginfo_t:si_codegives the event type (CLD_EXITED,CLD_KILLED,CLD_STOPPED,CLD_CONTINUED), along withsi_pid,si_signo(alwaysSIGCHLD),si_status, andsi_uid.13Bạ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ớiWNOWAIT, 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. TheWNOWAITflag: normally waiting on a child consumes that event; withWNOWAITthe child stays waitable, so you can wait for it again later to retrieve the same information.13Bẫ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 tronginfo), 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úcsiginfo_tvề 0 trước khi gọi, rồi kiểm trainfo.si_pidsau đó.13WithWNOHANG, a 0 return fromwaitid()can mean two things: either a child changed state (details ininfo), or no child changed state. The portable way to tell them apart is tomemset()thesiginfo_tto zero before the call, then testinfo.si_pidafterward.13Không có "0 nghĩa là nhóm của tôi"No "0 means my group"
Khác
waitpid(), không thể đặtidbằng 0 để nói "nhóm tiến trình của tôi"; vớiP_PGIDphải ghi rõ mã nhóm, lấy từgetpgrp().1Unlikewaitpid(), you cannot setidto 0 to mean "my own process group"; withP_PGIDyou must give the group ID explicitly, fromgetpgrp().1 -
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úcstruct rusage— gồm thời gian CPU đã dùng và các số liệu quản lý bộ nhớ.14 Bỏ qua phầnrusage,wait3()tương đươngwaitpid(-1, &status, options)(chờ con bất kỳ), cònwait4()tương đươngwaitpid(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.1Hai 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()andwait4()calls do a task similar towaitpid(), the principal difference being that they also return resource-usage information about the terminated child in astruct rusage— including CPU time used and memory-management statistics.14 Excluding therusageargument,wait3()is equivalent towaitpid(-1, &status, options)(any child), andwait4()towaitpid(pid, &status, options)(a selectable child). Their names reflect the number of arguments each takes.1Bạn gặp nó ở đâuWhere you meet it Hiếm khi cần: Kerrisk nói sách thường tránh dùngwait3()/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 avoidswait3()/wait4()because the extra figures are seldom required, and their lack of standardization limits portability.1Nguồ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 specifywait3()but marked it LEGACY.1Thu 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 theWUNTRACEDflag 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 */ ;
-
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ặcexit()). 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 chostatuslà0x1700, tách ra thành hai byte (23, 0).1Nếu con tự chạy hết việc rồi dừng,
WIFEXITEDbáo "đúng, con tự xong", vàWEXITSTATUScho 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()orexit()). ThenWEXITSTATUS(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 astatusof0x1700, splitting into the two bytes (23, 0).1Bạ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àWEXITSTATUStrả 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 valueWEXITSTATUSreturns to the parent shell.1 -
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ửiSIGABRT(số 6) cho con khi core dump đang bị tắt chostatus0x0006(0, 6); khi bật core dump,statuslà0x0086(0, 134) — bit 0x80 báo đã có core dump.1Nế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,
WIFSIGNALEDbáo điều đó, vàWTERMSIGcho 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. ThenWTERMSIG(status)gives the number of that signal, andWCOREDUMP(status)is true if the child also produced a core dump file.12 The book's example: sendingSIGABRT(signal 6) to a child with core dumps disabled gives astatusof0x0006(0, 6); with core dumps enabled,statusis0x0086(0, 134) — the 0x80 bit flags that a core was dumped.1Bạ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 traWIFSIGNALED/WTERMSIGở cha thường lộ ra nó bịSIGSEGV,SIGKILLhaySIGABRT.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 — checkingWIFSIGNALED/WTERMSIGin the parent often reveals it was hit bySIGSEGV,SIGKILL, orSIGABRT.1WCOREDUMP 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 thatWCOREDUMP()is not specified by SUSv3, though present on most UNIX systems; following the book, portable programs should guard it with#ifdef WCOREDUMP.12An toàn tín hiệu khi in trạng tháiSignal safety when printing status
Hàm
printWaitStatus()(Listing 26-2) dùngprintf(), 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.1TheprintWaitStatus()helper (Listing 26-2) usesprintf(), which is not async-signal-safe; so it should not be called from inside aSIGCHLDhandler.1 -
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ằngSIGCONT— macro này có từ Linux 2.6.10.12 Ví dụ trong sách: gửiSIGSTOP(số 19) cho con chostatus0x137f(19, 127) — byte thấp0x7flà dấu hiệu "bị dừng"; gửiSIGCONTchostatus0xffff(255, 255).1Con 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; thenWSTOPSIG(status)gives the number of that stopping signal.WIFCONTINUED(status)is true if the child was resumed bySIGCONT— this macro exists since Linux 2.6.10.12 The book's example: sendingSIGSTOP(signal 19) gives astatusof0x137f(19, 127) — the low byte0x7fmarks "stopped"; sendingSIGCONTgives0xffff(255, 255).1Bạn gặp nó ở đâuWhere you meet it Để nhận các sự kiện dừng/tiếp tục quawaitpid(), phải truyềnWUNTRACEDvà/hoặcWCONTINUEDtrongoptions— 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 throughwaitpid(), you must passWUNTRACEDand/orWCONTINUEDinoptions— 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) */
-
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ọigetppid()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ínhinittạo ra).1initcũng gánh việcwait()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.1Cha 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 agetppid()returning 1 can tell you whether a child's real parent is still alive (assuming the child was not created byinititself).1initalso takes on the job ofwait()-ing for these orphans when they die, so they never linger as zombies.1Bạ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 đượcinitnhậ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 deliberatelyfork()s and lets the parent exit, so the child becomes an orphan adopted byinitand runs in the background, detached from the terminal.1PR_SET_PDEATHSIGPR_SET_PDEATHSIG
Với thao tác
PR_SET_PDEATHSIGcủa lời gọiprctl()(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 thePR_SET_PDEATHSIGoperation of the Linux-specificprctl()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 -
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ộiwait()lấy trạng thái.1 Khi chawait(), nhân xóa zombie. Nếu cha chết mà chưawait(),initnhậ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ệnhps(1)hiển thị zombie bằng chuỗi<defunct>.1Con "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
psghi 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 towait()for its status.1 When the parentwait()s, the kernel removes the zombie. If the parent dies without waiting,initadopts 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. Theps(1)command shows a zombie with the string<defunct>.1Bạ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ảiwait()đều đặn, nếu không zombie tích tụ và cuối cùng khôngfork()đượ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 mustwait()regularly, or zombies pile up until you can no longerfork().1Cá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
initnhậ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 byinit.1Minh họa: make_zombieDemo: make_zombie
Listing 26-4 trong sách tạo một zombie rồi gửi
SIGKILLcho nó; ảnh chụppstrước và sau cho thấy tiến trình vẫn ở trạng thái<defunct>— bằng chứng rằngSIGKILLkhông giết được zombie.1The book's Listing 26-4 creates a zombie and sends itSIGKILL; thepssnapshots before and after show it still in the<defunct>state — proof thatSIGKILLcannot kill a zombie.1 -
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
SIGCHLDrồi gọiwait()bên trong là cách thu zombie gọn gàng. Nhưng có một cái bẫy:SIGCHLDlà tín hiệu chuẩn, mà tín hiệu chuẩn không xếp hàng. Khi handler đang chạy,SIGCHLDbị 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ỉ sinhSIGCHLDmột lần. Một handler chỉ gọiwait()đú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.1Chuông
SIGCHLDcó 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
SIGCHLDhandler and callingwait()inside it is a tidy way to reap zombies. But there is a trap:SIGCHLDis a standard signal, and standard signals are not queued. While the handler runs,SIGCHLDis temporarily blocked; if two or three children die in quick succession then, the kernel generatesSIGCHLDonly once. A handler that callswait()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.1Bạn gặp nó ở đâuWhere you meet it Reentrancy: vìwaitpid()trong handler có thể đổierrno, nên lưuerrnovào biến cục bộ ở đầu handler và khôi phục trước khi trả về, tránh phá giá trịerrnomà 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: becausewaitpid()in the handler may changeerrno, saveerrnointo a local at the handler's start and restore it before returning, so you do not corrupt theerrnothe main program relies on.1Cà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
SIGCHLDcho 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 aSIGCHLDfor 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.15Bỏ qua con đã chết: SIG_IGNIgnoring dead children: SIG_IGN
Đặt tường minh disposition của
SIGCHLDthànhSIG_IGNkhiế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 địnhSIGCHLDvốn đã bị bỏ qua, việc đặt tường minhSIG_IGNlại cho hành vi khác —SIGCHLDlà tín hiệu duy nhất có nét riêng này. CờSA_NOCLDWAITcủasigaction()cho hiệu ứng tương tự.156Explicitly setting the disposition ofSIGCHLDtoSIG_IGNmakes terminating children be removed at once instead of becoming zombies — but then the status is discarded andwait()can retrieve nothing. This is peculiar: althoughSIGCHLDis ignored by default anyway, explicitly settingSIG_IGNcauses different behaviour —SIGCHLDis the only signal with this quirk. Thesigaction()SA_NOCLDWAITflag 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.
-
Nêu ba hạn chế của
wait()màwaitpid()khắc phục. Giá trịpidbằ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ợpECHILDra sao? -
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
WEXITSTATUSluôn nằm trong 0–255? -
Phân biệt
WIFEXITED,WIFSIGNALED,WIFSTOPPEDvà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úcsiginfo_tmang những trường nào? -
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? -
Vì sao một handler
SIGCHLDchỉ gọiwait()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
errnotrong handler, và vì sao nên cài handler trước khi tạo con?
Vì sao
waitpid()tiện hơnwait()?Vì sao nên dùng macro để đọc kết quả con để lại?
Con có thể "xong", "bị giết", "bị dừng", "chạy lại" — macro nào cho biết từng cái?
Con "ma" (zombie) là gì, và ai dọn con mồ côi?
Vì sao trong chuông
SIGCHLDphải dọn theo vòng lặp?
Name three limits of
wait()thatwaitpid()removes. Which children dopidvalues of 0, −1, and less than −1 select?Why use the macros (
WIFEXITED…) to read the status value rather than decode bits yourself? Do the macros take a pointer or an integer?Distinguish
WIFEXITED,WIFSIGNALED,WIFSTOPPED, andWIFCONTINUED. Which companion macro gives each one's detail?What is a zombie and why does it arise? Why can't you kill a zombie with
SIGKILL, and who adopts an orphan?Why can a
SIGCHLDhandler that callswait()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ệuSIGCHLDđể 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 quaexec().1Giá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 theSIGCHLDsignal 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 viaexec().1