Sinyal POSIX konvensional dapat menginterupsi thread pada hampir semua batas instruksi lalu memindahkan kontrol ke signal handler. Alur kontrol asinkron tersebut membatasi operasi yang aman di dalam handler dan menambah kerumitan pada program yang control plane utamanya sudah berjalan melalui epoll, poll, atau select.

Linux signalfd() menyediakan antarmuka pengiriman yang berbeda. Proses memblokir sinyal tertentu melalui signal mask biasa, membuat signalfd untuk himpunan tersebut, lalu menerima sinyal pending dengan membaca record signalfd_siginfo terstruktur dari descriptor. Pada antarmuka kernel, objek tersebut tetap berupa sinyal; hanya proses konsumsinya yang dipindahkan ke I/O file descriptor biasa.

Pemblokiran menjadi bagian dari kontrak pengiriman

Membuat signalfd tidak otomatis mencegah sinyal yang sama mencapai handler tradisional. Sinyal target harus diblokir, biasanya dengan sigprocmask() pada proses single-threaded atau pthread_sigmask() pada proses multithreaded.

sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGHUP);

pthread_sigmask(SIG_BLOCK, &mask, NULL);

int sfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);

Pemblokiran mengubah titik tempat sinyal dikonsumsi. Sinyal pending yang cocok kemudian dapat dikembalikan oleh read() pada signalfd alih-alih menjalankan handler untuk sinyal yang diblokir tersebut.

Mask pada thread memerlukan perhatian khusus. Signal mask bersifat per-thread, dan thread baru mewarisi salinan mask milik thread pembuatnya. Program yang memusatkan konsumsi sinyal karena itu umumnya menetapkan mask sebelum membuat worker thread.

Setiap read mengembalikan metadata sinyal terstruktur

Descriptor mengembalikan satu atau beberapa record struct signalfd_siginfo. Operasi read yang berhasil harus memakai buffer yang cukup besar untuk setidaknya satu record lengkap.

struct signalfd_siginfo si;

ssize_t n = read(sfd, &si, sizeof(si));
if (n == sizeof(si)) {
    if (si.ssi_signo == SIGTERM) {
        /* begin shutdown */
    }
}

Field dapat mencakup nomor sinyal, PID dan UID pengirim, data integer atau pointer yang diantrekan, informasi timer, serta metadata lain yang relevansinya bergantung pada sumber sinyal. Mekanisme ini menghindari pemindahan state melalui variabel global hanya untuk berkomunikasi dari handler asinkron ke main loop.

Descriptor bersifat konsumtif: membaca sinyal pending menghapus instance sinyal tersebut dari himpunan pending yang direpresentasikan oleh operasi signalfd. Instance pending yang sama tidak tersedia lagi untuk konsumsi sinkron terpisah setelahnya.

Sinyal standar dan real-time mempertahankan semantik antrean yang berbeda

signalfd() tidak mengubah sinyal standar menjadi message channel dengan antrean penuh. Sinyal standar dapat mengalami coalescing saat diblokir: beberapa kemunculan sinyal standar yang sama dapat direpresentasikan oleh satu instance pending.

Sinyal real-time memiliki semantik antrean dan aturan urutan yang ditetapkan untuk pengiriman sinyal real-time POSIX. signalfd() mempertahankan semantik sinyal dasarnya, bukan menggantinya dengan aturan antrean khusus descriptor.

Perbedaan ini penting ketika aplikasi memperlakukan descriptor sebagai sumber event. Sinyal standar seperti SIGHUP sesuai untuk notifikasi perubahan state ketika beberapa kedatangan boleh menyatu. Sinyal tersebut bukan counter andal untuk setiap kemunculan.

epoll dapat memperlakukan sinyal sebagai sumber readiness lain

Signalfd bersifat pollable. Saat sinyal yang cocok berada dalam status pending, mekanisme readiness dapat melaporkan descriptor sebagai readable.

int ep = epoll_create1(EPOLL_CLOEXEC);

struct epoll_event ev = {
    .events = EPOLLIN,
    .data.fd = sfd,
};

epoll_ctl(ep, EPOLL_CTL_ADD, sfd, &ev);

Dengan mekanisme ini, satu event loop dapat mengoordinasikan socket, pipe, timer, dan sinyal tanpa handler asinkron yang menulis ke self-pipe. Descriptor tidak membuat pembangkitan sinyal menjadi sinkron; descriptor membuat konsumsinya kompatibel dengan model readiness file descriptor.

SFD_NONBLOCK berguna saat event loop menguras record yang tersedia sampai read() mengembalikan EAGAIN. SFD_CLOEXEC mencegah pewarisan descriptor secara tidak sengaja melewati execve() kecuali pewarisan memang diperlukan.

Mask pada descriptor dapat diganti

Pemanggilan signalfd() dengan descriptor signalfd yang sudah ada memperbarui himpunan sinyal yang terkait dengan descriptor tersebut.

sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGINT);

if (signalfd(sfd, &mask, SFD_CLOEXEC | SFD_NONBLOCK) == -1) {
    /* handle error */
}

Memperbarui mask descriptor dan memperbarui signal mask thread merupakan dua operasi terpisah. Perubahan pada salah satunya tidak otomatis menyinkronkan yang lain. Sinyal yang dikeluarkan dari himpunan signalfd tetapi tetap diblokir dapat bertahan sebagai pending tanpa jalur read yang cocok melalui descriptor tersebut.

Sinyal process-directed tetap berinteraksi dengan pemilihan thread

Signalfd tidak menghapus perbedaan POSIX antara sinyal process-directed dan thread-directed. Untuk sinyal process-directed, kernel dapat memilih thread yang memenuhi syarat berdasarkan aturan pengiriman sinyal normal. Konsumsi terpusat bekerja secara konsisten ketika sinyal terkait diblokir pada semua thread yang tidak boleh menerimanya secara asinkron.

Sinyal thread-directed juga mempertahankan semantik targetnya. Read pada signalfd tidak dapat diperlakukan sebagai mailbox universal untuk sinyal yang diarahkan ke thread lain.

Batas ini penting untuk desain yang kuat: signalfd() mengubah antarmuka yang digunakan untuk menerima sinyal pending tertentu, tetapi tidak menggantikan model sinyal pada proses dan thread.

Pengiriman berbasis descriptor mempersempit kode asinkron

Nilai praktis signalfd() terletak pada arsitektur. Kebijakan sinyal dapat tetap berada di dalam state machine yang sama dengan bagian lain dari layanan event-driven. Transisi shutdown, reload, dan pengelolaan child process dapat berjalan dalam alur kontrol biasa, bukan di dalam konteks async-signal.

Model tersebut tetap bergantung pada masking yang benar, state thread yang diwariskan, dan semantik antrean tiap kelas sinyal. Dengan batas-batas itu dinyatakan secara eksplisit, signalfd() memindahkan konsumsi sinyal Linux tertentu ke jalur I/O pollable biasa tanpa mengubah sinyal itu sendiri.