Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 44.226 phút đọc

Quy tắc vàng viết handler

Sau bài này bạn sẽ làm được

  • Kể các hàm async-signal-safe được phép gọi
  • Giải thích vì sao printf trong handler nguy hiểm
  • Áp dụng mẫu handler chỉ đặt cờ
  • Chứng minh vấn đề bằng một chương trình chạy được

Bộ xử lý tín hiệu chạy giữa chừng mọi thứ, kể cả giữa một lệnh malloc hay printf. Nên nó chỉ được gọi một tập nhỏ các hàm an toàn. Vi phạm quy tắc này tạo ra những lỗi hiếm, không tái hiện được, và cực khó tìm.

#Vấn đề

/* Handler chay KHONG DONG BO: no ngat luong chinh o BAT KY diem
   nao. Tuong tuong luong chinh dang o giua printf, dang giu khoa
   noi bo cua stdio, thi tin hieu toi va handler cung goi printf.

   printf thu lay khoa do -> khoa da bi luong chinh giu -> KHOA
   CHET. Chuong trinh treo, va no chi treo khi tin hieu toi DUNG
   luc printf dang chay, tuc rat hiem va khong tai hien duoc.

   Do la ly do handler chi duoc goi ham ASYNC-SIGNAL-SAFE: ham
   khong dung khoa hay trang thai toan cuc co the dang bi luong
   chinh dung do. */
Async-signal-safe
Một hàm an toàn để gọi từ bên trong bộ xử lý tín hiệu. Nó không dùng khoá dùng chung, không dùng trạng thái toàn cục có thể đang bị dở dang, nên gọi nó giữa chừng một hàm khác không gây hỏng.

#Hàm async-signal-safe

Trong handlerĐược phépVì sao
writeCóSyscall trực tiếp, không đệm, không khoá
_exitCóThoát ngay, không xả bộ đệm
read, closeCóSyscall trực tiếp
signal, kill, raiseCóAsync-signal-safe
Gán sig_atomic_tCóNguyên tử
printf, fprintfKHÔNGDùng khoá và bộ đệm stdio
malloc, freeKHÔNGDùng khoá heap, có thể đang bị giữ
exitKHÔNGChạy atexit, xả bộ đệm, không an toàn
strcpy, sprintfKHÔNG, một số bảnCó thể dùng locale hoặc trạng thái

#Vì sao printf nguy hiểm

sigbad.c, printf trong handler
#include <stdio.h>
#include <signal.h>

static void xu_ly(int s) {
    (void)s;
    printf("[trong handler] KHONG an toan!\n");   /* SAI: printf khong an toan */
}

int main(void) {
    signal(SIGINT, xu_ly);
    raise(SIGINT);
    printf("[main] sau raise\n");
    return 0;
}
terminal
gcc -O2 -Wall -Wextra sigbad.c -o sigbad.exe && ./sigbad.exe
[trong handler] KHONG an toan!
[main] sau raise

#Mẫu đúng

Handler chỉ đặt cờ, main xử lý
#include <signal.h>
#include <unistd.h>

static volatile sig_atomic_t co_tin_hieu = 0;

static void handler(int s) {
    (void)s;
    co_tin_hieu = 1;                /* CHI dat co */
    /* Neu that su can in trong handler, dung write (an toan): */
    /* write(STDERR_FILENO, "nhan tin hieu\n", 14); */
}

int main(void) {
    struct sigaction sa = {0};
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGINT, &sa, NULL);

    while (1) {
        if (co_tin_hieu) {          /* xu ly o vong lap chinh: AN TOAN */
            don_dep();
            break;
        }
        lam_viec();
    }
    return 0;
}
Làm việc trong handler
static void handler(int s) { (void)s; printf("dang thoat...\n"); /* khong an toan */ dong_moi_ket_noi(); /* dung khoa, malloc, khong an toan */ luu_du_lieu(); /* ghi tep phuc tap, khong an toan */ exit(0); /* chay atexit, khong an toan */ }
Chỉ đặt cờ
static void handler(int s) { (void)s; co_tin_hieu = 1; /* DUY NHAT dieu an toan */ } /* Don dep o vong lap chinh, noi moi ham deu an toan: */ if (co_tin_hieu) { dong_moi_ket_noi(); luu_du_lieu(); break; }

#Ống tự đánh thức

Tự làm thử

  1. Gọi printf trong cả main và handler, gửi tín hiệu liên tục, và quan sát chương trình hỏng.
  2. Viết lại theo mẫu handler chỉ đặt cờ và xác nhận nó không hỏng.
  3. In một thông điệp từ handler dùng write thay vì printf.
  4. Bắt SIGSEGV, in backtrace bằng backtrace_symbols_fd, rồi _exit.
  5. Đọc man 7 signal-safety và liệt kê mười hàm an toàn.
  6. Cài kỹ thuật self-pipe để tín hiệu đánh thức một vòng lặp select.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Handler chạy giữa chừng mọi thứ, nên chỉ được gọi hàm async-signal-safe.
  • write, _exit, kill, gán sig_atomic_t là an toàn; printf, malloc, exit thì không.
  • printf trong handler chạy đúng phần lớn thời gian và chỉ hỏng khi trùng thời điểm, nên là lỗi cực khó tìm.
  • Mẫu an toàn: handler chỉ đặt cờ, việc thật làm ở vòng lặp chính; nếu phải in thì dùng write.
  • Self-pipe hoặc signalfd kết hợp tín hiệu với vòng lặp sự kiện để thoát sạch một server.