Signal adalah notifikasi asynchronous yang dikirim kernel ke proses. Sinyal bisa datang dari kernel (seperti SIGSEGV saat akses memori ilegal), dari proses lain (perintah kill), atau dari user (Ctrl-C yang mengirim SIGINT). Setiap proses dapat memasang handler—fungsi yang dipanggil otomatis saat sinyal tiba. Handler ini berjalan di tengah-tengah eksekusi normal dan wajib mengikuti aturan async-signal-safety: hanya boleh memanggil fungsi yang aman dipakai di signal handler (daftar lengkap di man 7 signal-safety). Pada chapter ini kita memasang handler SIGINT dengan signal(), lalu menggantinya dengan sigaction() yang lebih kuat, dan akhirnya memakai pola self-pipe yang memindahkan semua kerja berat keluar dari handler.

Handler Sederhana dengan signal() #

API signal() adalah cara paling klasik memasang handler. Signature fungsi handler menerima satu argumen int sig yang berisi nomor sinyal yang diterima.

c
/* File: signal_sederhana.c - handler SIGINT (Ctrl-C) */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

static volatile sig_atomic_t stop = 0;

/* Handler hanya memakai async-signal-safe operations */
static void handler_sigint(int sig) {
    (void) sig;
    /* write adalah async-signal-safe */
    const char pesan[] = "\n[Terima SIGINT - akan keluar]\n";
    write(STDOUT_FILENO, pesan, sizeof(pesan) - 1);
    stop = 1;
}

int main(void) {
    signal(SIGINT, handler_sigint);

    printf("PID=%d - ketik Ctrl-C untuk kirim SIGINT\n", getpid());
    printf("Atau jalankan: kill -SIGINT %d\n", getpid());

    int i = 0;
    while (!stop) {
        printf("bekerja... %d\n", i++);
        fflush(stdout);
        sleep(1);
    }
    printf("Program keluar dengan baik.\n");
    return 0;
}

Compile dan jalankan—kita kirim SIGINT setelah 2 detik untuk simulasi Ctrl-C:

bash
gcc -std=c11 -Wall -Wextra -o signal_sederhana signal_sederhana.c
./signal_sederhana &
PID=$!; sleep 2; kill -SIGINT $PID; wait $PID

Output yang Dihasilkan:

text
PID=12882 - ketik Ctrl-C untuk kirim SIGINT
Atau jalankan: kill -SIGINT 12882
bekerja... 0
bekerja... 1
bekerja... 2

[Terima SIGINT - akan keluar]
Program keluar dengan baik.

Mari kita telaah baris-baris kode di atas:

  1. Baris 7 (static volatile sig_atomic_t stop = 0;): Variabel flag yang diperiksa di loop utama dan ditulis di handler. volatile mencegah compiler meng-cache nilai di register (loop harus membaca ulang dari memori setiap iterasi). sig_atomic_t menjamin bahwa penulisan ke variabel ini atomic di tingkat mesin—penting karena handler bisa dipanggil di tengah instruksi.
  2. Baris 11-13 (write(STDOUT_FILENO, ...)): Mengapa write, bukan printf? Karena printf tidak async-signal-safe—ia menggunakan buffer internal yang bisa rusak bila handler dipanggil saat printf sedang berjalan di main. write langsung memanggil kernel tanpa buffer.
  3. Baris 17 (signal(SIGINT, handler_sigint)): Memasang handler. Saat SIGINT tiba, kernel menjalankan handler_sigint alih-alih membunuh proses (default action SIGINT adalah terminate).
  4. Baris 24 (while (!stop)): Loop memeriksa flag stop. Saat handler set stop = 1, loop keluar di iterasi berikutnya dan program berakhir dengan rapi.
  5. sleep(1) di loop: Memberi kesempatan scheduler untuk mengeksekusi handler saat sinyal tiba. Tanpa sleep, loop yang ketat mungkin tidak sempat membaca stop yang baru di-set handler.

sigaction: API POSIX yang Lebih Kuat #

