Chương 21 · Trình xử lý tín hiệu Chapter 21 · Signal Handlers
Viết một trình xử lý tín hiệu cho an toàn — khi nó cắt ngang chương trình bất kỳ lúc nào Viết trình xử lý tín hiệu sao cho an toàn, vì nó xen vào đúng lúc chương trình đang dở tay Writing a signal handler safely — when it can cut in on your program at any moment
Nếu Chương 20 trả lời "có những tín hiệu nào và làm sao bắt được chúng", thì Chương 21 bước sang câu hỏi khó hơn: bên trong một trình xử lý tín hiệu (signal handler), ta được phép làm gì? Kerrisk mổ xẻ thiết kế của handler: tính khả tái nhập (reentrancy) và khái niệm hàm an toàn với tín hiệu bất đồng bộ (async-signal-safe); cách chia sẻ biến toàn cục an toàn qua kiểu volatile sig_atomic_t; các lối thoát khỏi handler — nhảy phi cục bộ bằng sigsetjmp()/siglongjmp(), kết thúc bằng _exit() hay abort(); chạy handler trên một ngăn xếp phụ với sigaltstack(); cờ SA_SIGINFO cùng cấu trúc siginfo_t; và chuyện system call bị ngắt rồi được khởi động lại (SA_RESTART).1
Hãy tưởng tượng bạn đang viết dở một câu thì có người gõ cửa báo tin khẩn. Bạn phải xử lý tin đó thật nhanh, thật gọn, rồi quay lại viết tiếp đúng chỗ đang dở. Trình xử lý tín hiệu cũng vậy: nó xen vào giữa lúc chương trình đang làm việc khác, nên phải cực kỳ cẩn thận — chỉ làm những việc an toàn, rồi trả lại quyền điều khiển. Chương này dạy cách làm điều đó. Nhớ nhé: handler là người gõ cửa báo tin khẩn, phải làm nhanh và an toàn rồi rút lui.
If Chapter 20 answered "which signals exist and how to catch them," Chapter 21 moves to the harder question: inside a signal handler, what are we actually allowed to do? Kerrisk dissects handler design: reentrancy and the notion of async-signal-safe functions; how to share global variables safely via the volatile sig_atomic_t type; the ways out of a handler — a nonlocal goto with sigsetjmp()/siglongjmp(), terminating with _exit() or abort(); running a handler on an alternate stack with sigaltstack(); the SA_SIGINFO flag and the siginfo_t structure; and the matter of interrupted and restarted system calls (SA_RESTART).1
Cốt lõi của mọi rắc rối là một sự thật: handler chạy bất đồng bộ. Nhân có thể gọi nó ngay giữa lúc chương trình chính đang thực thi một hàm bất kỳ — kể cả một hàm thư viện đang cập nhật dở cấu trúc dữ liệu toàn cục của chính nó. Vì thế handler không được giả định bất cứ điều gì về trạng thái mà nó cắt ngang, và chỉ được đụng vào những gì đã được chứng minh là an toàn trong hoàn cảnh ấy.2At the root of every difficulty is one fact: a handler runs asynchronously. The kernel may invoke it right in the middle of the main program executing any function — including a library function that is halfway through updating its own global data structures. The handler therefore cannot assume anything about the state it interrupts, and may touch only what is provably safe under those conditions.2
Luận đề của chương: vì một handler cắt ngang chương trình chính tại một điểm không đoán trước được, thiết kế đúng của nó bị chi phối bởi một kỷ luật duy nhất — làm càng ít càng tốt, và chỉ làm những việc an toàn khi mọi thứ khác đang đóng băng giữa chừng. Mọi kỹ thuật trong chương (async-signal-safe, sig_atomic_t, siglongjmp(), ngăn xếp phụ, SA_RESTART) đều là hệ quả của kỷ luật đó.1
Ý chính: handler xen vào giữa chừng, nên nguyên tắc vàng là làm thật ít và thật an toàn, rồi quay về ngay. Mọi mẹo trong chương đều phục vụ nguyên tắc đó. Nhớ nhé: trong handler, ít mà an toàn luôn thắng nhiều mà liều.
The chapter's thesis: because a handler interrupts the main program at an unpredictable point, its correct design is governed by a single discipline — do as little as possible, and do only what is safe while everything else is frozen mid-step. Every technique in the chapter (async-signal-safe functions, sig_atomic_t, siglongjmp(), an alternate stack, SA_RESTART) follows from that discipline.1
Cụm khái niệm 01 Concept cluster 01
Vì handler cắt ngang chương trình, nó chỉ được gọi hàm an toàn và chia sẻ biến thật cẩn thận Handler xen vào giữa chừng, nên chỉ làm việc an toàn và chia sẻ biến đúng cách Because a handler cuts in, it may call only safe functions and must share variables carefully
Kerrisk nêu hai thiết kế handler thông dụng. Thứ nhất: handler đặt một cờ toàn cục rồi trả về; chương trình chính định kỳ kiểm tra cờ đó và làm phần việc nặng. Thứ hai: handler dọn dẹp rồi kết thúc tiến trình, hoặc dùng nhảy phi cục bộ để quay về một điểm đã định trong chương trình chính.1 Dù chọn kiểu nào, một hiểm họa luôn rình rập: nếu handler gọi một hàm không khả tái nhập ngay lúc chương trình chính cũng đang ở trong hàm đó, dữ liệu có thể hỏng.12
Có hai cách viết handler cho gọn. Cách một: handler chỉ giơ một lá cờ báo "có việc" rồi lui ra; chương trình chính thấy cờ thì tự lo. Cách hai: handler dọn dẹp rồi tắt chương trình, hoặc nhảy về một chỗ an toàn đã hẹn trước. Nhưng coi chừng: nếu handler gọi một hàm mà chương trình chính đang gọi dở hàm đó, mọi thứ có thể rối tung. Nhớ nhé: handler nên giơ cờ rồi lui, đừng ôm việc nặng.
Kerrisk presents two common handler designs. First: the handler sets a global flag and returns; the main program periodically checks the flag and does the heavy work. Second: the handler performs cleanup and terminates the process, or uses a nonlocal goto to return to a predetermined point in the main program.1 Whichever you choose, one hazard always lurks: if the handler calls a non-reentrant function just as the main program is also inside that function, data can be corrupted.12
Định nghĩa. Một hàm là khả tái nhập (reentrant) nếu nó có thể được nhiều luồng thực thi gọi đồng thời một cách an toàn. Một hàm an toàn với tín hiệu bất đồng bộ (async-signal-safe) là hàm mà việc gọi nó từ trong một handler là an toàn — hoặc vì nó khả tái nhập, hoặc vì nó không thể bị tín hiệu làm gián đoạn. SUSv3 và trang signal-safety(7) liệt kê tập hàm được bảo đảm an toàn này.28
Nói đơn giản. Một hàm "an toàn để gọi trong handler" là hàm không bị rối khi bị gọi chồng lên chính nó. Chỉ những hàm nằm trong danh sách được bảo đảm mới nên gọi trong handler. Nhớ nhé: trong handler, chỉ gọi hàm nằm trong danh sách an toàn.
Definition. A function is reentrant if it can safely be executed simultaneously by multiple threads of execution. A function is async-signal-safe if calling it from within a handler is safe — either because it is reentrant, or because it cannot be interrupted by a signal. SUSv3 and the signal-safety(7) page list this guaranteed-safe set.28
/* Thiết kế 1: giơ cờ rồi về */
volatile sig_atomic_t gotSig = 0;
void handler(int sig){ gotSig = 1; }
/* main: ... while (!gotSig) pause(); */
/* Thiết kế 2: dọn dẹp rồi _exit() */
void handler(int sig){ cleanup(); _exit(1); }
Cách 1:
handler --> gotSig = 1;
main --> thấy cờ thì xử lý
Cách 2:
handler --> dọn dẹp rồi thoát
/* Design 1: set a flag and return */
volatile sig_atomic_t gotSig = 0;
void handler(int sig){ gotSig = 1; }
/* main: ... while (!gotSig) pause(); */
/* Design 2: clean up, then _exit() */
void handler(int sig){ cleanup(); _exit(1); }
-
reentrancy
Hàm không khả tái nhập: dùng dữ liệu toàn cục hoặc bộ nhớ tĩnh Vì sao có hàm không an toàn khi bị gọi chồng Non-reentrant functions: those using global or static data
Một hàm là không khả tái nhập khi nó dùng cấu trúc dữ liệu toàn cục hay tĩnh. Ví dụ kinh điển của tác giả: họ hàm
malloc()dùng một danh sách liên kết toàn cục của các khối nhớ rỗi; nếu handler gọimalloc()ngay lúc chương trình chính đang cập nhật dở danh sách đó, đống (heap) có thể hỏng.1 Tương tự, các hàm trả về con trỏ tới bộ nhớ cấp phát tĩnh — nhưstrtok(),getpwnam(),crypt()— cũng không khả tái nhập.12Hàm "không khả tái nhập" là hàm giữ một cuốn sổ chung và sửa sổ đó từng bước. Nếu đang sửa dở mà bị gọi lại lần nữa, cuốn sổ sẽ loạn.
malloc()là ví dụ: nó quản lý một danh sách bộ nhớ rỗi dùng chung. Nhớ nhé: hàm dùng sổ chung thì không được gọi chồng lên nhau.A function is non-reentrant when it uses global or static data structures. The author's classic example: the
malloc()family uses a global linked list of free blocks; if a handler callsmalloc()just as the main program is halfway through updating that list, the heap can be corrupted.1 Likewise, functions returning a pointer to statically allocated memory — such asstrtok(),getpwnam(),crypt()— are non-reentrant.12Bạn gặp nó ở đâuWhere you meet it Mỗi khi thấy một handler gọiprintf()để gỡ lỗi — đó là một lỗi tiềm ẩn, vì thư viện stdio không khả tái nhập.2 Khi thấy ai đó gọiprintf()bên trong handler để in thử — đó là thói quen nguy hiểm. Whenever you see a handler callprintf()to debug — that is a latent bug, because the stdio library is non-reentrant.2Vì sao stdio nguy hiểmWhy stdio is dangerous
Mọi hàm thư viện stdio đều không khả tái nhập: chúng giữ một vùng đệm tĩnh cùng các bộ đếm và con trỏ ghi vị trí. Nếu chương trình chính đang ở giữa một lời
printf()khi handler cũng gọiprintf(), lời thứ hai sẽ thao tác trên dữ liệu nửa vời, cho kết quả không đoán trước.2All stdio library functions are non-reentrant: they maintain a static buffer plus counters and pointers recording the position. If the main program is mid-printf()when a handler also callsprintf(), the second call operates on half-updated data, with unpredictable results.2Phiên bản "_r"The "_r" versions
Nhiều hàm không khả tái nhập có phiên bản khả tái nhập hậu tố
_r(ví dụstrtok_r(),getpwnam_r()): chúng nhận thêm một đối số là vùng đệm do người gọi cấp, nên không dựa vào bộ nhớ tĩnh dùng chung.1Many non-reentrant functions have reentrant counterparts with a_rsuffix (for examplestrtok_r(),getpwnam_r()): they take an extra caller-supplied buffer argument, so they do not rely on shared static memory.1 -
async-signal-safe
Danh sách hàm an toàn — và nghĩa vụ lưu lại errno Chỉ gọi hàm trong danh sách an toàn, và nhớ giữ errno The safe-function list — and the duty to save errno
Quy tắc thực hành: trong một handler, chỉ gọi những hàm mà SUSv3 (và
signal-safety(7)) bảo đảm là async-signal-safe — ví dụwrite(),_exit(),sigaction(),kill()— và tránh mọi thứ khác.2 Có một bẫy tinh vi: ngay cả khi một handler chỉ gọi hàm an toàn, nó vẫn có thể cập nhậterrno; nếu chương trình chính bị ngắt ngay sau một system call và chưa kịp đọcerrno, handler sẽ ghi đè mất giá trị đó. Vì vậy handler nên lưuerrnokhi vào và khôi phục khi ra.12Trong handler, chỉ dùng những hàm nằm trong "danh sách an toàn" đã được bảo đảm, như
write()hay_exit(). Thêm một mẹo nhỏ: nhiều hàm làm thay đổi biến báo lỗierrno; handler nên chép lạierrnolúc vào rồi trả về như cũ lúc ra, kẻo làm hỏng việc kiểm tra lỗi của chương trình chính. Nhớ nhé: vào handler thì cất errno, ra thì trả lại y nguyên.The practical rule: inside a handler, call only functions that SUSv3 (and
signal-safety(7)) guarantee to be async-signal-safe — for examplewrite(),_exit(),sigaction(),kill()— and avoid everything else.2 There is a subtle trap: even when a handler calls only safe functions, it may still updateerrno; if the main program was interrupted right after a system call and had not yet readerrno, the handler would clobber that value. So a handler should saveerrnoon entry and restore it on exit.12Bạn gặp nó ở đâuWhere you meet it Khi cần in gì đó từ handler: dùngwrite(STDOUT_FILENO, ...)thay choprintf(), vìwrite()nằm trong danh sách an toàn cònprintf()thì không.2 Muốn in trong handler thì dùngwrite()thay choprintf()— một cái an toàn, một cái không. When you must print from a handler: usewrite(STDOUT_FILENO, ...)instead ofprintf(), sincewrite()is on the safe list andprintf()is not.2POSIX bảo đảm điều gìWhat POSIX guarantees
POSIX.1 chỉ bắt buộc một tập hàm nhất định phải async-signal-safe; hiện thực có thể làm thêm hàm khác an toàn, nhưng không bắt buộc, nên chương trình khả chuyển không được trông cậy vào các hàm ngoài danh sách.29POSIX.1 only requires a specific set of functions to be async-signal-safe; an implementation may make others safe too, but is not obliged to, so portable programs must not rely on functions outside the list.29
Chặn tín hiệu thay vì tránh néBlock instead of avoid
Nếu chương trình chính buộc phải gọi một hàm không an toàn, nó có thể chặn tạm thời tín hiệu quanh lời gọi đó (bằng mặt nạ tín hiệu) để handler không chen vào giữa chừng — một cách thay thế cho việc cấm tiệt.1If the main program must call an unsafe function, it can temporarily block the signal around that call (via the signal mask) so the handler cannot cut in mid-way — an alternative to forbidding it outright.1
-
volatile sig_atomic_t
Cách duy nhất chắc chắn để chia sẻ một biến với handler Kiểu biến chuyên để handler và main cùng dùng The one sure way to share a variable with a handler
Khi handler và chương trình chính cùng dùng một biến toàn cục (thiết kế "giơ cờ"), phải bảo đảm việc đọc/ghi biến đó là nguyên tử (atomic). Chuẩn C quy định kiểu
sig_atomic_t— một kiểu nguyên mà việc đọc và ghi luôn diễn ra nguyên tử — và yêu cầu khai nó làvolatileđể trình biên dịch không giữ bản sao trong thanh ghi mà luôn đọc lại từ bộ nhớ.19 Lưu ý: chỉ việc đọc và ghi là nguyên tử; các phép như++hay--trênsig_atomic_tkhông được bảo đảm nguyên tử.1Khi handler giơ một lá cờ cho chương trình chính đọc, lá cờ ấy phải là loại mà thao tác đọc/ghi diễn ra "một phát ăn ngay", không bị cắt nửa chừng. Kiểu
sig_atomic_tlàm đúng việc đó, và ta thêm chữvolatileđể trình biên dịch luôn nhìn vào giá trị mới nhất. Nhưng đừng dùng phép tăng++trên nó. Nhớ nhé: cờ chia sẻ với handler phải là volatile sig_atomic_t, và chỉ gán 0 hay 1 thôi.When a handler and the main program share a global variable (the "set-a-flag" design), reads and writes of that variable must be atomic. The C standard specifies the
sig_atomic_ttype — an integer type whose reads and writes are always atomic — and requires declaring itvolatileso the compiler does not keep a copy in a register but always re-reads from memory.19 Note: only reads and writes are atomic; operations like++or--on asig_atomic_tare not guaranteed atomic.1Bạn gặp nó ở đâuWhere you meet it Mỗi biến cờ mà một handler đặt và chương trình chính đọc — khai báo nó đúng kiểuvolatile sig_atomic_tlà đã tránh được một lớp lỗi khó tái hiện.1 Mọi lá cờ mà handler đặt cho main đọc — hãy khai đúngvolatile sig_atomic_t. Every flag a handler sets and the main program reads — declaring it exactlyvolatile sig_atomic_theads off a class of hard-to-reproduce bugs.1
Cụm khái niệm 02 Concept cluster 02
Ngoài việc trả về bình thường, một handler còn ba lối thoát khác Những cách khác để kết thúc một handler ngoài việc trả về Besides returning normally, a handler has three other exits
Cách thông thường nhất để kết thúc một handler là trả về, khi đó chương trình chính chạy tiếp từ điểm bị ngắt. Nhưng tác giả nêu ba lối khác: dùng _exit() để kết thúc tiến trình; dùng abort() để kết thúc kèm ảnh đổ (core dump); và thực hiện một nhảy phi cục bộ bằng cặp sigsetjmp()/siglongjmp() để quay về một điểm đã định trong chương trình chính.16
Thường thì handler xong việc sẽ trả về, chương trình chạy tiếp chỗ cũ. Nhưng còn ba cách khác: tắt hẳn chương trình bằng _exit(); tắt kèm ghi lại hiện trường bằng abort(); hoặc "nhảy" về một chỗ an toàn đã hẹn trước. Nhớ nhé: handler có thể về chỗ cũ, tắt máy, hoặc nhảy về điểm hẹn.
The most common way to end a handler is to return, whereupon the main program resumes from the point it was interrupted. But the author lists three others: use _exit() to terminate the process; use abort() to terminate with a core dump; and perform a nonlocal goto with the sigsetjmp()/siglongjmp() pair to return to a predetermined point in the main program.16
Định nghĩa. Dùng longjmp() thường để thoát khỏi handler có một vấn đề về mặt nạ tín hiệu (signal mask): khi handler được gọi, nhân chặn tạm tín hiệu đang xử lý; nếu ta thoát bằng longjmp() thì mặt nạ có thể không được khôi phục, khiến tín hiệu bị chặn mãi. Cặp sigsetjmp(env, savesigs)/siglongjmp() giải quyết việc này: nếu savesigs khác 0, mặt nạ tín hiệu được lưu vào env và sẽ được siglongjmp() khôi phục.14
Nói đơn giản. Khi handler đang chạy, tín hiệu vừa tới bị "khóa" lại. Nếu bạn nhảy ra bằng longjmp() thường, cái khóa đó có thể không được mở lại — tín hiệu kẹt luôn. Cặp sigsetjmp/siglongjmp biết cách lưu và mở lại cái khóa ấy. Nhớ nhé: nhảy ra khỏi handler thì dùng siglongjmp, đừng dùng longjmp.
Definition. Using an ordinary longjmp() to leave a handler has a signal mask problem: when the handler is invoked, the kernel temporarily blocks the signal being handled; if we exit via longjmp(), the mask may not be restored, leaving the signal blocked forever. The sigsetjmp(env, savesigs)/siglongjmp() pair solves this: if savesigs is nonzero, the signal mask is saved in env and restored by siglongjmp().14
#include <setjmp.h>
sigjmp_buf env;
if (sigsetjmp(env, 1) == 0) {
/* lần đầu: cài handler, chạy... */
} else {
/* vừa siglongjmp về, mask đã khôi phục */
}
/* trong handler: siglongjmp(env, 1); */
if (sigsetjmp(env, 1) == 0) {
/* lần đầu */
} else {
/* vừa nhảy về */
}
/* handler: siglongjmp(env, 1); */
#include <setjmp.h>
sigjmp_buf env;
if (sigsetjmp(env, 1) == 0) {
/* first time: install handler, run... */
} else {
/* arrived via siglongjmp, mask restored */
}
/* in handler: siglongjmp(env, 1); */
-
sigsetjmp / siglongjmp
Nhảy phi cục bộ từ handler — nhưng phải giữ đúng mặt nạ tín hiệu Nhảy về chỗ an toàn mà không để tín hiệu bị kẹt A nonlocal goto from a handler — keeping the mask right
sigsetjmp()vàsiglongjmp()giốngsetjmp()/longjmp()của Chương 6, nhưng thêm đối sốsavesigsđể chủ động quyết định có lưu-và-khôi-phục mặt nạ tín hiệu hay không. Điều này cần thiết vìsetjmp()/longjmp()thường không quy định rõ số phận của mặt nạ — trên một số hệ nó được khôi phục, trên hệ khác thì không, khiến hành vi không khả chuyển.14Cặp này giống cặp nhảy đã học ở Chương 6, nhưng có thêm một công tắc: bật lên thì nó nhớ và trả lại đúng "danh sách tín hiệu đang bị khóa". Nhờ vậy sau khi nhảy về, chương trình không bị kẹt với một tín hiệu bị khóa quên mở. Nhớ nhé: bật savesigs = 1 để mặt nạ tín hiệu được trả lại đúng.
sigsetjmp()andsiglongjmp()are like Chapter 6'ssetjmp()/longjmp(), but add asavesigsargument to deliberately decide whether the signal mask is saved and restored. This is needed becausesetjmp()/longjmp()leave the mask's fate unspecified — on some systems it is restored, on others not, making the behavior non-portable.14Bạn gặp nó ở đâuWhere you meet it Trong một vòng lặp chính muốn "thiết lập lại" sau khi nhận một tín hiệu nào đó (ví dụSIGINT) thay vì thoát —siglongjmp()đưa luồng điều khiển về đầu vòng lặp một cách sạch sẽ.1 Khi muốn chương trình "quay về vạch xuất phát" sau khi nhận tín hiệu, thay vì thoát hẳn. In a main loop that wants to "reset" after a certain signal (saySIGINT) rather than exit —siglongjmp()cleanly returns control to the top of the loop.1Nguồn gốc của sự khác biệtOrigin of the difference
Tác giả giải thích: System V không khôi phục mặt nạ qua
longjmp(), còn BSD thì có; để tránh phụ thuộc vào nền tảng, chuẩn hóa cặpsigsetjmp()/siglongjmp()cho ta quyền kiểm soát tường minh.14The author explains: System V does not restore the mask acrosslongjmp(), whereas BSD does; to avoid platform dependence, the standardizedsigsetjmp()/siglongjmp()pair gives us explicit control.14Vẫn còn cái bẫy volatileThe volatile trap persists
Giống
longjmp(), biến cục bộ bị tối ưu vào thanh ghi có thể mang giá trị sai sausiglongjmp(); biến cần giữ đúng vẫn phải khaivolatile.14As withlongjmp(), locals optimized into registers may hold wrong values aftersiglongjmp(); variables that must survive still needvolatile.14 -
_exit() not exit()
Kết thúc từ handler: dùng _exit(), không dùng exit() Muốn thoát trong handler thì gọi _exit, đừng gọi exit Terminating from a handler: use _exit(), not exit()
Nếu handler cần kết thúc tiến trình, phải gọi
_exit()chứ không gọiexit(). Lý do:exit()xả các vùng đệm stdio và chạy các bộ xử lý thoát (exit handler) doatexit()/on_exit()đăng ký — những việc không async-signal-safe. Ngược lại,_exit()kết thúc tiến trình ngay lập tức mà không làm các việc đó, nên nằm trong danh sách an toàn.172Khi handler muốn tắt chương trình, dùng
_exit()— nó tắt ngay. Đừng dùngexit(), vìexit()còn lo dọn dẹp vùng đệm và chạy thêm vài việc không an toàn khi đang ở trong handler. Nhớ nhé: thoát trong handler thì dùng _exit(), cái "có gạch dưới".If a handler must terminate the process, it must call
_exit(), notexit(). The reason:exit()flushes stdio buffers and runs exit handlers registered withatexit()/on_exit()— none of which is async-signal-safe. By contrast,_exit()terminates the process immediately without doing any of that, so it is on the safe list.172Bạn gặp nó ở đâuWhere you meet it Trong một handler bắt tín hiệu "chết người" (fatal) để dọn dẹp gọn trước khi tắt — kết thúc bằng_exit()để không kích hoạt lại thư viện stdio từ bên trong handler.7 Trong handler bắt một tín hiệu nguy hiểm để dọn trước khi tắt — nhớ kết thúc bằng_exit(). In a handler catching a fatal signal to tidy up before exiting — finish with_exit()so you do not re-enter the stdio library from inside the handler.7 -
abort()
abort(): kết thúc kèm ảnh đổ để mổ xẻ về sau abort(): tắt chương trình và lưu lại hiện trường abort(): terminate with a core dump for later analysis
Hàm
abort()kết thúc tiến trình đang gọi và sinh một ảnh đổ lõi (core dump) để gỡ lỗi, bằng cách phát tín hiệuSIGABRTcho chính tiến trình.16 Điểm đáng nhớ:abort()được thiết kế để luôn kết thúc được tiến trình — nếuSIGABRTđang bị chặn hoặc bị bỏ qua,abort()sẽ hoàn tác cái đó để bảo đảm tín hiệu có hiệu lực.6abort()tắt chương trình nhưng trước khi tắt còn chụp lại "hiện trường" (core dump) để sau này soi xem vì sao hỏng. Nó được làm sao cho chắc chắn tắt được, dù ai đó có cố chặn tín hiệu kết thúc. Nhớ nhé: abort() tắt máy và để lại ảnh hiện trường cho ta điều tra.The
abort()function terminates the calling process and produces a core dump for debugging, by raising theSIGABRTsignal at the process itself.16 A memorable point:abort()is designed to always succeed in terminating the process — ifSIGABRTis blocked or ignored,abort()undoes that to ensure the signal takes effect.6Bạn gặp nó ở đâuWhere you meet it Mỗi khi một thư viện phát hiện một điều kiện "không thể xảy ra" (vi phạm bất biến nội bộ) và chọn dừng ngay với ảnh đổ, thay vì chạy tiếp trong trạng thái hỏng — đó là lúc gọiabort().6 Khi một thư viện gặp lỗi "lẽ ra không bao giờ xảy ra" và muốn dừng ngay để điều tra. Whenever a library detects an "impossible" condition (an internal invariant violated) and chooses to stop at once with a dump, rather than run on in a broken state — that is whenabort()is called.6
Cụm khái niệm 03 Concept cluster 03
Chọn ngăn xếp cho handler, và yêu cầu nhân cung cấp thêm thông tin về tín hiệu Cho handler một ngăn xếp riêng, và xin thêm thông tin về tín hiệu Choosing the handler's stack, and asking the kernel for more signal detail
Mặc định, handler chạy trên chính ngăn xếp của tiến trình. Nhưng nếu ngăn xếp đã tràn — khiến nhân phát SIGSEGV — thì không còn chỗ để dựng khung cho handler. sigaltstack() cho phép khai báo một ngăn xếp phụ (alternate signal stack) để handler chạy ở đó.15 Một hướng khác là lấy thêm thông tin về tín hiệu: đặt cờ SA_SIGINFO khi cài handler, và handler sẽ nhận một con trỏ tới cấu trúc siginfo_t mô tả tín hiệu.13
Bình thường handler mượn ngăn xếp của chương trình chính để làm việc. Nhưng nếu ngăn xếp đã đầy ứ thì handler không có chỗ đứng. sigaltstack() dành sẵn một ngăn xếp phụ cho những ca như vậy. Ngoài ra, nếu muốn handler biết rõ hơn "tín hiệu này từ đâu tới", bật cờ SA_SIGINFO là nhân sẽ trao kèm một tờ khai chi tiết. Nhớ nhé: sigaltstack cho handler chỗ đứng dự phòng, SA_SIGINFO cho nó biết nhiều hơn.
By default, a handler runs on the process's own stack. But if that stack has overflowed — causing the kernel to raise SIGSEGV — there is no room to build a frame for the handler. sigaltstack() lets you declare an alternate signal stack for the handler to run on.15 A separate avenue is getting more information about the signal: set the SA_SIGINFO flag when installing the handler, and it receives a pointer to a siginfo_t structure describing the signal.13
Định nghĩa. Để lập một ngăn xếp phụ cần ba bước: (1) cấp một khối nhớ làm ngăn xếp; (2) gọi sigaltstack() để báo cho nhân biết vị trí và kích thước khối đó; (3) khi cài handler bằng sigaction(), đặt cờ SA_ONSTACK.15 Hằng SIGSTKSZ là kích thước gợi ý cho một ngăn xếp phụ thông thường, còn MINSIGSTKSZ là kích thước tối thiểu cần để chạy một handler.5
Nói đơn giản. Muốn có ngăn xếp phụ, làm ba việc: xin một khối nhớ, báo cho hệ thống biết khối đó ở đâu, và khi cài handler thì bật cờ "hãy dùng ngăn xếp phụ". Hệ thống còn gợi ý sẵn một cỡ vừa đủ. Nhớ nhé: cấp nhớ, báo chỗ, bật cờ SA_ONSTACK — đủ ba bước.
Definition. Establishing an alternate stack takes three steps: (1) allocate a block of memory for the stack; (2) call sigaltstack() to tell the kernel its location and size; (3) when installing the handler with sigaction(), set the SA_ONSTACK flag.15 The SIGSTKSZ constant is a suggested size for a typical alternate stack, and MINSIGSTKSZ is the minimum needed to run a handler.5
stack_t ss;
ss.ss_sp = malloc(SIGSTKSZ);
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
sigaltstack(&ss, NULL);
/* rồi: sa.sa_flags |= SA_ONSTACK; */
/* handler kiểu SA_SIGINFO: */
void h(int sig, siginfo_t *si, void *uc);
ss.ss_sp = vùng nhớ xin được
ss.ss_size = SIGSTKSZ
sigaltstack(&ss, NULL);
sa.sa_flags |= SA_ONSTACK;
stack_t ss;
ss.ss_sp = malloc(SIGSTKSZ);
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
sigaltstack(&ss, NULL);
/* then: sa.sa_flags |= SA_ONSTACK; */
/* SA_SIGINFO-style handler: */
void h(int sig, siginfo_t *si, void *uc);
-
sigaltstack
Ngăn xếp phụ: cách duy nhất bắt được SIGSEGV do tràn ngăn xếp Khi ngăn xếp tràn, chỉ ngăn xếp phụ mới cứu được The alternate stack: the only way to catch a stack-overflow SIGSEGV
Khi ngăn xếp chính cạn kiệt, nhân phát
SIGSEGV. Nhưng để chạy handler cho tín hiệu đó, nhân lại cần chỗ trên ngăn xếp — mà ngăn xếp thì đã tràn. Vì vậy, cách duy nhất để bắt được mộtSIGSEGVdo tràn ngăn xếp là chạy handler trên một ngăn xếp phụ đã dựng sẵn bằngsigaltstack().15 Cấu trúcstack_tgồm ba trường:ss_sp(địa chỉ gốc),ss_size(số byte), vàss_flags(cờ nhưSS_ONSTACK,SS_DISABLE).5Khi ngăn xếp đầy tràn, hệ thống báo lỗi
SIGSEGV. Nhưng để xử lý lỗi đó, handler lại cần chỗ trên ngăn xếp — vốn đã hết. Thế nên phải chuẩn bị trước một ngăn xếp phụ; đó là cách duy nhất bắt được lỗi tràn này. Nhớ nhé: muốn bắt lỗi tràn ngăn xếp thì phải có ngăn xếp phụ dựng từ trước.When the main stack is exhausted, the kernel raises
SIGSEGV. But to run a handler for that signal, the kernel needs room on the stack — which has overflowed. So the only way to catch a stack-overflowSIGSEGVis to run the handler on an alternate stack set up in advance withsigaltstack().15 Thestack_tstructure has three fields:ss_sp(base address),ss_size(number of bytes), andss_flags(flags such asSS_ONSTACK,SS_DISABLE).5Bạn gặp nó ở đâuWhere you meet it Trong các bộ dò lỗi và trình thu thập "backtrace khi sập" — chúng gần như luôn lập một ngăn xếp phụ để còn chạy được handler ngay cả khi ngăn xếp chính đã hỏng.5 Trong công cụ ghi lại "vì sao chương trình sập" — chúng luôn chuẩn bị ngăn xếp phụ để còn chạy được khi ngăn xếp chính hỏng. In crash-handlers and "backtrace on crash" collectors — they almost always set up an alternate stack so the handler can still run even when the main stack is broken.5SS_ONSTACK và SS_DISABLESS_ONSTACK and SS_DISABLE
Khi truy vấn trạng thái bằng
sigaltstack(), cờSS_ONSTACKcho biết tiến trình hiện đang thực thi trên ngăn xếp phụ (không được đổi nó lúc này), cònSS_DISABLEdùng để vô hiệu hóa ngăn xếp phụ đang có.5When you query the state withsigaltstack(), theSS_ONSTACKflag indicates the process is currently executing on the alternate stack (which must not be changed then), whileSS_DISABLEdisables an existing alternate stack.5Kích thước nên lấy độngSize it dynamically
Trên hệ thống hiện đại, nên lấy
SIGSTKSZ/MINSIGSTKSZlúc chạy (ví dụ quasysconf()) thay vì coi chúng là hằng cố định, vì giá trị phù hợp có thể phụ thuộc đặc tính phần cứng của máy.5On modern systems, obtainSIGSTKSZ/MINSIGSTKSZat run time (for example viasysconf()) rather than treating them as fixed constants, since the right value can depend on the machine's hardware characteristics.5 -
SA_SIGINFO
SA_SIGINFO: handler nhận thêm một cấu trúc siginfo_t Bật SA_SIGINFO để handler biết nhiều hơn về tín hiệu SA_SIGINFO: the handler receives a siginfo_t structure
Một handler thông thường chỉ nhận số hiệu tín hiệu. Nếu cài handler với cờ
SA_SIGINFO, nó nhận một nguyên mẫu mở rộng —void handler(int sig, siginfo_t *si, void *ucontext)— và khi đó phải điền trườngsa_sigactioncủa cấu trúcsigactionthay vì trườngsa_handler.13 Cấu trúcsiginfo_tchứa nhiều thông tin:si_signo(số hiệu),si_code(nguồn phát tín hiệu),si_pid/si_uid(PID và UID của tiến trình gửi), vàsi_value(dữ liệu kèm theo khi dùng tín hiệu thời gian thực).13Handler thường chỉ biết "số mấy đã tới". Bật cờ
SA_SIGINFO, handler sẽ nhận kèm một tờ khai (siginfo_t) ghi rõ: ai gửi, gửi từ đâu, kèm giá trị gì. Khi dùng cờ này, ta khai handler vào ôsa_sigactionchứ không phải ôsa_handler. Nhớ nhé: SA_SIGINFO đổi lấy một tờ khai chi tiết, nhưng phải khai handler vào đúng ô.An ordinary handler receives only the signal number. If you install the handler with the
SA_SIGINFOflag, it gets an extended prototype —void handler(int sig, siginfo_t *si, void *ucontext)— and must then fill thesa_sigactionfield of thesigactionstructure instead ofsa_handler.13 Thesiginfo_tstructure carries much detail:si_signo(signal number),si_code(the signal's origin),si_pid/si_uid(the sending process's PID and UID), andsi_value(data attached when using realtime signals).13Bạn gặp nó ở đâuWhere you meet it Khi cần biết ai gửi một tín hiệu, hay truyền kèm một giá trị cùng tín hiệu thời gian thực (quasigqueue()) —SA_SIGINFOlà cách lấy những dữ liệu đó.3 Khi muốn biết ai gửi tín hiệu, hoặc nhận kèm một con số theo tín hiệu — hãy bật SA_SIGINFO. When you need to know who sent a signal, or to carry a value alongside a realtime signal (viasigqueue()) —SA_SIGINFOis how you get that data.3 -
si_code / si_addr
si_addr: địa chỉ đã gây lỗi khi sập vì SIGSEGV Khi sập, siginfo_t còn cho biết địa chỉ gây lỗi si_addr: the address that faulted on a SIGSEGV crash
Với các tín hiệu do lỗi phần cứng như
SIGSEGV,SIGBUS,SIGILLhaySIGFPE, trườngsi_addrcủasiginfo_tchứa địa chỉ bộ nhớ đã gây ra lỗi.13 Trườngsi_codethì cho biết nguồn gốc của tín hiệu — ví dụSI_USER(dokill()),SI_QUEUE(dosigqueue()),SI_KERNEL(do nhân), hay các mã riêng của từng tín hiệu nhưSEGV_MAPERR/SEGV_ACCERRchoSIGSEGV.3Khi chương trình sập vì chạm nhầm vùng nhớ, tờ khai còn ghi rõ địa chỉ nào đã gây ra sự cố (trường
si_addr), và lỗi đến từ đâu (trườngsi_code). Đó là manh mối quý để truy ra nguyên nhân. Nhớ nhé: khi sập, siginfo_t chỉ cho ta địa chỉ phạm lỗi và xuất xứ của tín hiệu.For hardware-fault signals such as
SIGSEGV,SIGBUS,SIGILL, orSIGFPE, thesi_addrfield ofsiginfo_tholds the memory address that caused the fault.13 Thesi_codefield tells the signal's origin — for exampleSI_USER(fromkill()),SI_QUEUE(fromsigqueue()),SI_KERNEL(from the kernel), or signal-specific codes likeSEGV_MAPERR/SEGV_ACCERRforSIGSEGV.3Bạn gặp nó ở đâuWhere you meet it Trong báo cáo sập của nhiều chương trình lớn: dòng "fault address 0x…" được lấy thẳng từsi_addrtrong handlerSIGSEGV.3 Dòng "địa chỉ lỗi 0x…" trong báo cáo sập chính là lấy từ si_addr của tờ khai. In the crash reports of large programs: the "fault address 0x…" line is read straight fromsi_addrin theSIGSEGVhandler.3
Cụm khái niệm 04 Concept cluster 04
Khi tín hiệu ngắt một lời gọi đang chờ: EINTR và cờ SA_RESTART Khi tín hiệu cắt ngang một lời gọi đang đứng chờ When a signal interrupts a blocked call: EINTR and the SA_RESTART flag
Nếu một handler được gọi trong lúc một system call đang đứng chờ (blocked), thì theo mặc định, sau khi handler trả về, lời gọi đó không được tiếp tục mà thất bại với mã lỗi EINTR.19 Người lập trình có hai lựa chọn: hoặc tự kiểm tra EINTR rồi gọi lại bằng tay, hoặc cài handler với cờ SA_RESTART để một số system call được nhân tự động khởi động lại.13
Giả sử chương trình đang đứng chờ gõ phím thì có tín hiệu tới. Handler chạy xong, nhưng việc "chờ gõ phím" không tự nối lại — nó báo lỗi EINTR ("bị ngắt"). Bạn có thể tự gọi lại, hoặc bật cờ SA_RESTART để hệ thống tự nối lại giúp. Nhớ nhé: bị tín hiệu ngắt thì gặp lỗi EINTR; bật SA_RESTART để đỡ phải gọi lại tay.
If a handler is invoked while a system call is blocked, then by default, after the handler returns, that call is not resumed but fails with the error EINTR.19 The programmer has two options: either check for EINTR and manually call again, or install the handler with the SA_RESTART flag so that certain system calls are automatically restarted by the kernel.13
Định nghĩa. EINTR ("interrupted system call") là mã lỗi mà một lời gọi đang chờ trả về khi nó bị một handler làm gián đoạn. Mẫu xử lý thủ công kinh điển là lặp lại lời gọi chừng nào còn gặp EINTR.19 Cờ SA_RESTART biến việc khởi động lại thành tự động — nhưng chỉ cho một số lời gọi, không phải tất cả.1
Nói đơn giản. EINTR là tín báo "việc đang chờ bị cắt ngang". Cách xử lý tay: cứ gặp EINTR thì gọi lại. Cờ SA_RESTART làm việc gọi-lại đó tự động, nhưng không áp dụng cho mọi lời gọi. Nhớ nhé: thấy EINTR thì gọi lại; SA_RESTART làm giúp nhưng không cho tất cả.
Definition. EINTR ("interrupted system call") is the error a blocked call returns when a handler interrupts it. The classic manual pattern is to repeat the call as long as it keeps returning EINTR.19 The SA_RESTART flag makes restarting automatic — but only for some calls, not all.1
/* khởi động lại thủ công */
while ((n = read(fd, buf, len)) == -1
&& errno == EINTR)
continue; /* bị ngắt: thử lại */
if (n == -1) { /* lỗi thật */ }
/* hoặc: sa.sa_flags |= SA_RESTART; */
while (read(...) == -1
&& errno == EINTR)
continue; /* bị ngắt, gọi lại */
/* hoặc bật SA_RESTART */
/* manual restart */
while ((n = read(fd, buf, len)) == -1
&& errno == EINTR)
continue; /* interrupted: retry */
if (n == -1) { /* real error */ }
/* or: sa.sa_flags |= SA_RESTART; */
-
EINTR
Mặc định, lời gọi bị ngắt sẽ thất bại — không tự nối lại Mặc định: việc đang chờ bị ngắt thì báo lỗi, không tự tiếp By default, an interrupted call fails — it does not auto-resume
Hành vi mặc định là một "bẫy" cho người mới: các lời gọi có thể chặn lâu — như
read()từ thiết bị chậm,wait(), hayaccept()— sẽ trả về-1vớierrnobằngEINTRnếu bị handler cắt ngang, dù chưa làm xong việc.19 Mã bền vững phải lường trước điều này và lặp lại lời gọi khi gặpEINTR, nếu không nó sẽ thỉnh thoảng thất bại một cách khó hiểu mỗi khi có tín hiệu tới.1Đây là bẫy hay gặp: những việc "đứng chờ lâu" như đọc bàn phím hay chờ con kết thúc, nếu bị tín hiệu cắt ngang, sẽ báo lỗi
EINTRdù chưa xong. Code tốt phải biết gặp lỗi đó thì bình tĩnh gọi lại. Nhớ nhé: EINTR không phải lỗi thật, chỉ là "bị cắt ngang, hãy gọi lại".The default behavior is a trap for newcomers: calls that may block for a long time — like
read()from a slow device,wait(), oraccept()— return-1witherrnoset toEINTRif a handler cuts in, even though the work is unfinished.19 Robust code must anticipate this and repeat the call onEINTR, or it will occasionally fail mysteriously whenever a signal arrives.1Bạn gặp nó ở đâuWhere you meet it Bất cứ chương trình nào vừa cài handler (ví dụ choSIGCHLD) vừa đọc/ghi thiết bị chậm — thiếu vòng lặpEINTRlà một lỗi rất thường gặp.9 Mọi chương trình vừa bắt tín hiệu vừa đọc/ghi chậm — quên xử lý EINTR là lỗi rất phổ biến. Any program that both installs a handler (say forSIGCHLD) and reads/writes a slow device — missing theEINTRloop is a very common bug.9 -
SA_RESTART
SA_RESTART giúp tự khởi động lại — nhưng không cho mọi lời gọi SA_RESTART gọi lại giúp, nhưng không phải việc nào cũng được SA_RESTART auto-restarts — but not for every call
Đặt cờ
SA_RESTARTkhi cài handler khiến nhân tự khởi động lại nhiều lời gọi bị ngắt, như thể chúng chưa từng bị gián đoạn.13 Nhưng tác giả nhấn mạnh một điều then chốt:SA_RESTARTkhông có tác dụng với mọi lời gọi. Một số lời gọi — nhưpoll(),select(),epoll_wait(),msgrcv()/msgsnd(),semop(), hay các thao tác socket có đặt thời gian chờ — luôn thất bại vớiEINTRdù đã bật cờ.19Bật
SA_RESTARTthì hệ thống tự gọi lại giúp nhiều việc bị ngắt, đỡ cho bạn viết vòng lặp. Nhưng có một số việc — nhưselect(),poll(), một vài thao tác mạng có hẹn giờ — thì dù bật cờ vẫn cứ báoEINTR. Với chúng, bạn vẫn phải tự lo. Nhớ nhé: SA_RESTART đỡ nhiều việc, nhưng một số việc vẫn phải gọi lại tay.Setting the
SA_RESTARTflag when installing a handler makes the kernel automatically restart many interrupted calls, as if they had never been interrupted.13 But the author stresses a crucial point:SA_RESTARTdoes not work for every call. Some calls — such aspoll(),select(),epoll_wait(),msgrcv()/msgsnd(),semop(), or socket operations with a timeout set — always fail withEINTReven with the flag set.19Bạn gặp nó ở đâuWhere you meet it Trong vòng lặp sự kiện dùngselect()/poll(): dù có bậtSA_RESTART, vẫn phải xử lýEINTRcho đúng — đây là chỗ nhiều bug nằm ẩn.9 Trong vòng lặp dùngselect()/poll(): vẫn phải tự xử lý EINTR, bật cờ không cứu được. In an event loop usingselect()/poll(): even withSA_RESTART, you still must handleEINTR— this is where many bugs hide.9siginterrupt() — cách cũsiginterrupt() — the older way
Trước
sigaction(), hàmsiginterrupt()dùng để bật/tắt việc khởi động lại cho một tín hiệu cụ thể. Nay nó bị coi là lỗi thời;SUSv4đánh dấu nó obsolete và khuyến nghị dùng cờSA_RESTARTcủasigaction()thay thế.110Beforesigaction(), thesiginterrupt()function toggled restarting for a specific signal. It is now considered dated;SUSv4marks it obsolete and recommends theSA_RESTARTflag ofsigaction()instead.110Khởi động lại cũng có mặt tráiRestarting has a downside too
Đôi khi ta muốn một lời gọi bị ngắt để thoát khỏi trạng thái chờ (ví dụ để kiểm tra một cờ đã đổi). Khi đó không nên bật
SA_RESTART, mà chủ động dựa vàoEINTRđể phá vỡ sự chờ đợi.1Sometimes we want a call interrupted so we can break out of a wait (for example to check a flag that changed). Then you should not setSA_RESTART, but deliberately rely onEINTRto break the wait.1 -
slow system calls
Chỉ "system call chậm" mới bị ngắt — và vài trường hợp đặc biệt Chỉ những việc chờ lâu mới bị ngắt giữa chừng Only "slow" calls get interrupted — plus a few special cases
Chuyện gián đoạn chỉ xảy ra với các system call chậm — những lời gọi có thể chặn vô hạn định, như đọc/ghi thiết bị chậm (bàn phím, đường ống, socket),
wait(), hay chờ khóa tệp.1 Các lời gọi hoàn tất ngay (như đọc từ tệp đĩa thông thường) thì không bị ảnh hưởng. Tác giả còn lưu ý một số lời gọi luôn bị ngắt bất kể cờ (nhưpause()), và liệt kê cụ thể trong sách những lời gọi nào khởi động lại được vớiSA_RESTART, những lời gọi nào thì không.19Chỉ những việc "có thể đứng chờ rất lâu" mới bị tín hiệu cắt ngang — như chờ gõ phím hay chờ dữ liệu qua mạng. Việc xong ngay (đọc tệp trên đĩa) thì không sao. Sách còn kê rõ việc nào tự nối lại được, việc nào không. Nhớ nhé: chỉ việc chờ lâu mới sợ bị ngắt; việc xong ngay thì yên tâm.
Interruption only happens to slow system calls — those that may block indefinitely, such as read/write on slow devices (keyboard, pipes, sockets),
wait(), or waiting on a file lock.1 Calls that complete at once (like reading from an ordinary disk file) are unaffected. The author also notes some calls are always interrupted regardless of the flag (likepause()), and the book specifically lists which calls are restartable withSA_RESTARTand which are not.19Bạn gặp nó ở đâuWhere you meet it Khi gỡ lỗi một máy chủ mạng "thỉnh thoảng đứt kết nối lúc nhận tín hiệu" — gần như luôn vì một lời gọi socket chậm bị ngắt mà không được xử lýEINTR.1 Khi máy chủ mạng thỉnh thoảng trục trặc đúng lúc nhận tín hiệu — thường vì một việc mạng chậm bị ngắt mà không xử lý. When debugging a network server that "occasionally drops a connection on a signal" — almost always a slow socket call interrupted without handlingEINTR.1
Ba ngộ nhận thường gặp Three common misconceptions
“Trong handler gọi hàm nào cũng được” “Any function may be called inside a handler”
Ngộ nhận: đã vào được handler thì gọi printf(), malloc() gì cũng an toàn.
Thực tế: chỉ được gọi các hàm async-signal-safe mà SUSv3 bảo đảm. printf(), malloc() không khả tái nhập nên có thể làm hỏng dữ liệu.2
Myth: once inside a handler, calling printf() or malloc() is safe.
Reality: only async-signal-safe functions guaranteed by SUSv3 may be called. printf() and malloc() are non-reentrant and can corrupt data.2
“longjmp() dùng được trong handler như bình thường” “longjmp() works fine from a handler”
Ngộ nhận: cứ dùng setjmp()/longjmp() để nhảy ra khỏi handler.
Thực tế: longjmp() không quy định rõ số phận của mặt nạ tín hiệu, có thể khiến tín hiệu bị chặn mãi. Dùng sigsetjmp()/siglongjmp() với savesigs để kiểm soát.14
Myth: just use setjmp()/longjmp() to jump out of a handler.
Reality: longjmp() leaves the signal mask's fate unspecified, possibly leaving a signal blocked forever. Use sigsetjmp()/siglongjmp() with savesigs to control it.14
“SA_RESTART khởi động lại mọi lời gọi bị ngắt” “SA_RESTART restarts every interrupted call”
Ngộ nhận: bật SA_RESTART là hết lo EINTR.
Thực tế: nhiều lời gọi (select(), poll(), semop(), socket có thời gian chờ…) luôn trả EINTR dù đã bật cờ; vẫn phải tự xử lý.19
Myth: set SA_RESTART and EINTR is no longer a worry.
Reality: many calls (select(), poll(), semop(), timed socket ops…) always return EINTR even with the flag; you must still handle it.19
Tự kiểm Self-check
Năm câu hỏi để biết bạn đã viết được một trình xử lý tín hiệu an toàn Năm câu hỏi kiểm tra nhanh Five questions to check you can write a safe signal handler
Trả lời được những câu này nghĩa là bạn đã nắm kỷ luật thiết kế handler — nền cho mọi chương trình hệ thống phải sống chung với tín hiệu bất đồng bộ.
Nếu trả lời được, nghĩa là bạn đã hiểu chương này.
Answering these means you grasp the discipline of handler design — the groundwork for any systems program that must coexist with asynchronous signals.
-
Phân biệt hàm khả tái nhập và async-signal-safe. Vì sao
printf()vàmalloc()không an toàn để gọi trong handler?Gợi ý sâu hơn: vì sao một handler nên lưu và khôi phục
errnodù chỉ gọi hàm an toàn? -
Vì sao một biến chia sẻ giữa handler và chương trình chính nên có kiểu
volatile sig_atomic_t? Vì sao không được dùng++trên nó?Gợi ý sâu hơn: chữ
volatilengăn trình biên dịch làm điều gì? -
Vì sao nên dùng
sigsetjmp()/siglongjmp()thay chosetjmp()/longjmp()khi nhảy ra khỏi handler? Đối sốsavesigslàm gì?Gợi ý sâu hơn: vì sao kết thúc một handler phải dùng
_exit()chứ khôngexit()? -
Trong hoàn cảnh nào bắt buộc phải dùng ngăn xếp phụ (
sigaltstack())? Nêu ba bước để lập nó.Gợi ý sâu hơn: cờ
SA_SIGINFOcho handler nhận thêm gì, và trườngsi_addrhữu ích khi nào? -
Mặc định, điều gì xảy ra với một system call đang chờ khi bị handler cắt ngang? Cờ
SA_RESTARTgiúp gì, và vì sao chưa đủ?Gợi ý sâu hơn: kể vài lời gọi luôn trả
EINTRbất kểSA_RESTART.
Vì sao không nên gọi
printf()bên trong một handler?Lá cờ mà handler đặt cho chương trình chính đọc nên khai kiểu gì?
Muốn thoát hẳn chương trình từ trong handler thì nên gọi hàm nào?
Ngăn xếp phụ dùng để làm gì?
Khi tín hiệu cắt ngang một việc đang chờ, mặc định chuyện gì xảy ra?
Distinguish reentrant from async-signal-safe. Why are
printf()andmalloc()unsafe to call in a handler?Why should a variable shared between a handler and the main program be
volatile sig_atomic_t? Why can't you use++on it?Why prefer
sigsetjmp()/siglongjmp()oversetjmp()/longjmp()when jumping out of a handler? What doessavesigsdo?When is an alternate stack (
sigaltstack()) required? Name the three steps to set one up.By default, what happens to a blocked system call interrupted by a handler? What does
SA_RESTARThelp, and why is it not enough?
Khép lại chương Closing the chapter
Một trình xử lý tín hiệu chạy bất đồng bộ, cắt ngang chương trình giữa chừng; vì thế kỷ luật của nó là làm ít và chỉ làm những việc an toàn — gọi hàm async-signal-safe, chia sẻ biến qua
volatile sig_atomic_t, thoát ra bằng_exit()haysiglongjmp(), và lường trướcEINTR. Nắm được điều này, bạn đã sẵn sàng cho các mô hình tín hiệu nâng cao và cho câu chuyện lớn hơn về vòng đời tiến trình.1Handler xen vào giữa chừng, nên hãy làm thật ít và thật an toàn: giơ cờ, gọi hàm an toàn, thoát đúng cách. Hiểu được chừng này là đủ nền để đi tiếp. Nhớ nhé: trong handler, ít mà an toàn luôn thắng nhiều mà liều.
A signal handler runs asynchronously, cutting in on the program mid-step; so its discipline is to do little and do only what is safe — call async-signal-safe functions, share variables via
volatile sig_atomic_t, exit via_exit()orsiglongjmp(), and anticipateEINTR. Grasp this, and you are ready for advanced signal models and the larger story of the process lifecycle.1