İçeriğe geç
academia.sh

Ders 03 / 15

Sinyaller

Bir sinyal isleyicisi ana akisin disinda, beklenmedik bir noktada calisir; guvenli tek paylasim bicimi volatile sig_atomic_t bir bayraktir, ve bir sinyal engellenmisken tekrar gelirse ac ldiginda yalniz bir kez teslim edilir.

Yayın Güncelleme

Bu derste

  • bir isleyicinin kaldırdığı bayrağın ana akışa ulaştığını, volatile sig_atomic_t üzerinden ölçer
  • engellenmiş bir sinyalin bekletilmiş durumunu sigpending ile doğrudan gözlemler ve maske kalkınca yalnız bir kez teslim edildiğini gösterir
  • isleyici içinde güvenli olmayan çağrı ve volatile olmayan paylaşımın neden tanımsız davranış olduğunu düzyazıyla açıklar
İçindekiler

fork bir kopya verdi, iki taraf birbirinden habersiz kaldı. Bu ders farklı bir taşıma biçimi ölçüyor: bir sinyal. Süreç normal akışını sürdürürken çekirdek onu durdurup kayıtlı bir işleyiciyi çalıştırır, işleyici döner, akış kaldığı yerden sürer. İşleyici ile ana akış aynı bellek alanını paylaşıyor, ama bu paylaşım forktaki gibi bir kopyalama değil — işleyicinin bıraktığı iz, ana akışa bir kez ulaşan bir aktarım. Kurstaki sınıflardan aktarım, tam olarak bunu tanımlıyor.

Bu ders raise(SIGUSR1) kullanıyor, kill ya da dış bir kaynak değil — ölçümün kendi kendine yeterli olması için. Gerçek kullanımda bir sinyal üç kaynaktan gelebilir: başka bir süreç kill çağrısıyla gönderir, çekirdek bir donanım durumuna (geçersiz bellek erişimi, aritmetik hata) tepki olarak üretir, ya da süreç kendi kendine raise ile gönderir. Üçü de aynı teslim mekanizmasını kullanır — isleyici hangi yoldan geldiğini ayırt edemez, yalnız hangi sinyal numarasının geldiğini bilir. SIGUSR1 seçimi de rastgele değil: POSIX bu ikiliyi (SIGUSR1, SIGUSR2) özel olarak uygulamaya bırakır, çekirdek onlara önceden tanımlı bir anlam yüklemez; bu yüzden bu ölçüm, SIGINT ya da SIGTERM gibi önceden tanımlı bir davranışı olan bir sinyalin varsayılan işleyicisini değiştirme riskine girmeden çalışabiliyor.

İşleyicinin Kaldırdığı Bayrak

Ölçüm birimleri aynı kalıyor. Bu dersin kendi dosyası sinyal_olcumu.c, üç ayrı soruyu ölçüyor: işleyicinin kaldırdığı bayrak ana akışa ulaşıyor mu, engellenmiş bir sinyal maskeliyken çekirdek onu bekletilmiş olarak görünür kılıyor mu, ve maske kalkınca kaç kez teslim ediliyor.

// gozlem.h — bir degisikligin kime ulastigini siniflayan kayit
#ifndef GOZLEM_H
#define GOZLEM_H

enum Sinif { KOPYA, ORTAK, AKTARIM, GOZLENMEYEN };

/* Bir degisikligi kaydeder: gordu, degisikligi yapmayan tarafin onu
   okuyup okuyamadigidir (1 ya da 0). */
void gozlem(const char *degisiklik, enum Sinif sinif, int gordu);

/* Sinif basina sayilari ve ulasan / ulasmayan toplamini basar. */
void gozlem_ozeti(void);

#endif
// gozlem.c — sayilar yalniz bu birimde tutulur
#include <stdio.h>
#include "gozlem.h"

static const char *const adlar[] = { "kopya", "ortak", "aktarim", "gozlenmeyen" };
static int sayilar[4];
static int ulasan = 0;
static int ulasmayan = 0;

void gozlem(const char *degisiklik, enum Sinif sinif, int gordu) {
    /* ortak ve aktarim ulasir, kopya ve gozlenmeyen ulasmaz */
    int beklenen = (sinif == ORTAK || sinif == AKTARIM);
    sayilar[sinif]++;
    if (gordu) {
        ulasan++;
    } else {
        ulasmayan++;
    }
    printf("%-34s %-8s %s%s\n", degisiklik, gordu ? "gordu" : "gormedi",
           adlar[sinif], gordu == beklenen ? "" : "  TUTARSIZ");
}