sigaction() memberi kontrol penuh: kita bisa memilih flag (misalnya SA_RESTART untuk me-restart syscall yang terinterupsi), memblokir sinyal lain selama handler berjalan (sa_mask), dan membaca handler lama. Struktur dasar:

c
/* File: sigaction_demo.c - sigaction dengan SA_RESTART */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

static volatile sig_atomic_t hitung = 0;

static void handler(int sig) {
    (void) sig;
    hitung++;
    char buf[64];
    int n = snprintf(buf, sizeof(buf), "[SIGUSR1 diterima, hitung=%d]\n", hitung);
    write(STDOUT_FILENO, buf, n);
}

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;   /* syscalls di-restart otomatis */

    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    printf("PID=%d - kirim SIGUSR1: kill -SIGUSR1 %d\n", getpid(), getpid());

    for (int i = 0; i < 5; i++) {
        sleep(1);
        printf("detik %d (hitung=%d)\n", i + 1, hitung);
        fflush(stdout);
    }

    printf("Total sinyal diterima: %d\n", hitung);
    return 0;
}

Compile dan jalankan—kirim SIGUSR1 beberapa kali dari terminal lain:

bash
gcc -std=c11 -Wall -Wextra -o sigaction_demo sigaction_demo.c
./sigaction_demo &
PID=$!; sleep 1; kill -SIGUSR1 $PID; sleep 1; kill -SIGUSR1 $PID; wait $PID

Output yang Dihasilkan:

text
PID=12899 - kirim SIGUSR1: kill -SIGUSR1 12899
detik 1 (hitung=0)
[SIGUSR1 diterima, hitung=1]
detik 2 (hitung=1)
detik 3 (hitung=1)
[SIGUSR1 diterima, hitung=2]
detik 4 (hitung=2)
detik 5 (hitung=2)
Total sinyal diterima: 2

Mari kita telaah baris-baris kode di atas:

  1. Baris 14-18 (struct sigaction sa;): Struktur POSIX standar. Field sa_handler menunjuk fungsi handler. Field sa_mask adalah signal set yang akan diblokir selama handler berjalan—SIGUSR1 tidak bisa menginterupsi dirinya sendiri.
  2. Baris 16 (sigemptyset(&sa.sa_mask);): Mengosongkan sa_mask terlebih dahulu. Tanpa sigemptyset, struktur yang tidak di-init (memset 0) bisa berisi bit acak.
  3. Baris 17 (sa.sa_flags = SA_RESTART;): Saat handler menginterupsi syscall (misalnya sleep), syscall otomatis di-restart setelah handler selesai. Tanpa SA_RESTART, syscall kembali dengan error EINTR—program harus menangani error ini manual.
  4. Baris 19 (sigaction(SIGUSR1, &sa, NULL)): Memasang handler untuk SIGUSR1. Argumen ketiga NULL berarti abaikan handler lama. Bila kita lewatkan struct sigaction old_sa, kita bisa membaca konfigurasi sebelumnya dan mengembalikannya nanti—berguna untuk library yang ingin mengembalikan state setelah selesai.
  5. Output urutan: Sinyal diterima saat sleep sedang berjalan. Dengan SA_RESTART, sleep diinterupsi, handler dipanggil, lalu sleep di-restart hingga total 1 detik. hitung di main bertambah setelah sleep selesai.

Pola Self-Pipe: Pemrosesan Sinyal di Main Loop #

Keterbatasan utama signal handler adalah daftar fungsi async-signal-safe sangat terbatas—printf, malloc, pthread_mutex_lock semuanya tidak aman. Untuk aplikasi kompleks, pola self-pipe memindahkan semua kerja ke main loop:

  1. Buat pipe pipe_fd[2] di awal program.
  2. Handler hanya menulis satu byte ke pipe_fd[1].
  3. Main loop membaca dari pipe_fd[0] dan memproses sinyal dengan fungsi apa pun—tidak ada batasan async-signal-safety.
