Signals (Tín hiệu) là một cách để gửi những message đơn giản đến một tiến trình hoặc một nhóm tiến trình. Điều quan trọng cần nhớ là signal không phải là exception, nhưng signal có thể được kích hoạt do một exception. Signals là một trong những phương thức giao tiếp liên tiến trình (IPC) lâu đời nhất, chúng được dùng để báo hiệu các sự kiện bất đồng bộ cho một hoặc nhiều tiến trình.

Một signal có thể được tạo ra bởi thao tác bàn phím (ví dụ: nhấn Ctrl+C) hoặc bởi một tình trạng lỗi (ví dụ: tiến trình cố gắng truy cập vào vùng nhớ không tồn tại trong virtual memory của nó). Shell cũng sử dụng signals để truyền các lệnh điều khiển job (job control) tới các tiến trình con.

Kernel dùng signals để thông báo cho các tiến trình về những sự kiện của hệ thống. Tuy nhiên, signal chỉ có thể được xử lý khi tiến trình đang ở user mode (process context). Nếu một signal được gửi đến tiến trình khi nó đang ở kernel mode (kernel context), signal đó sẽ được xử lý ngay khi tiến trình quay trở lại user mode.

Tuỳ vào các loại kiến trúc và hệ điều hành khác nhau thì chúng có thể cung cấp các loại signals khác nhau. Signals sẽ được truyền bởi system call là kill. Câu lệnh sẽ có dạng là:


$ kill -[SIGNALS] [PID]

Để biết được device của bạn đang hỗ trợ các loại signals nào thì bạn có thể sử dụng lệnh: 


$ kill -l



Các bạn có thể thắc mắc không có signal 32 và 33 thì chúng được POSIX dành cho threading nội bộ. Khi Linux hỗ trợ POSIX threads (pthread), hai signal này được giữ lại để phục vụ hệ thống luồng (threads). Nên thay vì cho phép dùng bình thường, kernel reserve (giữ lại) và không cho ứng dụng người dùng (user-space) truy cập. (Các bạn có thể tham khảo thêm ở đây signals-32-and-33).


Mỗi signal có một tên signal duy nhất, là một chữ viết tắt bắt đầu bằng SIG (ví dụ: SIGINT cho tín hiệu ngắt interrupt), và một mã số signal tương ứng. Ngoài ra, với tất cả các loại signal có thể xảy ra, hệ thống đều định nghĩa một hành động mặc định được thực hiện khi signal xuất hiện. Có bốn loại hành động mặc định như sau:


  • Exit: buộc tiến trình phải thoát.
  • Core: buộc tiến trình thoát và tạo một tệp core dump.
  • Stop: dừng (tạm ngưng) tiến trình.
  • Ignore: bỏ qua signal; không thực hiện hành động nào.

Lưu ý, Signals sẽ không được gửi ngay lập tức đến tiến trình ngay sau khi chúng được tạo ra (ví dụ: nhấn Ctrl+C). Thay vào đó, chúng phải chờ cho đến khi tiến trình quay trở lại usermod. Mỗi lần một tiến trình thoát khỏi một system call, các trường signalblocked của nó sẽ được kiểm tra. Nếu có bất kỳ signal nào không bị chặn (unblocked), chúng sẽ được gửi đến tiến trình tại thời điểm đó.

Điều này có vẻ là một phương thức không đáng tin cậy vì nó phụ thuộc vào việc tiến trình thường xuyên kiểm tra signals. Tuy nhiên, trên thực tế mọi tiến trình trong hệ thống đều thực hiện system call liên tục, kể cả khi ghi một ký tự ra terminal, nên signals luôn có cơ hội được xử lý. Process cũng có thể tự chọn chờ signal nếu muốn; trong trường hợp này, nó sẽ bị treo ở trạng thái Interruptible cho đến khi một signal được gửi đến. Code xử lý signal của Linux sẽ xem xét cấu trúc sigaction tương ứng với từng signal không bị chặn của tiến trình hiện tại.

Nếu signal có handler được đặt là hành động mặc định, kernel sẽ tự xử lý signal đó. Ngược lại, tiến trình có thể tự định nghĩa handler riêng cho signal. Signal handler là một hàm đặc biệt được gọi mỗi khi signal được tạo ra, và cấu trúc sigaction lưu địa chỉ của hàm này. Nhiệm vụ của kernel là gọi hàm handler của tiến trình khi signal xảy ra.

Lưu ý: SIGKILLSIGSTOP là hai signal duy nhất mà không thể thay đổi hành vi; kernel luôn thực hiện chức năng mặc định của chúng.

Mỗi signal có thể ở một trong ba trạng thái:


  1. Có handler riêng: tiến trình đã định nghĩa handler riêng cho signal.
  2. Handler mặc định: signal được xử lý bởi hàm mặc định của nó. Ví dụ, handler mặc định của SIGINT sẽ kết thúc chương trình.
  3. Bị bỏ qua (ignored): signal bị bỏ qua. Việc bỏ qua signal đôi khi cũng được gọi là chặn signal (blocking).