void gozlem_ozeti(void) {
    printf("siniflar: kopya %d, ortak %d, aktarim %d, gozlenmeyen %d\n",
           sayilar[KOPYA], sayilar[ORTAK], sayilar[AKTARIM], sayilar[GOZLENMEYEN]);
    printf("ulasan %d, ulasmayan %d\n", ulasan, ulasmayan);
}
// sinyal_olcumu.c — isleyicinin kaldirdigi bayrak ve engellenmis teslim sayisi
#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include "gozlem.h"

static volatile sig_atomic_t bayrak = 0;
static volatile sig_atomic_t teslim_sayisi = 0;

static void isleyici(int s) {
    (void)s;
    bayrak = 1;
    teslim_sayisi++;
}

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = isleyici;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    /* birinci olcum: isleyicinin kaldirdigi bayrak ana akisa ulasiyor mu */
    raise(SIGUSR1);
    gozlem("isleyicinin kaldirdigi bayrak", AKTARIM, bayrak == 1);

    /* ikinci olcum: engellenmisken iki kez gelen sinyal, acilinca bir kez teslim edilir */
    sigset_t kume;
    sigemptyset(&kume);
    sigaddset(&kume, SIGUSR1);
    sigprocmask(SIG_BLOCK, &kume, NULL);

    teslim_sayisi = 0;
    raise(SIGUSR1);
    raise(SIGUSR1);
    int engelliyken = (teslim_sayisi == 0);

    /* ucuncu olcum: maske altindayken sinyal bekletilmis olarak goruluyor mu */
    sigset_t bekleyenler;
    sigpending(&bekleyenler);
    int bekliyor = sigismember(&bekleyenler, SIGUSR1);
    gozlem("maskeliyken sinyalin bekletilmis gorunmesi", AKTARIM,
           engelliyken && bekliyor == 1);

    sigprocmask(SIG_UNBLOCK, &kume, NULL);
    gozlem("acilista teslim edilen sinyal sayisi", AKTARIM,
           engelliyken && teslim_sayisi == 1);

    gozlem_ozeti();
    return 0;
}
isleyicinin kaldirdigi bayrak      gordu    aktarim
maskeliyken sinyalin bekletilmis gorunmesi gordu    aktarim
acilista teslim edilen sinyal sayisi gordu    aktarim
siniflar: kopya 0, ortak 0, aktarim 3, gozlenmeyen 0
ulasan 3, ulasmayan 0

SA7. Birinci ölçüm dosdoğru: raise(SIGUSR1) çekirdeğe süreci durdurmasını ve isleyiciyi çalıştırmasını söylüyor, isleyici bayrakı 1 yapıyor, raise döndükten sonra ana akış bu bayrağı okuyor ve 1 buluyor. Bayrak volatile sig_atomic_t türünde — bu tür seçimi rastgele değil, bir kuralın karşılığı: derleyici bu türdeki bir değişkeni her okumada bellekten yeniden okumak, ve okuma ya da yazmayı yarım bırakmamak zorunda. İşleyici ana akıştan bağımsız, beklenmedik bir noktada araya giriyor; volatile olmayan bir değişken derleyici tarafından bir yazmaçta tutulabilir, ve işleyicinin yaptığı değişiklik ana akışın gördüğü kopyaya hiç yansımayabilir.

Maske Altında Bekletme

SA8. İkinci ölçüm doğrudan bekletilmiş durumu gözlemliyor. sigprocmask(SIG_BLOCK, ...) çağrısından sonra gönderilen bir sinyal hemen teslim edilmez; çekirdek onu bekletilmiş olarak tutar, ama bu durum önceki ölçümdeki gibi dolaylı biçimde (bir sayacın artmaması üzerinden) değil, sigpending ile doğrudan okunabilir: sigpending(&bekleyenler) çağrısı, çağıran sürecin engellenmiş, henüz teslim edilmemiş sinyallerinin kümesini doldurur, ve sigismember(&bekleyenler, SIGUSR1) bu kümede SIGUSR1in olup olmadığını sorar. İki raise çağrısından sonra, teslim gerçekleşmeden önce bu sorgu 1 döner — çekirdek sinyali kaybetmemiş, işlememiş, bir yerde tutuyor. Bu, “bekletme” sözcüğünün düzyazı bir tasviri değil, çalıştırılabilir bir ölçüm: sinyal_olcumu.c maskenin altında gerçekten bir durumun var olduğunu, o durumun süreç tarafından sorgulanabilir olduğunu gösteriyor.

