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.
/* 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:
gcc -std=c11 -Wall -Wextra -o signal_sederhana signal_sederhana.c
./signal_sederhana &
PID=$!; sleep 2; kill -SIGINT $PID; wait $PIDOutput yang Dihasilkan:
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:
- Baris 7 (
static volatile sig_atomic_t stop = 0;): Variabel flag yang diperiksa di loop utama dan ditulis di handler.volatilemencegah compiler meng-cache nilai di register (loop harus membaca ulang dari memori setiap iterasi).sig_atomic_tmenjamin bahwa penulisan ke variabel ini atomic di tingkat mesin—penting karena handler bisa dipanggil di tengah instruksi. - Baris 11-13 (
write(STDOUT_FILENO, ...)): Mengapawrite, bukanprintf? Karenaprintftidak async-signal-safe—ia menggunakan buffer internal yang bisa rusak bila handler dipanggil saatprintfsedang berjalan di main.writelangsung memanggil kernel tanpa buffer. - Baris 17 (
signal(SIGINT, handler_sigint)): Memasang handler. Saat SIGINT tiba, kernel menjalankanhandler_sigintalih-alih membunuh proses (default action SIGINT adalah terminate). - Baris 24 (
while (!stop)): Loop memeriksa flagstop. Saat handler setstop = 1, loop keluar di iterasi berikutnya dan program berakhir dengan rapi. sleep(1)di loop: Memberi kesempatan scheduler untuk mengeksekusi handler saat sinyal tiba. Tanpa sleep, loop yang ketat mungkin tidak sempat membacastopyang 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:
/* 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:
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 $PIDOutput yang Dihasilkan:
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: 2Mari kita telaah baris-baris kode di atas:
- Baris 14-18 (
struct sigaction sa;): Struktur POSIX standar. Fieldsa_handlermenunjuk fungsi handler. Fieldsa_maskadalah signal set yang akan diblokir selama handler berjalan—SIGUSR1 tidak bisa menginterupsi dirinya sendiri. - Baris 16 (
sigemptyset(&sa.sa_mask);): Mengosongkansa_maskterlebih dahulu. Tanpasigemptyset, struktur yang tidak di-init (memset 0) bisa berisi bit acak. - Baris 17 (
sa.sa_flags = SA_RESTART;): Saat handler menginterupsi syscall (misalnyasleep), syscall otomatis di-restart setelah handler selesai. TanpaSA_RESTART, syscall kembali dengan errorEINTR—program harus menangani error ini manual. - Baris 19 (
sigaction(SIGUSR1, &sa, NULL)): Memasang handler untuk SIGUSR1. Argumen ketigaNULLberarti abaikan handler lama. Bila kita lewatkanstruct sigaction old_sa, kita bisa membaca konfigurasi sebelumnya dan mengembalikannya nanti—berguna untuk library yang ingin mengembalikan state setelah selesai. - Output urutan: Sinyal diterima saat
sleepsedang berjalan. DenganSA_RESTART,sleepdiinterupsi, handler dipanggil, lalusleepdi-restart hingga total 1 detik.hitungdi main bertambah setelahsleepselesai.
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:
- Buat pipe
pipe_fd[2]di awal program. - Handler hanya menulis satu byte ke
pipe_fd[1]. - Main loop membaca dari
pipe_fd[0]dan memproses sinyal dengan fungsi apa pun—tidak ada batasan async-signal-safety.
/* 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:
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 $PIDOutput yang Dihasilkan:
PID=12968 - kirim sinyal: kill -USR1/USR2/TERM 12968
Diterima: SIGUSR1
Diterima: SIGUSR2
Diterima: SIGTERM
Total sinyal diproses: 3Mari kita telaah baris-baris kode di atas:
- Baris 9-13 (
handler): Handler sangat minimal—hanya menulis 1 byte ke pipe.writeadalah 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. - Baris 21-23 (
fcntl F_SETFL O_NONBLOCK): Set read-end pipe menjadi non-blocking. Tanpa ini,readakan memblokir bila tidak ada sinyal—main loop tidak bisa melakukan hal lain. Dengan non-blocking,readmengembalikan-1dengan errnoEAGAINbila pipe kosong—main loop bisa lanjut bekerja. - Baris 33-37 (
sigactionx3): Memasang handler yang sama untuk SIGUSR1, SIGUSR2, SIGTERM. Karena handler menulis(char) sig, main loop bisa membedakan sinyal mana yang tiba. - Baris 46 (
read(pipe_fd[0], buf, sizeof(buf))): Membaca hingga 16 byte sekaligus. Bila banyak sinyal tertumpuk, satureadbisa mendapat beberapa byte—loopformemproses semuanya. - Baris 53 (
switch ((int) buf[i])): Di main loop, kita bebas memakaiprintf,malloc, atau fungsi apa pun. Tidak ada batasan async-signal-safety karena kode ini tidak berjalan di signal handler.