Linux lưu thông tin về signals trong task_struct của mỗi process. Số lượng signals được hỗ trợ bị giới hạn bởi kiến trúc của CPU (CPU 32-bit = 32 signals, CPU 64-bit = 64 signals). Các signal đang chờ xử lý được lưu trong trường signal, còn signal bị chặn được lưu trong trường blocked. Ngoại trừ SIGSTOP và SIGKILL, tất cả các signal đều có thể bị chặn. Nếu một signal bị chặn được tạo ra, nó sẽ chờ xử lý cho đến khi được mở chặn. Linux cũng lưu thông tin về cách mỗi tiến trình xử lý tất cả signal có thể có trong một mảng các cấu trúc sigaction, trỏ bởi task_struct của tiến trình. Trong mảng này, mỗi phần tử lưu địa chỉ của hàm handler hoặc cờ (flag) cho biết process muốn: bỏ qua signal hoặc để kernel xử lý signal. Tiến trình có thể thay đổi cách xử lý signal mặc định bằng cách gọi system call, và các system call này sẽ thay đổi cấu trúc sigaction tương ứng cũng như cập nhật mask của các signal bị chặn.



Trong Linux, một process có thể rơi vào trạng thái ngủ (sleeping) để chờ một sự kiện nào đó. Nếu nó đang “ngủ có thể bị đánh thức” (interruptible sleep, trạng thái S) thì khi có signal, process sẽ tỉnh dậy để xử lý signal đó. Nếu nó ngủ ở trạng thái “không thể bị đánh thức” (uninterruptible sleep, trạng thái D – ví dụ chờ I/O), thì signal sẽ không đánh thức nó được.

Kernel lưu danh sách các signal đang chờ xử lý trong struct task_struct của mỗi process. Danh sách signal pending được lưu bằng bitmask 32-bit (hoặc 64-bit tùy kiến trúc). Mỗi signal tương ứng với 1 bit trong mask.

Ví dụ:

Bit 2 bật → có SIGINT pending

Bit 9 bật → có SIGKILL pending

...

Vì mỗi loại signal chỉ có 1 bit, nên kernel chỉ lưu được tối đa 1 signal pending cho mỗi loại. Nếu cùng một signal đến 10 lần liên tục thì kernel chỉ nhớ 1 lần (coalescing). Nếu có nhiều signal pending cùng lúc, kernel không biết signal nào đến trước, vì bitmask không lưu thời gian. Do đó kernel xử lý theo thứ tự từ số hiệu signal thấp đến cao.

Ví dụ, nếu cả SIGINT (2) và SIGTERM (15) cùng pending → kernel xử lý SIGINT trước.

Một process có thể block một số signal bằng sigprocmask() hoặc sigaction(). Nếu một signal bị block, nó không được xử lý ngay, nhưng vẫn được lưu trong pending mask. Khi un-block process xử lý ngay lập tức các signal đang pending.


Note: SIGKILL có giá trị là 9 nên 2 câu lệnh dưới đây sẽ có tác dụng như nhau:


$ kill -SIGKILL 12345
$ kill -9 12345


Đăng kí hàm signal handler với: sigaction().

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);


  • signum: Số hiệu signal (SIGINT, SIGTERM, 2, 9…).
  • act: struct mô tả cách xử lý signal mới.
  • oldact: struct để lưu handler cũ (có thể NULL).


Cấu trúc này mô tả process sẽ xử lý signal như thế nào:


struct sigaction
{
     void (*sa_handler)(int signum);
     void (*sa_sigaction)(int signum, siginfo_t *siginfo, void *uctx);
     sigset_t sa_mask;
     int sa_flags;
     void (*sa_restorer)(void);
};

sa_handler là con trỏ đến hàm xử lý signal (signal handler). Hàm này nhận một số nguyên duy nhất — chính là mã của signal mà nó xử lý — và trả về void.

Ngoài ra, sigaction() còn cho phép bạn sử dụng một loại hàm xử lý signal nâng cao hơn. Nếu cần dùng kiểu nâng cao, trường sa_sigaction sẽ trỏ đến hàm xử lý đó. Hàm này nhận được nhiều thông tin hơn về nguồn gốc của signal.

Để sử dụng handler dạng sa_sigaction, bạn phải đặt cờ SA_SIGINFO trong sa_flags của struct sigaction. Tương tự như sa_handler, sa_sigaction cũng nhận tham số đầu tiên là mã signal. Nhưng thêm vào đó, nó nhận một con trỏ đến cấu trúc siginfo_t. Cấu trúc này mô tả chi tiết nguồn gốc của signal. (Ví dụ, trường si_pid của siginfo_t chứa PID của tiến trình đã gửi signal. Ngoài ra còn nhiều trường khác cung cấp rất nhiều thông tin hữu ích về signal. Bạn có thể xem chi tiết hơn trong trang man sigaction.)