SA9. Üçüncü ölçüm bu bekletilmiş durumun açılışta ne yaptığını sayıyor. Aynı sinyal engellenmişken iki kez raise edilse bile, çoğu standart sinyal için bekletilmiş durum tekildir — teslim edilecek kopya sayısı değil, yalnız “bekleyen var mı” bilgisi tutulur. Maske kalktığında (SIG_UNBLOCK) çekirdek bekletilmiş sinyali teslim eder, ama yalnız bir kez: teslim_sayisi engelli süre boyunca 0 kalıyor, açılışta 1e çıkıyor, 2ye değil. sigpending ölçümüyle birlikte okununca bu iki bulgu tek bir resim veriyor: engelliyken kümede bir bit bilgi var — “bekleyen var” ya da “yok” — ve bu bit, kaç kez raise çağrıldığından bağımsız. Bit açılışta bir teslime dönüşüyor, sayaç değeri kadar teslime değil.

sigactionın kendisi de aynı tekillik kuralını, işleyici çalışırken kullanıyor. sa.sa_mask boş küme olarak ayarlandı, ama çekirdek yine de her işleyici çağrısı sırasında o sinyalin kendisini otomatik olarak engeller — SA_NODEFER bayrağı açıkça istenmedikçe, bir işleyici kendi çalıştığı sinyalin ikinci bir kopyasıyla yeniden girilmez. Bu, sinyal_olcumu.c’nin ölçtüğü “maske altında bekletme”nin küçük bir örneği: işleyici içindeyken gelen aynı sinyal kaybolmuyor, işleyici dönene kadar bekletiliyor, sonra tek seferde teslim ediliyor. sa_mask alanı bu otomatik engellemeyi genişletmek için var — bir işleyici birden fazla sinyal türüyle ilgileniyorsa, aralarındaki yarışı önlemek için birbirlerini de bu alana eklemek gerekir; bu ders tek sinyalle çalıştığı için sigemptyset ile boş bırakıyor.

Engellemenin bir başka kullanımı, kontrol-ve-eylem yarışını önlemektir. Bir program bir bayrağı okuyup ona göre bir işlem yapacaksa, okuma ile işlem arasına bir sinyal girip bayrağı değiştirebilir; bu ders bunu sınamıyor, ama örüntü kısaca şudur: ilgili sinyal sigprocmask(SIG_BLOCK, ...) ile engellenir, bayrak okunur ve karar verilir, sonra maske geri açılır. Böylece “oku ve karar ver” adımı sinyalin arasına girmesine kapalı bir pencere içinde kalır — sinyal_olcumu.c’deki sigpending ölçümü, bu pencerenin içinde bekletilmiş bir sinyalin gerçekten orada durduğunu, kaybolmadığını doğrulayan aynı mekanizmaya dayanıyor.

İşleyici İçinde Tanımsız Davranış

SA10. isleyici işlevi yalnız iki satır: bir bayrağı kaldırmak ve bir sayacı artırmak. Bu kısalık kural gereği, tercih değil. Bir sinyal işleyicisi ana akışın herhangi bir noktasında araya girebilir — bir malloc çağrısının ortasında, bir kilidi tutan bir kod parçasının içinde, hatta printfin kendi iç tamponunu güncellediği bir anda. İşleyici içinde eşzamansız-sinyal-güvenli olmayan bir çağrı yapmak — malloc, printf, kilitli bir kaynağa erişen herhangi bir işlev — kesintiye uğrattığı kodla çakışabilir; bu tanımsız davranıştır ve bu ders böyle bir çağrıyı koşturmuyor, yalnız burada tarif ediyor. Aynı şekilde, işleyici ile ana akış arasında volatile olmayan bir değişken paylaşmak da tanımsız davranıştır: derleyici bu değişkenin işleyici tarafından değiştirilebileceğini bilmediği için, ana akıştaki okumaları yeniden düzenleyebilir ya da tamamen atlayabilir. sinyal_olcumu.c bu yüzden yalnız sig_atomic_t türünde, volatile işaretli değişkenlere dokunuyor, ve işleyici içinde hiçbir işlev çağırmıyor. Bu kısıt yalnız “iyi bir alışkanlık” değil; kesintiye uğrayan çağrı bir kütüphanenin dahili kilidini ya da statik tamponunu tam ortada tutarken işleyici aynı kilidi ya da tamponu tekrar kullanmaya kalkarsa sonuç kilitlenme, bozulmuş veri ya da rastgele bir çökme olabilir — derleyici ya da çalışma zamanı burada hiçbir hata bildirmez, davranış tanım dışına çıkar.