c
/* File: self_pipe.c - pola self-pipe untuk signal handling yang aman */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

static int pipe_fd[2];   /* pipe_fd[0]: baca, pipe_fd[1]: tulis */

static void handler(int sig) {
    /* Handler hanya menulis 1 byte - async-signal-safe */
    char byte = (char) sig;
    write(pipe_fd[1], &byte, 1);
}

int main(void) {
    if (pipe(pipe_fd) != 0) {
        perror("pipe");
        return 1;
    }

    /* Set read end non-blocking */
    int flags = fcntl(pipe_fd[0], F_GETFL);
    fcntl(pipe_fd[0], F_SETFL, flags | O_NONBLOCK);

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGUSR1, &sa, NULL);
    sigaction(SIGUSR2, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    printf("PID=%d - kirim sinyal: kill -USR1/USR2/TERM %d\n", getpid(), getpid());

    int total = 0;
    int berhenti = 0;
    while (!berhenti) {
        char buf[16];
        ssize_t n = read(pipe_fd[0], buf, sizeof(buf));
        if (n > 0) {
            for (ssize_t i = 0; i < n; i++) {
                const char *nama;
                switch ((int) buf[i]) {
                    case SIGUSR1: nama = "SIGUSR1"; break;
                    case SIGUSR2: nama = "SIGUSR2"; break;
                    case SIGTERM: nama = "SIGTERM"; break;
                    default:      nama = "UNKNOWN"; break;
                }
                printf("Diterima: %s\n", nama);
                total++;
                if ((int) buf[i] == SIGTERM) berhenti = 1;
            }
        } else if (n == -1 && errno == EAGAIN) {
            /* Tidak ada sinyal tertunda - lanjut */
        }
        sleep(1);
    }
    printf("Total sinyal diproses: %d\n", total);
    close(pipe_fd[0]);
    close(pipe_fd[1]);
    return 0;
}

Compile dan jalankan—kirim SIGUSR1, SIGUSR2, dan SIGTERM dari terminal lain:

bash
gcc -std=c11 -Wall -Wextra -o self_pipe self_pipe.c
./self_pipe &
PID=$!
sleep 1; kill -SIGUSR1 $PID
sleep 1; kill -SIGUSR2 $PID
sleep 1; kill -SIGTERM $PID
wait $PID

Output yang Dihasilkan:

text
PID=12968 - kirim sinyal: kill -USR1/USR2/TERM 12968
Diterima: SIGUSR1
Diterima: SIGUSR2
Diterima: SIGTERM
Total sinyal diproses: 3

Mari kita telaah baris-baris kode di atas:

  1. Baris 9-13 (handler): Handler sangat minimal—hanya menulis 1 byte ke pipe. write adalah async-signal-safe; tidak ada buffer internal, tidak ada alokasi memori. Inilah inti pola self-pipe: pekerjaan minimum di handler, semua kerja berat di main loop.
  2. Baris 21-23 (fcntl F_SETFL O_NONBLOCK): Set read-end pipe menjadi non-blocking. Tanpa ini, read akan memblokir bila tidak ada sinyal—main loop tidak bisa melakukan hal lain. Dengan non-blocking, read mengembalikan -1 dengan errno EAGAIN bila pipe kosong—main loop bisa lanjut bekerja.
  3. Baris 33-37 (sigaction x3): Memasang handler yang sama untuk SIGUSR1, SIGUSR2, SIGTERM. Karena handler menulis (char) sig, main loop bisa membedakan sinyal mana yang tiba.
  4. Baris 46 (read(pipe_fd[0], buf, sizeof(buf))): Membaca hingga 16 byte sekaligus. Bila banyak sinyal tertumpuk, satu read bisa mendapat beberapa byte—loop for memproses semuanya.
  5. Baris 53 (switch ((int) buf[i])): Di main loop, kita bebas memakai printf, malloc, atau fungsi apa pun. Tidak ada batasan async-signal-safety karena kode ini tidak berjalan di signal handler.