The Linux Programming Interface Chương 21 · Signals: Signal Handlers

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); }

  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ọi malloc() 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.12

    Hà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 calls malloc() 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 as strtok(), getpwnam(), crypt() — are non-reentrant.12

    Bạn gặp nó ở đâuWhere you meet it Mỗi khi thấy một handler gọi printf() để 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ọi printf() bên trong handler để in thử — đó là thói quen nguy hiểm. Whenever you see a handler call printf() to debug — that is a latent bug, because the stdio library is non-reentrant.2

    Vì 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ọi printf(), 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 calls printf(), the second call operates on half-updated data, with unpredictable results.2

    Phiê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 _r suffix (for example strtok_r(), getpwnam_r()): they take an extra caller-supplied buffer argument, so they do not rely on shared static memory.1

  2. 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ật errno; nếu chương trình chính bị ngắt ngay sau một system call và chưa kịp đọc errno, handler sẽ ghi đè mất giá trị đó. Vì vậy handler nên lưu errno khi vào và khôi phục khi ra.12

    Trong 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ỗi errno; handler nên chép lại errno lú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 example write(), _exit(), sigaction(), kill() — and avoid everything else.2 There is a subtle trap: even when a handler calls only safe functions, it may still update errno; if the main program was interrupted right after a system call and had not yet read errno, the handler would clobber that value. So a handler should save errno on entry and restore it on exit.12

    Bạn gặp nó ở đâuWhere you meet it Khi cần in gì đó từ handler: dùng write(STDOUT_FILENO, ...) thay cho printf(), vì write() nằm trong danh sách an toàn còn printf() thì không.2 Muốn in trong handler thì dùng write() thay cho printf() — một cái an toàn, một cái không. When you must print from a handler: use write(STDOUT_FILENO, ...) instead of printf(), since write() is on the safe list and printf() is not.2

    POSIX 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

  3. 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ên sig_atomic_t không được bảo đảm nguyên tử.1

    Khi 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_t là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_t type — an integer type whose reads and writes are always atomic — and requires declaring it volatile so 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 a sig_atomic_t are not guaranteed atomic.1

    Bạ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ểu volatile sig_atomic_t là đã 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 đúng volatile sig_atomic_t. Every flag a handler sets and the main program reads — declaring it exactly volatile sig_atomic_t heads 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); */

  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ống setjmp()/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.14

    Cặ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() and siglongjmp() are like Chapter 6's setjmp()/longjmp(), but add a savesigs argument to deliberately decide whether the signal mask is saved and restored. This is needed because setjmp()/longjmp() leave the mask's fate unspecified — on some systems it is restored, on others not, making the behavior non-portable.14

    Bạ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 (say SIGINT) rather than exit — siglongjmp() cleanly returns control to the top of the loop.1

    Nguồ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ặp sigsetjmp()/siglongjmp() cho ta quyền kiểm soát tường minh.14The author explains: System V does not restore the mask across longjmp(), whereas BSD does; to avoid platform dependence, the standardized sigsetjmp()/siglongjmp() pair gives us explicit control.14

    Vẫ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 sau siglongjmp(); biến cần giữ đúng vẫn phải khai volatile.14As with longjmp(), locals optimized into registers may hold wrong values after siglongjmp(); variables that must survive still need volatile.14

  2. _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ọi exit(). Lý do: exit() xả các vùng đệm stdio và chạy các bộ xử lý thoát (exit handler) do atexit()/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.172

    Khi handler muốn tắt chương trình, dùng _exit() — nó tắt ngay. Đừng dùng exit(), 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(), not exit(). The reason: exit() flushes stdio buffers and runs exit handlers registered with atexit()/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.172

    Bạ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
  3. 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ệu SIGABRT cho 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ếu SIGABRT đ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.6

    abort() 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 the SIGABRT signal at the process itself.16 A memorable point: abort() is designed to always succeed in terminating the process — if SIGABRT is blocked or ignored, abort() undoes that to ensure the signal takes effect.6

    Bạ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ọi abort().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 when abort() 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);

  1. 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ột SIGSEGV do tràn ngăn xếp là chạy handler trên một ngăn xếp phụ đã dựng sẵn bằng sigaltstack().15 Cấu trúc stack_t gồm ba trường: ss_sp (địa chỉ gốc), ss_size (số byte), và ss_flags (cờ như SS_ONSTACK, SS_DISABLE).5

    Khi 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-overflow SIGSEGV is to run the handler on an alternate stack set up in advance with sigaltstack().15 The stack_t structure has three fields: ss_sp (base address), ss_size (number of bytes), and ss_flags (flags such as SS_ONSTACK, SS_DISABLE).5

    Bạ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.5

    SS_ONSTACK và SS_DISABLESS_ONSTACK and SS_DISABLE

    Khi truy vấn trạng thái bằng sigaltstack(), cờ SS_ONSTACK cho 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òn SS_DISABLE dùng để vô hiệu hóa ngăn xếp phụ đang có.5When you query the state with sigaltstack(), the SS_ONSTACK flag indicates the process is currently executing on the alternate stack (which must not be changed then), while SS_DISABLE disables an existing alternate stack.5

    Kích thước nên lấy độngSize it dynamically

    Trên hệ thống hiện đại, nên lấy SIGSTKSZ/MINSIGSTKSZ lúc chạy (ví dụ qua sysconf()) 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, obtain SIGSTKSZ/MINSIGSTKSZ at run time (for example via sysconf()) rather than treating them as fixed constants, since the right value can depend on the machine's hardware characteristics.5

  2. 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ường sa_sigaction của cấu trúc sigaction thay vì trường sa_handler.13 Cấu trúc siginfo_t chứ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).13

    Handler 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_sigaction chứ 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_SIGINFO flag, it gets an extended prototype — void handler(int sig, siginfo_t *si, void *ucontext) — and must then fill the sa_sigaction field of the sigaction structure instead of sa_handler.13 The siginfo_t structure carries much detail: si_signo (signal number), si_code (the signal's origin), si_pid/si_uid (the sending process's PID and UID), and si_value (data attached when using realtime signals).13

    Bạ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 (qua sigqueue()) — SA_SIGINFO là 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 (via sigqueue()) — SA_SIGINFO is how you get that data.3
  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, SIGILL hay SIGFPE, trường si_addr của siginfo_t chứa địa chỉ bộ nhớ đã gây ra lỗi.13 Trường si_code thì cho biết nguồn gốc của tín hiệu — ví dụ SI_USER (do kill()), SI_QUEUE (do sigqueue()), SI_KERNEL (do nhân), hay các mã riêng của từng tín hiệu như SEGV_MAPERR/SEGV_ACCERR cho SIGSEGV.3

    Khi 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ường si_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, or SIGFPE, the si_addr field of siginfo_t holds the memory address that caused the fault.13 The si_code field tells the signal's origin — for example SI_USER (from kill()), SI_QUEUE (from sigqueue()), SI_KERNEL (from the kernel), or signal-specific codes like SEGV_MAPERR/SEGV_ACCERR for SIGSEGV.3

    Bạ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_addr trong handler SIGSEGV.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 from si_addr in the SIGSEGV handler.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; */

  1. 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(), hay accept() — sẽ trả về -1 với errno bằng EINTR nế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ặp EINTR, 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 EINTR dù 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(), or accept() — return -1 with errno set to EINTR if a handler cuts in, even though the work is unfinished.19 Robust code must anticipate this and repeat the call on EINTR, or it will occasionally fail mysteriously whenever a signal arrives.1

    Bạn gặp nó ở đâuWhere you meet it Bất cứ chương trình nào vừa cài handler (ví dụ cho SIGCHLD) vừa đọc/ghi thiết bị chậm — thiếu vòng lặp EINTR là 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 for SIGCHLD) and reads/writes a slow device — missing the EINTR loop is a very common bug.9
  2. 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_RESTART khi 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_RESTART khô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ới EINTR dù đã bật cờ.19

    Bật SA_RESTART thì 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áo EINTR. 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_RESTART flag 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_RESTART does not work for every call. Some calls — such as poll(), select(), epoll_wait(), msgrcv()/msgsnd(), semop(), or socket operations with a timeout set — always fail with EINTR even with the flag set.19

    Bạn gặp nó ở đâuWhere you meet it Trong vòng lặp sự kiện dùng select()/poll(): dù có bật SA_RESTART, vẫn phải xử lý EINTR cho đúng — đây là chỗ nhiều bug nằm ẩn.9 Trong vòng lặp dùng select()/poll(): vẫn phải tự xử lý EINTR, bật cờ không cứu được. In an event loop using select()/poll(): even with SA_RESTART, you still must handle EINTR — this is where many bugs hide.9

    siginterrupt() — cách cũsiginterrupt() — the older way

    Trước sigaction(), hàm siginterrupt() 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_RESTART của sigaction() thay thế.110Before sigaction(), the siginterrupt() function toggled restarting for a specific signal. It is now considered dated; SUSv4 marks it obsolete and recommends the SA_RESTART flag of sigaction() instead.110

    Khở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ào EINTR để 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 set SA_RESTART, but deliberately rely on EINTR to break the wait.1

  3. 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ới SA_RESTART, những lời gọi nào thì không.19

    Chỉ 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 (like pause()), and the book specifically lists which calls are restartable with SA_RESTART and which are not.19

    Bạ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 handling EINTR.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.

  1. 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 errno dù chỉ gọi hàm an toàn?

  2. 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ữ volatile ngăn trình biên dịch làm điều gì?

  3. Vì sao nên dùng sigsetjmp()/siglongjmp() thay cho setjmp()/longjmp() khi nhảy ra khỏi handler? Đối số savesigs làm gì?

    Gợi ý sâu hơn: vì sao kết thúc một handler phải dùng _exit() chứ không exit()?

  4. 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_SIGINFO cho handler nhận thêm gì, và trường si_addr hữu ích khi nào?

  5. 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_RESTART giúp gì, và vì sao chưa đủ?

    Gợi ý sâu hơn: kể vài lời gọi luôn trả EINTR bất kể SA_RESTART.

  1. Vì sao không nên gọi printf() bên trong một handler?

  2. Lá cờ mà handler đặt cho chương trình chính đọc nên khai kiểu gì?

  3. Muốn thoát hẳn chương trình từ trong handler thì nên gọi hàm nào?

  4. Ngăn xếp phụ dùng để làm gì?

  5. Khi tín hiệu cắt ngang một việc đang chờ, mặc định chuyện gì xảy ra?

  1. Distinguish reentrant from async-signal-safe. Why are printf() and malloc() unsafe to call in a handler?

  2. Why should a variable shared between a handler and the main program be volatile sig_atomic_t? Why can't you use ++ on it?

  3. Why prefer sigsetjmp()/siglongjmp() over setjmp()/longjmp() when jumping out of a handler? What does savesigs do?

  4. When is an alternate stack (sigaltstack()) required? Name the three steps to set one up.

  5. By default, what happens to a blocked system call interrupted by a handler? What does SA_RESTART help, 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() hay siglongjmp(), và lường trước EINTR. 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.1

Handler 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() or siglongjmp(), and anticipate EINTR. Grasp this, and you are ready for advanced signal models and the larger story of the process lifecycle.1