POSIX, işleyici içinde çağrılması güvenli olan küçük bir işlev kümesi tanımlar — write, _exit, sigprocmask, bu dersin kullandığı sigpending gibi doğrudan sistem çağrıları bu listede, ama tamponlu giriş/çıkış (printf, fopen), bellek ayırma (malloc, free) ve çoğu kütüphane işlevi listede değildir. Bu ayrımın nedeni tamponlama ve dahili durum: printf kendi iç tamponunu ve olası bir kilidini günceller, bu güncelleme yarım kalmışken aynı işlevin işleyici içinde tekrar çağrılması o dahili durumu tutarsız bırakabilir. sinyal_olcumu.c yalnız bayrak = 1 ve teslim_sayisi++ yapıyor — ikisi de doğrudan bellek yazması, hiçbir dahili durum ya da kilit içermiyor, bu yüzden işleyici içinde güvenle çalışıyor.

Bu ayrım, önceki kursun sorduğu “adımı hangi iş parçacığı koşturuyor” sorusundan farklı bir eksen: bir sinyal işleyicisi ayrı bir iş parçacığı değildir, aynı akışın beklenmedik bir noktada kesilmesidir. Kesintiye uğrayan kod ile işleyici aynı yığıtı ve aynı kayıt kümesini geçici olarak paylaşır — bu yüzden işleyicinin bıraktığı iz, forkun verdiği bir kopya değil, aynı akışa dönen bir aktarımdır.

Bu paylaşım çok iş parçacıklı bir süreçte daha da daralır: POSIX’te bir sinyal işleyicisi süreç düzeyinde kaydedilir, ama hangi iş parçacığının onu çalıştıracağı teslime bağlıdır — bir iş parçacığının kendine yönelttiği bir sinyal (pthread_kill ile) o iş parçacığında çalışır, sürece yönelen bir sinyal ise maskelenmemiş herhangi bir iş parçacığında çalışabilir. Bu ders tek iş parçacıklı çalıştığı için bu belirsizlik ortaya çıkmıyor; sonraki ders birden çok iş parçacığını tanıttığında, sinyal işleyicisinin hangi akışta çalışacağı sorusu da o dersin kapsamı dışında kalan, ama bilinmesi gereken bir sınır olarak duruyor.

SA11. M08/K04 dersi Java’da volatilei çok iş parçacıklı bir bellek modeli içinde, görünürlük ve sıralama garantisi veren bir anahtar sözcük olarak ele alıyordu: bir iş parçacığının yazdığı değeri başka bir iş parçacığının doğru sırada görmesini sağlayan, çalışma zamanı ve donanım arasında koordine edilmiş bir sözleşme. Buradaki volatile sig_atomic_tnin verdiği garanti çok daha dar: birden çok çekirdek ya da iş parçacığı arasında bir görünürlük sırası kurmuyor, yalnız aynı akışın, bir sinyal işleyicisi tarafından kesintiye uğratıldığında, derleyicinin o değişkeni yazmaçta tutmasını ya da okumasını yeniden sıralamasını engelliyor. C standardı bu türü çok işlemcili bellek sıralaması için tanımlamıyor; POSIX bunu yalnız aynı süreç içindeki senkron olmayan bir kesinti için garanti ediyor. İki dilin aynı anahtar sözcüğü, iki farklı sorunu çözüyor: biri tek akışlı bir kesinti sınırını, öbürü çok çekirdekli bir donanımın bellek tutarlılık sınırını aşıyor.

Sınırlayıcı Ölçüm

