setjmp.h dan signal.h

Lompatan non-lokal dan penanganan signal pada tingkat pengenalan.

Daftar isi

Normalnya alur program C bergerak lewat pemanggilan fungsi dan nilai kembali, tetapi dua header ini menyediakan penyimpangan dari pola itu. Header setjmp.h memungkinkan eksekusi melompat kembali ke titik yang telah ditandai sebelumnya, sedangkan signal.h menangani sinyal (signal), yaitu pemberitahuan asinkron dari sistem operasi seperti penekanan Ctrl+C. Keduanya termasuk alat yang jarang dibutuhkan sehari-hari dan dipakai paling aman dalam bentuk yang sederhana, sehingga pembahasannya cukup sampai tingkat pengenalan beserta batas-batasnya.

Lompatan non-lokal dengan setjmp dan longjmp#

Fungsi setjmp menandai satu titik pada kode dan menyimpan lingkungan eksekusinya ke dalam objek bertipe jmp_buf. Pemanggilan pertama mengembalikan 0. Apabila longjmp(mem, nilai) dipanggil dari fungsi mana pun di bawahnya, eksekusi melompat kembali ke titik setjmp tersebut seolah setjmp baru saja kembali. Kali ini nilai kembinya adalah nilai yang diberikan longjmp. Perpindahan semacam ini dikenal sebagai lompatan non-lokal (non-local jump) karena melewati banyak tingkat pemanggilan sekaligus:

c lompat-non-lokal.c
#include <setjmp.h>
#include <stdio.h>

jmp_buf titik_kembali;

void proses_dalam(int level)
{
    printf("proses_dalam: level %d tidak memenuhi syarat\n", level);
    longjmp(titik_kembali, 1);
    printf("baris ini tidak pernah tercapai\n");
}

int main(void)
{
    if (setjmp(titik_kembali) == 0) {
        printf("main: jalur normal dimulai\n");
        proses_dalam(5);
        printf("main: baris ini tidak pernah tercapai\n");
    } else {
        printf("main: eksekusi berlanjut dari titik lompatan\n");
    }
    return 0;
}
text Keluaran
main: jalur normal dimulai
proses_dalam: level 5 tidak memenuhi syarat
main: eksekusi berlanjut dari titik lompatan

Fungsi proses_dalam tidak pernah kembali ke pemanggilnya secara normal, melainkan melompat langsung ke setjmp pada main. Pola ini berguna ketika error terjadi sangat dalam di tumpukan pemanggilan dan setiap tingkat menengah tidak punya sesuatu yang bermakna untuk dilakukan. Contohnya pemeriksa sintaks yang lebih baik berhenti sekaligus daripada meneruskan hasil error dari satu fungsi ke fungsi berikutnya. Nilai kedua longjmp dipakai untuk membedakan titik lompatan yang berbeda apabila satu jmp_buf dipakai bersama.

Nilai kembali setjmp yang berbeda-beda memungkinkan satu fungsi menangani beberapa jenis lompatan sekaligus, misalnya 1 untuk error sintaks dan 2 untuk kehabisan memori. Pemanggilan setjmp boleh dilakukan lebih dari satu kali pada fungsi yang sama, selama setiap jmp_buf-nya terpisah. Setiap longjmp menimpa keadaan lama pada jmp_buf yang ditujunya, sehingga lompatan berikutnya selalu menggantikan keadaan lompatan sebelumnya.

Batasan setjmp dan longjmp#

Lompatan non-lokal memiliki batasan yang nyaris tidak ada padanannya pada mekanisme kontrol lain. Variabel lokal biasa pada fungsi yang memuat setjmp berisi nilai tidak pasti (indeterminate) setelah lompatan, kecuali dideklarasikan volatile. Penyebabnya, lingkungan yang disimpan jmp_buf tidak menjamin keadaan register maupun memori lokalnya. Satu jmp_buf juga hanya sah selama fungsi pemilik setjmp belum kembali, dan longjmp menuju jmp_buf yang sudah usang merupakan perilaku tak terdefinisi:

c Contoh: variabel yang perlu volatile
// tidak-dijalankan
int main(void)
{
    jmp_buf konteks;
    volatile int sudah_dibersihkan = 0;   /* wajib volatile agar sah setelah lompatan */
    int sementara = 0;                    /* tidak pasti nilainya setelah lompatan */

    if (setjmp(konteks) == 0) {
        kerja_berat();
    } else if (sudah_dibersihkan == 0) {
        sudah_dibersihkan = 1;
        bebaskan_sumber_daya();
        longjmp(konteks, 2);
    }
    return 0;
}

Batasan lain menyangkut sumber daya. Apabila longjmp melewati titik pembebasan memori atau penutupan berkas, sumber daya itu tertinggal tanpa pernah dilepas, karena tidak ada mekanisme pembersihan otomatis sepanjang jalur lompatan. Karena itu pola yang lazim adalah mengumpulkan keadaan pembersihan pada satu tempat yang dituju longjmp, sebagaimana pada contoh di atas, alih-alih membiarkan tiap tingkat fungsi membersihkan dirinya sendiri.