Tham số cuối cùng mà handler sa_sigaction nhận là con trỏ đến ucontext_t. Kiểu dữ liệu này thay đổi tùy theo kiến trúc CPU. 

Đây sẽ là ví dụ của sử dụng hàm sigaction():


~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test.c ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
/*
 * This is to test sigaction
*/
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

void my_handler(int sig, siginfo_t *info, void *context) {
    printf("Caught signal %d\n", sig);
    printf("Signal sent by PID: %d\n", info->si_pid);
}

int main() {
    printf("I am %lu\n", (unsigned long)getpid());

    struct sigaction act;
    memset(&act, 0, sizeof(act));

    act.sa_sigaction = my_handler;
    act.sa_flags = SA_SIGINFO;

    sigaction(SIGINT, &act, NULL);

    printf("Program running... Press Ctrl+C\n");

    while (1) {
        sleep(1);
    }

    return 0;
}


Overall: đây là chương trình cơ bản trong đó giúp bạn handler SIGINT (tương đương việc nhấn tổ hợp phím Ctrl+C trên bàn phím). Hàm handler sẽ in ra số hiệu của SIGINT cùng pid của process gửi đến nó. Để chạy code trên các bạn compile với dòng lệnh sau:


$ gcc test.c -o test
$ ./test



Giải thích một chút, lần đầu tiên mình nhấn tổ hợp phím Ctrl+C, chương trình báo rằng PID là 0 ý nghĩa rằng là SIGINT được sinh ra từ terminal. Lần 2 mình tạo 1 terminal khác và gửi bằng câu lệnh kill lúc này sẽ báo ra PID của process gửi đến. 

Một ưu điểm nữa của sigaction() là nó cho phép bạn chỉ định những signal nào có thể được nhận trong khi handler đang chạy. Nói cách khác, bạn toàn quyền kiểm soát những signal nào được phép “chen vào” khi chương trình đang xử lý một signal khác. Bạn thao tác với trường sa_mask trong struct sigaction. sa_mask có kiểu sigset_t, đại diện cho signal mask (tập hợp các signal bị chặn tạm thời khi handler đang chạy). Các hàm để thao tác với sigset_t:

int sigemptyset(sigset_t *) Xóa toàn bộ mask (không chặn signal nào)

int sigfillset(sigset_t *)  Đặt tất cả signal vào mask (chặn tất cả)

int sigaddset(sigset_t *, int signum) Thêm signal cụ thể vào mask (chặn signal này)

int sigdelset(sigset_t *, int signum) Bỏ signal cụ thể khỏi mask (cho phép signal này)

int sigismember(sigset_t *, int signum) Kiểm tra signal có nằm trong mask hay không


Ví dụ: Viết chương trình sử signal SIGINT trong 5 giây, trong khoảng thời gian đó thì SIGTERM sẽ bị chặn.


#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
// Handler đơn giản
void handler(int sig) {
    printf("Handling signal %d\n", sig);
    sleep(5); // giả lập xử lý lâu
    printf("Done handling %d\n", sig);
}

int main() {
    struct sigaction act;

    // Khởi tạo struct về 0 để tránh giá trị rác
    memset(&act, 0, sizeof(act));

    // Thiết lập handler cho SIGINT
    act.sa_handler = handler;

    // Khởi tạo sa_mask rỗng
    sigemptyset(&act.sa_mask);

    // Thêm SIGTERM vào mask → block SIGTERM trong khi handler xử lý SIGINT
    sigaddset(&act.sa_mask, SIGTERM);

    // Đăng ký handler cho SIGINT
    if (sigaction(SIGINT, &act, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    printf("PID = %d\n", getpid());
    printf("Press Ctrl+C to trigger SIGINT\n");

    while (1) {
        pause(); // đợi signal
    }

    return 0;
}


Kết quả: Khi bắt đầu in ra “Handling signal” trong 5 giây tiếp theo sẽ không thể gửi được SIGTERM cho process ấy.


Giải thích:


  • Khi chạy chương trình, nhấn Ctrl + C để bắt đầu vào hàm handler(). Lúc này, sau khi in ra log “Handling signal 2” thì process sẽ sleep 5 giây..
  • Khi gửi tín hiệu SIGTERM tới process từ một terminal khác, process sẽ không phản hồi vì đã bị block trước đó.
  • Process vẫn bị Terminated sau khi xử lý handler xong đó là gì SIGTERM chỉ bị block trong lúc handler chạy, nó vẫn được lưu vào signal peding nên sau khi hoàn thành handler() nó tiếp tục xử lý SIGTERM.