Bu ölçüm standart sinyalleri saymaz. POSIX standart sinyalleri için bekletilmiş durum bir bayraktır, bir sayaç değildir: aynı sinyal engelliyken kaç kez gönderilirse gönderilsin, açılışta yalnız bir teslim gerçekleşir. sinyal_olcumu.c bunu tam olarak gösteriyor — iki raise çağrısına karşılık teslim_sayisi yalnız 1e çıkıyor. Gerçek zamanlı sinyaller (SIGRTMIN ve üzeri) farklı bir kuraldadır ve birikerek kuyruğa alınabilir, ama bu ders standart sinyallerle sınırlı ve o farkı konu edinmiyor.

Bu sınır, ölçümün neyi doğrulayabileceğini de belirliyor: sinyal_olcumu.c, engellenmiş süre boyunca kaç kez raise çağrıldığını değil, yalnız açılıştan sonra kaç kez teslim edildiğini sayıyor. İki raise çağrısı tek bir teslime düşse bile, bu bir kayıp değil — çekirdeğin standart sinyaller için verdiği garanti zaten “en az bir kez”, “her çağrı için bir kez” değil. Ders bu garantiyi aşan bir sayım yapmıyor.

Bu ders ayrıca signal() işlevini değil, sigactionı kullanıyor; bu bir stil tercihi değil. Eski signal() arayüzünün davranışı sistemler arasında farklılaşır — bazı eski Unix türevlerinde işleyici çağrıldıktan sonra tekrar varsayılana döner, bazılarında dönmez — ve bu belirsizlik yüzünden taşınabilir kod yazmak ister. sigaction, işleyicinin kalıcılığını, hangi ek sinyallerin işleyici sırasında engelleneceğini (sa_mask) ve kesintiye uğrayan sistem çağrılarının yeniden başlatılıp başlatılmayacağını (SA_RESTART) açıkça belirtme imkânı veriyor. Bu ders SA_RESTARTı ayarlamıyor; bu, engelleyici bir sistem çağrısı sinyal tarafından kesintiye uğrarsa çağrının EINTR hatasıyla dönebileceği anlamına gelir, ama sinyal_olcumu.c böyle bir çağrı içermediği için bu davranış bu derste gözlenmiyor — yalnız ileride karşılaşılacak bir sınır olarak not düşülüyor.

Özet

  • Bir sinyal işleyicisi ana akışın dışında, beklenmedik bir noktada çalışır; bıraktığı iz ana akışa aynı süreç içinde geri döner.
  • volatile sig_atomic_t, işleyici ile ana akış arasında güvenli tek paylaşım biçimidir; derleyicinin yeniden sıralamasını ve yarım okuma-yazmayı engeller.
  • Bu dersin sınıfı aktarım: işleyicinin kaldırdığı bayrak ana akışa ulaşıyor, ama bir kayıt gibi sürekli paylaşılmıyor — bir kez taşınıyor.
  • sigpending ile bekletilmiş durum doğrudan gözlemlenebilir: maskeliyken kümede tutulan bilgi tek bir bit, “bekleyen var mı” — kaç kez gönderildiği değil; bu yüzden engellenmiş bir sinyal birden çok kez gönderilse bile teslim maske kalkınca yalnız bir kez gerçekleşir.
  • İşleyici içinde eşzamansız-sinyal-güvenli olmayan bir çağrı ya da volatile olmayan bir paylaşım tanımsız davranıştır; bu ders ikisini de koşturmuyor.
  • C’deki volatile sig_atomic_t ile Java’daki volatile aynı sözcüğü taşır ama farklı sorunları çözer: biri tek akışlı kesinti güvenliği, öbürü çok iş parçacıklı görünürlük garantisidir.

Sonraki Adım

Bir sonraki ders aktarım sınıfını sürdürüyor, ama farklı bir kanaldan: bir borunun taşıdığı baytlar, çocuktan ebeveyne bir kez akan ve ikinci okumada dosya sonuyla biten bir aktarım. Ortak nokta şu: sinyalde taşınan iz tek bir bit ya da tek bir sayaç artışıydı, boruda taşınan iz ise değişken uzunlukta bir bayt dizisi olacak — ikisi de aynı “bir kez, geri dönüşsüz” aktarım kuralına uyuyor, ama taşıdıkları bilginin biçimi ve miktarı kökten farklı.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat