Signal-Safe Kavramı
Unix türevi sistemlerde iyi uygulama yazmanın temel kurallarından biri signal safety ve thread safety kavramlarının anlaşılmasıdır.
Signal-Safe kavramı yoğunlukla Async-Signal-Safe biçiminde de ifade edilir.
Reentrancy
Konuya geçmeden önce reentrant kod kavramını incelememizde fayda var. Bir fonksiyon eğer aynı anda birden fazla task içinden çağrılsa da içerde kullanılan/üretilen verilerde herhangi bir bozulmaya yol açmayacak şekilde dizayn edilmiş ise, reentrant olduğunu söyleyebiliriz.
Dolayısıyla reentrant bir fonksiyonun karakteristik özelliklerini aşağıdaki gibi sıralayabiliriz:
Farklı çağrımlarında statik bir alan üzerinde değişiklik yapmaya çalışmaz
Sadece yerel değişkenler kullanır veya global bir değişken kullanımı zorunlu ise, yerel bir kopyasını çıkartarak işlemlerde yerel kopyasını kullanır
Geri dönüş değeri olarak asla herhangi bir statik alanın adresini dönmez
Fonksiyon içerisinden reentrant olmayan başka bir fonksiyon çağrılmaz
Bir thread-safe fonksiyon ise aynı anda birden fazla thread içerisinden, paylaşımlı bir alan kullanılsa dahi güvenle çağrılabilir. Paylaşımlı alan kullanımı söz konusu ise, ilgili alana erişimin thread-safe fonksiyonlarda gerekli kilit mekanizmalarıyla sıralı halde yapılması garanti edilir.
Bir fonksiyon eğer thread-safe ise aynı zamanda reentrant'dır.
Sinyal Kesmeleri
Sinyaller donanım kesmelerine benzer şekilde yazılımın akışını bir anda kesip ayrı bir yere dallanma (sinyal işleyici fonksiyonu) sonra kaldığı yerden devam etme özelliğine sahiptir. Tek işlemcili ve tek bir thread ile çalışan uygulamalarda dahi, signal-safety önemli bir problemdir.
Konunun daha iyi anlaşılması için görece zararsız örneklerden yola çıkalım. Örnek olarak uygulamanızın belirli bir bölümünde printf
fonksiyonu ile konsola bir çıktı gönderdiğinizi varsayalım. Eğer printf fonksiyonu henüz çalışmasını bitirmemişken işlemin yarısında uygulamaya sinyal gelirse ve sinyalin işlendiği fonksiyon içerisinde de ayrı bir printf fonksiyonu çalışıyorsa, sinyalin işlemi tamamlanıp asıl uygulamaya geri dönülüp ilerlendiğinde konsoldaki çıktılar birbirine karışacaktır.
Şimdi daha tehlikeli bir senaryoya göz atalım. Uygulamanızda malloc()
ile heap alanından bir miktar bellek talep ettiğinizi düşünelim. İşlem henüz tamamlanmamışken bir sinyal ile kesintiye uğrar ve sinyal işleyici fonksiyonumuzda doğrudan veya dolaylı olarak malloc()
fonksiyonunu çağırırsak ne olur?
Malloc işlemi performans açısından uygulama içerisindeki allokasyonları bağlı liste yapıları ile tutar. Bu liste yapısı güncelleniyorken sinyal nedeniyle yeniden çağrılması kritik hatalara yol açabilecektir.
Aşağıdaki kod parçacığını ve 64bit mimarideki karşılığını inceleyelim:
int y;
int x = 3;
y = x * 15;
/* derleyicinin ürettiği kod */
movl $3, -4(%rbp)
movl -4(%rbp), %edx
movl %edx, %eax
sall $4, %eax
subl %edx, %eax
movl %eax, -8(%rbp)
Yukarıdaki örnekte programcı açısından tek satırlık y = x * 15
satırı işlemcide 5 adımda gerçekleştirilmektedir (sayı yazmaca yükleniyor, sall
ile 4 bit kaydırılıp 16 ile çarpılmış oluyor, sonra elde edilen değerden başlangıçtaki sayı çıkartılıp 15 ile çarpım değerine ulaşılıyor). Bu 5 adımın herhangi birinde sinyal nedeni ile kesinti gerçekleşebilir. Dolayısıyla kaynak kod seviyesindeki tek satırlık basit bir işlem dahi çokça işlem adımına yol açıyor olabilir.
Sinyal Maskeleme
Signal-safe olmayan fonksiyonların kullanımına yönelik 2 temel çözüm bulunur:
signal-safe olmayan fonksiyonların kullanıldığı bölümlerde sinyalleri maskelemek
sinyallerin işlendiği callback fonksiyonlarında signal-safe olmayan fonksiyonların kullanımından kaçınmak
Maskeleme işlemleri için sinyal kümeleri ve bloklama konularına geçelim.