Kompiler juga bebas menganggap jalur yang berakhir longjmp tidak pernah kembali, sehingga pengoptimalan pada fungsi pemilik setjmp dapat menghasilkan kode yang berbeda dari tulisan aslinya. Karena itu bagian fungsi yang memanggil setjmp sebaiknya dibuat sesederhana mungkin, dan perhitungan penting ditempatkan di fungsi terpisah. Pola ini sekaligus membatasi jumlah variabel yang terancam kehilangan nilainya setelah lompatan.

Signal: pemberitahuan asinkron dari sistem#

Sinyal (signal) adalah pemberitahuan dari sistem operasi bahwa sesuatu terjadi. Contohnya penekanan Ctrl+C yang dikirim sebagai SIGINT, permintaan penghentian sebagai SIGTERM, pelanggaran akses memori sebagai SIGSEGV, dan kesalahan aritmetika seperti pembagian nol bilangan bulat sebagai SIGFPE. Setiap sinyal punya penangan bawaan (default), dan mayoritas menghentikan program. Fungsi signal(nomor, handler) memasang fungsi penangan (handler) sendiri untuk satu nomor sinyal, dengan bentuk fungsi penangan yaitu void f(int):

c tangkap-signal.c
#include <signal.h>
#include <stdio.h>

volatile sig_atomic_t sinyal_masuk = 0;

void tangkap_int(int nomor)
{
    (void)nomor;
    sinyal_masuk = 1;   /* handler hanya menandai, kerja utama di main */
}

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

    printf("Program berjalan dan memasang handler SIGINT.\n");
    raise(SIGINT);

    while (sinyal_masuk == 0) {
        /* menunggu handler mengisi tanda */
    }
    printf("Handler menandai kedatangan sinyal pada variabel tanda.\n");
    return 0;
}
text Keluaran
Program berjalan dan memasang handler SIGINT.
Handler menandai kedatangan sinyal pada variabel tanda.

Program ini memasang handler untuk SIGINT, lalu mengirim sinyal itu kepada dirinya sendiri lewat raise sehingga contohnya tetap dapat dijalankan tanpa terminal terpisah. Handler tidak mencetak apa pun dan tidak menyentuh data lain, melainkan hanya mengisi satu variabel bertipe volatile sig_atomic_t. Tipe itulah satu-satunya jenis data yang dijamin aman dibagi antara handler dan program utama. Pola ini dikenal sebagai flag pattern, yaitu handler hanya menandai kedatangan sinyal dan seluruh pekerjaan pemrosesannya dikerjakan program utama.

Pada pemakaian nyata, sinyal datang dari luar program, misalnya ketika pengguna menekan Ctrl+C pada terminal. Cara mengujinya adalah menjalankan program di satu terminal, menekan kombinasi tombol tersebut, lalu mengamati pesan berhentinya. Contoh perintahnya tidak dapat diverifikasi di sandbox ini karena membutuhkan proses yang berjalan lama dan interaksi terminal, sehingga cukup dicatat bentuknya:

bash
# tidak-dijalankan
./proses-lama
^C   # Ctrl+C mengirim SIGINT kepada proses latar depan

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul pada pemakaian pertama kedua header ini.

  • Membaca variabel lokal biasa setelah lompatan. Nilainya tidak dijamin tetap, dan programnya bisa berperilaku berbeda antar-eksekusi. Deklarasikan variabel yang dibaca setelah lompatan sebagai volatile, atau pindahkan nilainya ke objek statis.
  • Memanggil fungsi pustaka dari dalam handler. printf, malloc, dan sebagian besar fungsi lain tidak terdaftar aman untuk sinyal, sehingga handler yang mencetak bisa terkunci (deadlock) justru saat error terjadi. Gunakan pola penandaan dengan sig_atomic_t.
  • Menganggap longjmp pengganti pembersihan sumber daya. Lompatan melewati baris free dan fclose tanpa menggantinya, sehingga memori dan berkas tertinggal. Rancang titik lompatan agar melewati tempat pembersihan dilakukan secara eksplisit.

Ringkasan#

  • setjmp menandai titik lompatan dan menyimpan lingkungan eksekusinya ke jmp_buf, sedangkan longjmp mengembalikan eksekusi ke titik itu dengan nilai pembeda.
  • Variabel lokal pada fungsi pemilik setjmp tidak pasti setelah lompatan kecuali dinyatakan volatile, dan jmp_buf menjadi usang setelah fungsinya kembali.
  • Sinyal adalah pemberitahuan asinkron dari sistem operasi, dengan SIGINT untuk Ctrl+C, SIGTERM untuk permintaan berhenti, dan SIGSEGV untuk pelanggaran akses memori.
  • Handler sinyal yang aman hanya mengisi variabel volatile sig_atomic_t, sedangkan pekerjaan utamanya dikerjakan program utama setelah tanda terbaca.

Referensi#