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.
sigpendingile 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
volatileolmayan bir paylaşım tanımsız davranıştır; bu ders ikisini de koşturmuyor. - C’deki
volatile sig_atomic_tile Java’dakivolatileaynı 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.