> UNIX/Linux Sistemlerinde Sinyal İşlemleri: Sinyal işlemleri UNIX/Linux sistemlerinde sistem programlama faaliyetlerinde yoğun bir biçimde kullanılmaktadır. Dolayısıyla iyi bilinmesi gerekmektedir. "thread" ler konusu kadar kapsamlı olmasa bile yine de kapsamlı bir konudur. Pekiyi nedir bu sinyal kavramı? Sinyaller, kesme mekanizmalarına da benzetilebilir. UNIX/Linux sistemlerde asenkron işlem yapılmasına olanak sağlarlar. Sinyaller normalde proseslere gönderilmektedir fakat "thread" kavramının da işletim sistemine eklenmesiyle "thread" lere de sinyal gönderilebilir. Fakat burada dikkat etmemiz gereken husus ise sinyali sadece kendi "thread" imize gönderebiliyor oluşumuzdur. Çünkü "thread" lerin ID değerleri yalnızda ait oldukları proseslerde anlamlıdırlar. Bir prosese sinyal geldiğinde prosesin akışı o anda kesilir ve ismine "Sinyal Fonksiyonu (Signal Function)" denilen bir fonksiyon çalıştırılır. Daha sonra bu fonksiyon bittiğinde akış tekrardan kaldığı yere geri döner ve o noktadan çalışmaya devam eder. Böylelikle bir işi yaparken araya başka işlemlerin girebilmesine olanak sağlamış oluyoruz. Pekiyi bir sinyal nasıl oluşur? Burada çeşitli durumlar söz konusudur. Örneğin, programcının yaptığı çeşitli ihlallerde, işletim sistemi tarafından prosese sinyal gönderilmektedir. Öte yandan bazı sinyaller ise bazı aygıt sürücüleri tarafından proseslere gönderilir. Örneğin, terminal aygıt sürücüsü prosesin ilişkin olduğu terminale, programcının "CTRL+C" ya da "CTRL+Backspace" tuşlarına basmasından dolayı, bazı sinyaller gönderebilmektedir. Benzer şekilde bir program da başka bir programa açıkça sinyal gönderebilmektedir. Örneğin, bazı POSIX fonksiyonları belirli şartlar oluştuğunda sinyal gönderebilmektedir. Diğer yandan bir sinyal bir prosese gönderildiğinde o prosesteki hangi "thread" in bu sinyali alacağı, POSIX standartlarınca, ilgili işletim sistemini yazanların isteğine bırakılmıştır. Bütün bunlara ek olarak, her bir sinyal bir numaraya sahiptir. POSIX sistemlerinde sinyallere ilişkin numaraların ne olacağı yine işletim sisteminin yazanların isteğine bırakılmıştır ancak taşınabilirlik sağlamak için bu sinyal numaraları "signal.h" başlık dosyası içerisinde "SIGXXX" içerisinde sembolik sabitler ile "define" edilmişlerdir. Dolayısıyla bu sembolik sabitleri kullanmalıyız. Bir diğer yandan, yukarıda da açıklandığı üzere, bir prosese sinyal geldiğinde prosesin akışı o anda kesilip ismine sinyal fonksiyonu denilen fonksiyona geçmektedir. Aslında bu durum tam olarak böyle değildir. Şöyleki; prosese bir sinyal gelmesi halinde, eğer programcı bir sinyal fonksiyonu "set" etmişse, yukarıda anlatılan senaryo gerçekleşmektedir. Eğer böyle bir sinyal fonksiyonu "set" EDİLMEMİŞSE, bu durumda "Varsayılan Eylem (Default Action)" uygulanmaktadır. Bu ise sinyalin ne olduğuna bağlıdır. Bazı sinyaller için bu aksiyon ilgili sinyalin görmezden gelinmesiyken, bazı sinyallerde prosesin sonlandırılması biçimindedir. Bazı sinyallerde ise bu aksiyon hem prosesin sonlandırılması hem de "core file" oluşturulması biçimindedir. Buradaki "core file" aslında teşhis amacıyla oluşturulan ve "debugger" altında incelenebilen özel dosyalardır. Dolayısıyla programcı bu varsayılan aksiyonları sinyal temelinde öğrenmesi gerekmektedir. Bütün bunlara ek olarak, bir sinyal oluştuğunda ilk önce bu sinyal hedef prosese "deliver" edilir. Daha sonra iş bu proses gelen bu sinyali mümkün olan en kısa zamanda işlemek ister. Eğer bir sinyal fonksiyonu da "set" edilmişse, bu fonksiyonu en kısa zamanda çağırmak isteyecektir. İşte bir prosese sinyalin geldiği an ile ilgili sinyal fonksiyonunun çağrılması arasındaki bu zamana ise sinyalin askıda olması, yani "pending" olması denmektedir. Eğer programın akışı sinyalin geldiği anda bir sistem fonksiyonu içerisindeyse ve bu sistem fonksiyonu da uzun sürecek bir eylem başlatmışsa ya da açıkça bloke olmuşsa, ilgili sistem fonksiyon işletim sistemi tarafından başarısızlıkla sonlandırır ve tez zamanda ilgili sinyal fonksiyonunu çağırır. Eğer bir POSIX fonksiyonu da gelen sinyalden dolayı başarısız olmuşsa, "errno" değişkeninin değeri "EINTR" değerine çekilecektir. Buradan da görüleceği üzere bir sinyal geldiği anda programın akışınının aniden kesilmesi gerçekleşmez, bir takım ön kontrollerden geçmektedir. Örneğin, bizler bir "pipe" üzerinden "read" yapmak isteyelim ancak okunacak hiç "byte" bulunmasın. Bu durumda "read" fonksiyonu blokeye yol açacaktır. İşte bu sırada bir sinyal gelirse, "read" fonksiyonu başarısızlıkla geri döner ve "errno" değişkeni "EINTR" değerini alır. Bu tür durumlarda bizler ilgili fonksiyonun başarısız olma nedeninin gelen sinyalden olduğunu anlayıp, onu tekrardan çağırmak da isteyebiliriz. Şöyleki: while((result = read(...)) == -1 && errno == EINTR) ; if(result == -1) exit_sys("read"); Pek tabii programcı bu tür POSIX fonksiyonlarının otomatik olarak yeniden çağrılmasını da sağlayabilir. Eğer bu sağlanmışsa, ilgili fonksiyon hiç geri dönmez ancak ilgili sinyal fonksiyonu da çalıştırılır. Bu otomatik yeniden çağırma işlemi ise kütüphane tarafından değil, bizzat çekirdek tarafından gerçekleştirilir. Tabii bu durum her POSIX fonksiyonu için de geçerli değildir. Buradaki husus ilgili sistem fonksiyonunun ya da onu çağıran POSIX fonksiyonunun "yavaş bir fonksiyon" olması gerekmektedir. Yani blokeye yol açabilecek sistem fonksiyonlarıdır. Bir diğer deyişle programcı, çağırdığı sistem fonksiyonunun ya da POSIX fonksiyonunun bir sinyal karşısındaki davranışını da BİLMEK ZORUNDADIR. Tabii bir sinyal için sinyal fonksiyonu atanmamışsa ve varsayılan aksiyon da görmezden gelinmesiyse, bu durumda ilgili sistem fonksiyonunun ya da POSIX fonksiyonunun başarısız olmasının, o fonksiyonu yeniden çağırmanın da bir önemi kalmamaktadır. Şimdi bu "yavaş fonksiyonlar" ile kastedilen şeyi detaylandıralım; burada kastedilen esas şey programın akışının bir sistem fonksiyonu içerisinde göreli bir biçimde uzun zaman bekleyebilmesidir. Örneğin, "read" fonksiyonu ile bir disk dosyasından okuma yapmak isteyelim ve bu işlem sırasında da bir sinyal meydana gelsin. Şimdi bu okuma işlemi yavaş bir işlem değildir. Dolayısıyla "read" fonksiyonu başarı ile geri döndükten sonra, eğer "set" edilmiş ise, ilgili sinyal fonksiyonu çağrılacaktır. Ancak "read" fonksiyonu ile bir "pipe" ya da "socket" üzerinden yaparsak, okunacak bilgi kalmadığında bloke oluşacaktır. İşte bu durumda "read" fonksiyonunu "yavaş fonksiyon" olarak değerlendireceğiz. Böylesi bir durumda da sinyal gelmesi halinde ilk önce "read" fonksiyonu başarısızla sonuçlanacak ve programın akışı daha sonra ilgili sinyal fonksiyonuna geçecek eğer herhangi bir fonksiyon belirlenmişse. Pekiyi bizler bir sinyal oluştuğunda çağrılacak olan "sinyal fonksiyonu" nu nasıl "set" edebiliriz? Burada karşımıza iki farklı yöntem çıkmaktadır. İlki "signal" fonksiyonu kullanılarak, ikincisi ise "sigaction" fonksiyonu ile. Maalesef POSIX standartlarının oluşturulduğu dönemlerde, UNIX ve türevi sistemlerdeki ilgili sinyal fonksiyonunun davranışları arasında farklılık görülmekteydi. Dolayısıyla POSIX standartlarınca "signal" fonksiyonunun davranışı "öyle de davranabilir, böyle de davranabilir" biçimindedir. Bu da beraberinde taşınabilirlik sorunlarını getirdi. İşte "signal" fonksiyonunun oluşturduğu taşınabilirlik sorunu, "sigaction" isimli POSIX fonksiyonu ile çözülmüştür. Kursun işleyişi sırasında ilk olarak "signal", daha sonra "sigaction" fonksiyonları anlatılacak olup programcının "sigaction" fonksiyonunu kullanması tavsiye edilmektedir. Şimdi de bu fonksiyonları inceleyelim: >> "signal" : Fonksiyonun prototipi aşağıdaki gibidir. #include void (*signal(int sig, void (*func)(int)))(int); Görüleceği üzere fonksiyonun prototipi karışık. Bunu aşağıdaki biçimde de yazabiliriz: #include void (*signal(int sig, void (*func)(int))) (int); Buradan da anlaşılacağı üzere "signal" fonksiyonunun geri dönüş değeri bir fonksiyon göstericisi. Öyle bir gösterici ki gösterdiği fonksiyonun geri dönüş değeri "void", aldığı parametre ise "int" türden. Pekiyi bu "signal" fonksiyonu ne türden argümanlar almaktadır? Şöyleki: #include void (*signal ( int sig, void (*func)(int) ) ) (int); Buradan da görüleceği üzere "signal" fonksiyonunun birinci parametresi "int" türden, ikinci parametresi ise yine bir fonksiyon göstericisi. Bu fonksiyon göstericisi ise öyle bir gösterici ki gösterdiği fonksiyon "void" türden geri dönüş değerine sahip ve "int" türden parametre almaktadır. Yani "signal" fonksiyonunun birinci parametresine hangi sinyal için "set" işlemi uygulanacağı, ikinci parametre ise çağrılacak fonksiyonu belirmektedir. Fonksiyonun geri dönüş değeri ise başarı durumunda bir önceki sinyal fonksiyonunun adresiyle, hata durumunda ise "SIG_ERR" özel değeri ile geri dönmektedir. Bu özel değer ise "signal.h" içerisinde tanımlanmış bir fonksiyon adresidir ve başarısızlığı anlatmaktadır. * Örnek 1, Aşağıdaki program normal şartlarda "for" döngüsünü çalıştırmaktadır. Fakat klavyeden "CTRL+C" yaptığımız zaman işletim sistemi prosesimize bir "SIGINT" sinyalini gönderecektir. Normal şartlarda bu sinyal için herhangi bir sinyal fonksiyonu atanmadığı zaman proses sonlandırılırken, aşağıdaki örnekte bir fonksiyon atadığımız için artık prosesimiz sonlanmayacak ve atanmış olan fonksiyon çağrılacaktır. O fonksiyondan sonra akış tekrardan "for" döngüsüne gireceği için, programımızı sonlandırmak için başka yöntemler denemek durumunda kalacağız. #include #include #include void sigint_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # ^CA signal occured!.. ^CA signal occured!.. ... */ if(signal(SIGINT, sigint_handler) == SIG_ERR) exit_sys("signal"); for(;;) ; return 0; } void sigint_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise "read" fonksiyonu yavaş fonksiyon olarak ele alınmıştır. Yukarıda açıklanan otomatik yeniden başlatma özelliği aktif edildiğinden, ilgili sinyal fonksiyonundan hemen sonra tekrardan "read" fonksiyonu çağrılmaktadır. #include #include #include #include void sigint_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # ^CA signal occured!.. ^CA signal occured!.. ^CA signal occured!.. ... */ if(signal(SIGINT, sigint_handler) == SIG_ERR) exit_sys("signal"); char buffer[4096 + 1]; ssize_t result; if((result = read(0, buffer, 4096)) == -1) exit_sys("read"); buffer[result] = '\0'; printf("Entered Value: %s\n", buffer); return 0; } void sigint_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi "signal" fonksiyonunun ikinci parametresindeki fonksiyon göstericisinin gösterdiği fonksiyonun parametresi neden "int" türdendir? Bunun yegane amacı, işletim sisteminin oluşan sinyalin numarasını da sinyal fonksiyonuna aktardığından dolayı, farklı sinyallere aynı fonksiyonu "set" etmektir. Böylelikle ilgili sinyal fonksiyonunun gövdesinde hangi sinyal nedeniyle fonksiyonun çağrılmış olduğunu belirleyebilir. Diğer yandan "signal" fonksiyonunun ikinci parametresine şu değerlerden birisini de geçebiliriz: "SIG_DFL", "SIG_IGN". -> "SIG_DFL" : Sinyali "default action" a çekmek için kullanılır. Yani bu değer kullanıldığında, sanki hiç sinyal fonksiyonu "set" edilmemiş gibi bir etki oluşturmaktadır. Varsayılan davranışın ne olacağı ise ilgili sinyalin dökümanlarında belirtilmiştir. -> "SIG_IGN" : Sinyali görmezden gelme, yani "ignore" etme, aksiyonu alması için kullanılır. Bir sinyal "SIG_IGN" ile görmezden gelinirse, bu sinyal oluştuğunda, sanki hiç oluşmamış gibi bir davranış gösterecektir. Fakat her sinyal "ignore" EDİLEMEMEKTEDİR. Pekala "signal" fonksiyonunun geri dönüş değeri de "SIG_DFL" veya "SIG_IGN" değerlerinden birisi olabilir. Örneğin, biz bir sinyali ilk kez "set" ediyorsak bir önceki sinyal fonksiyonu muhtemelen "SIG_DFL" veya "SIG_IGN" biçiminde olacaktır. * Örnek 1, #include #include #include void sigint_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # SIG_DFL */ void (*old_handler)(int); if((old_handler = signal(SIGINT, SIG_IGN)) == SIG_ERR) exit_sys("signal"); if(old_handler == SIG_DFL) printf("SIG_DFL"); return 0; } void sigint_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de "signal" fonksiyonunun olumsuz yönlerine değinelim. Yukarıda da açıklandığı üzere POSIX standartlarınca bu fonksiyon "implementation defined" olarak tanımlanmıştır. Bu da taşınabilirliği bozmaktadır. Bunun da yegane sebebi şunlardır: -> Eski "AT&T" UNIX sistemlerinde ve bu kökten gelen sistemlerde "signal" fonksiyonu ile bir "set" işlemi yapıldıktan sonra, bir sinyal oluşması durumunda, ilgili sinyal aksiyonu varsayılan olarak değiştiriliyordu. Bunu gidermek için de programcılar ilgili sinyal fonksiyonunun bünyesinde yeniden bir "set" işlemi yapmaktaydılar. Örneğin, void signal_handler(int sno) { if(signal(SIGXXX, signal_handler) == SIG_ERR) exit_sys("signal"); //... } Böylece sinyal varsayılana çekilir çekilmez tekrardan olması gereken değere "set" edilmektedi. Fakat aşağıdaki durumda için bir önlem alınamıyor, bu da programın sonlanmasına neden olmaktaydı. void signal_handler(int sno) { /* BU NOKTADA AYNI İLGİLİ SİNYAL VARSAYILANA ÇEKİLİ DURUMDADIR. EĞER AYNI SİNYALDEN EĞER AYNI SİNYAL YENİDEN OLUŞURSA, PROGRAM SONLANABİLMEKTEDİR. */ if(signal(SIGXXX, signal_handler) == SIG_ERR) exit_sys("signal"); //... } Ancak "BSD" UNIX sistemlerinde bu problem, ilgili sinyalin varsayılana çekilmemesi sağlanarak aşılmaya çalışılmıştır. Dolayısıyla BSD ve türevi sistemlerde bu varsayılana çekme durumu gerçekleşmemektedir. Linux sistemlerinde ise "AT&T" semantiği uygulandığı için yine ilgili sinyal varsayılana çekilmektedir. Ancak "signal" isimli POSIX fonksiyonu, "glibc" kütüphanesinin belirli bir versiyonundan sonra, "signal" sistem fonksiyonu yerine "sigaction" sistem fonksiyonu kullanılarak yazıldığı için, sinyali yine varsayılana ÇEKMEMEKTEDİR. Dolayısıyla artık Linux sistemlerinde "AT&T" değil, "BSD" semantiği uygulanmaktadır. POSIX standartları ise "AT&T" & "BSD" sistemlerindeki farklılıkların geçerli olabilmesi için mekanizmayı işletim sistemini yazanların isteğine bırakmıştır. -> "AT&T" ve bu kökten gelen sistemlerde bir sinyal oluştuğunda o sinyal, ilgili sinyal fonksiyonu çalıştığı süre boyunca bloke EDİLMİYORDU. Yani aynı sinyalden üst üste gelmesi durumunda ilgili sinyal fonksiyonu da "recursive" biçimde çalıştırılmış oluyor, bu da "stack overflow" gibi sorunlara neden olabiliyordu. "BSD" sistemlerinde ise bir sinyal oluştuğunda o sinyal, ilgili sinyal fonksiyonu çalıştığı süre boyunca BLOKE EDİLİYORDU. Böylece "recursive" biçimde ilgili sinyal fonksiyonunun çalışması engellenmiş olurdu. Linux sistemlerinde ise "signal" isimli sistem fonksiyonu "AT&T" semantiğini, "signal" isimli POSIX fonksiyonu ise, "glibc" kütüphanesinin belirli bir versiyonundan sonra, "sigaction" isimli sistem fonksiyonunu çağırmakta ve "BSD" semantiğini uygulamaktadır. POSIX standartlarınca, ilgili sinyal fonksiyonu çalıştığı sürece aynı sinyalin bloke edilip edilmeyeceğini işletim sistemini yazanlara bırakmıştır. -> "AT&T" ve bu kökten gelen sistemlerde yavaş sistem fonksiyonları başarısız olmakta ve "errno" değişkeni "EINTR" değerini almaktadır. Halbuki "BSD" sistemlerinde ise otomatik "restart" işlemi yapılmaktadır. Linux sistemleri ise "AT&T" semantiği kullandığı için otomatik "restart" işlemi yapmamaktadır. Ancak "signal" fonksiyonu, "glibc" kütüphanesinin belirli bir versiyonundan sonra, "sigaction" sistem fonksiyonu çağrılacak biçimde yazılmıştır ve otomatik "restart" işlemi YAPILMAKTADIR. POSIX standartlarınca ise bu özellik ilgili işletim sistemini yazanların isteğine bırakılmıştır. Bu durumda Linux sistemlerindeki "glibc" kütüphanesinde bulunan "signal" fonksiyonu: -> Sinyali tekrardan varsayılana çekmez. -> Sinyali, sinyal fonksiyonu çalıştığı sürece bloke etmekte. -> Otomatik "restart" işlemini uygulamaktadır. >> "alarm" fonksiyonu : Öte yandan "alarm" isimli bir POSIX fonksiyonu daha bulundurulmuştur. Bu fonksiyona geçilen süre bilgisi dolduğunda, fonksiyonu çağıran prosese "SIGALRM" isimli sinyali göndermektedir. Bu sinyalin varsayılan davranışı ise prosesin sonlandırılması biçimindedir. "alam" fonksiyonunu birden çok çağırdığımızda süreler biriktirilmez ve her yeni çağrı bir öncekini devre dışı bırakarak sayacı yeniden "set" etmektedir. Fonksiyonun prototipi şöyledir: #include unsigned alarm(unsigned seconds); Fonksiyon saniye sayısını parametre olarak alır. Bu parametreye "0" değerinin geçilmesi durumunda, daha önceki "alarm" işlevinin devre dışı bırakılacağı anlamına gelmektedir. Eğer daha önce bu fonksiyon çağrılıp da bir "set" işlemi yapılmışsa, o "set" işlemi için kalan saniye sayısını geri döndürmektedir. Eğer daha önce bu fonksiyon çağrılmamışsa ya da çağrılmış fakat süresi dolmuşsa, "0" değeri ile geri dönmektedir. FONKSİYON BAŞARISIZ OLAMAMAKTADIR. * Örnek 1, #include #include #include #include void sigalrm_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 1 2 3 A signal occured!.. 4 5 6 7 8 9 */ if(signal(SIGALRM, sigalrm_handler) == SIG_ERR) exit_sys("signal"); alarm(5); for(int i = 0; i < 10; ++i){ sleep(1); printf("%d\n", i); } return 0; } void sigalrm_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "sigaction" : "signal" fonksiyonunun oldukça iyileştirilmiş halidir ve o fonksiyondaki semantik belirsizlikler kaldırılmıştır. Dolayısıyla programcının "sigaction" fonksiyonunu kullanması iyi bir tekniktir. Ancak bu fonksiyonun kullanımı, "signal" fonksiyonu kullanımından daha karışıktır. Fonksiyonun prototipi ise aşağıdaki gibidir: #include int sigaction(int sig, const struct sigaction * act, struct sigaction * oact); Fonksiyonun birinci parametresi yine "set" edilecek sinyalin numarasını belirtmektedir. Fonksiyonun ikinci parametresi "sigaction" isimli bir yapı nesnesinin adresini almaktadır. Programcı sinyal "set" işlemi ile ilgili bazı bilgileri, "sigaction" türünden yapı nesnesinin içerisine yerleştirir ve sonra bu nesnenin adresini de fonksiyona geçer. Fonksiyonun üçüncü parametresi yine bu türdendir, "NULL" değeri geçilebilir. Bu durumda bir önceki sinyal "set" işlemine ait özellikleri temin edememiş oluruz. Ancak "NULL" değeri geçilmezse, bu parametreye adresi geçilen yapı nesnesine daha önceki sinyal "set" özellikleri yerleştirilecektir. Pekala ikinci parametreye de "NULL" değeri geçilebilir, bu durumda sadece bir önceki sinyal "set" işlemine ait özellikleri temin etmiş olacağız. Hem ikinci hem de üçüncü parametreye "NULL" değeri de geçilebilir. Fakat böyle yapmanın bir mantığı yoktur. Fonksiyon başarı durumunda "0" değerine, hata durumunda ise "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Fonksiyonun kullanımındaki en önemli nokta şüphesiz "sigaction" yapı nesnesinin içinin doldurulmasıdır. Bu yapı türü "signal.h" içerisinde aşağıdaki biçimde bildirilmiştir: struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; }; Yapının, -> "sa_handler" : Sinyal fonksiyonunun adresini temsil etmektedir. "SIG_DFL" ve "SIG_IGB" özel değerleri de girilebilir. -> "sa_sigaction" : Daha gelişmiş olan sinyal fonksiyonunun adresini temsil etmektedir. UNIX/Linux sistemlerine "güvenilir sinyaller" adı altında bazı semantik eklemeler sonucunda böylesi bir sinyal fonksiyonu da eklenmiştir. Yapının ya bu elemanına ya da "sa_handler" isimli elemanına adres geçmesi gerekmektedir. Her iki elemana da adres geçmemesi GEREKMEKTEDİR. "SIG_DFL" ve "SIG_IGB" özel değerleri de girilebilir. -> sa_mask : Sinyal bloke kümesini belirtmektedir. Varsayılan durumda bir sinyal oluştuğu zaman, ilgili sinyal fonksiyonu çalıştığı süre boyunca, aynı numaralı sinyal bloke olmaktadır. Bir sinyal bloke olduğunda, o sinyal ilgili porsese gelmesi durumunda "pending" durumda bekletilmesi demektir. Fakat sinyaller istiflenemezler. Yani bir sinyal fonksiyonu işletilirken aynı sinyalden birden fazla gelmesi durumunda, akış sinyal fonksiyonundan çıktıktan sonra, sadece bir defa için ilgili sinyal fonksiyonu yeniden çağrılır. Bu şekilde ilgili sinyal fonksiyonunun "recursive" biçimde çağrılması engellenmiş olmaktadır. Ancak programcı isterse, ilgili sinyal fonksiyonu çalıştığı sürece, başka sinyallerinde bloke edilmesini sağlayabilir. İşte yapının bu elemanı bu amaçla kullanılmaktadır. Bir çeşit bir "bit" dizisi olarak düşünülmüştür. Yani bu dizinin çeşitli elemanları, çeşitli sinyalleri bloke edilip edilmeyeceğini belirtmektedir. Dolayısıyla bu dizinin elemanlarını "set" ve "reset" etmek için çeşitli fonksiyonlar bulundurulmuştur. Tabii bu fonksiyonlar makro biçimde de yazılmış olabilirler. Bunların listesi şu şekildedir: "sigemptyset", "sigfillset", "sigaddset", "sigdelset", "sigismember". Bunlardan, -> "sigemptyset" : "bit" dizisi içerisindeki bütün "bit" leri "reset" etmektedir. -> "sigfillset" : "bit" dizisi içerisindeki bütün "bit" leri "set" etmektedir. -> "sigdelset" : "bit" dizisinin belli bir "bit" ini "reset" etmektedir. -> "sigaddset" : "bit" dizisinin belli bir "bit" ini "set" etmektedir. -> "sigismember" : Belli bir sinyalin "bit" dizisi içerisindeki değerini, yani "set" ya da "reset" olduğunu, bize vermektedir. Fonksiyonlar başarı durumunda "0", hata durumunda "-1" değerine geri dönmektedir. Ancak "sigismember" başarı durumunda "0" ya da "1", hata durumunda ise "-1" değerine geri dönmektedir. Tabii bu fonksiyonların başarısının kontrolüne gerek yoktur. Bu fonksiyonların tipik kullanımı aşağıdaki biçimdedir: sigset_t sset; sigemptyset(&sset); sigaddset(&ssset, SIGINT); sigaddset(&sset, SIGALARM); Böylelikle "SIGINT" ve "SIGALARM" sinyalleri bir küme haline getirilmiş oldu. Henüz bu sinyaller için bloke işlemi UYGULANMADI. Bu bloke işlemini de tipik olarak aşağıdaki biçimde yapabiliriz: struct sigaction sa; //... sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGINT); sigaddset(&sa.sa_mask, SIGALRM); //... if(sigaction(SIGTERM, &sa, NULL) == -1) exit_sys("sigaction"); Artık "SIGTERM" sinyali için oluştuğunda ilgili sinyal fonksiyonu çağrılacaktır. Programın akışı da bu fonksiyon içerisinde bulunduğu sürece "SIGINT" ve "SIGALRM" sinyalleri bloke edilecektir. Zaten varsayılan durumda ilgili sinyalin kendisinin de bloke olduğundan bahsetmiştik. Bir diğer deyişle ilgili sinyalin kendisi zaten bloke olmakta. Bizler de bu "bit" dizisi ile ilave bloke olmasını istediğimiz sinyalleri belirtmiş oluyoruz. Programcı yapının "sa_mask" elemanına bir değer mutlaka atamalıdır. Anımsanacağı üzere yerel nesnelerin içerisinde rastgele değerler vardır. Kaldıki "global" isim alanındaki nesne olsalar bile içi boş bir "bit" dizisi belirtmesi garanti değildir. Dolayısıyla yapının bu elemanına şöyle değer atayabiliriz: sigemptyset(&sa.sa_mask); Artık ilgili sinyal fonksiyonu çalışırken, ilgili sinyal dışındaki hiç bir sinyal bloke edilmeyecektir. -> "sa_flags" : Bazı sembolik sabitlerin "bitwise-OR" işlemine sokulmasıyla oluşturulur. Bu sembolik sabitler şunlardır: "SA_RESETHAND", "SA_RESTART", "SA_SIGINFO", "SA_NODEFER", "SA_NOCLDWAIT", "SA_NOCLDSTOP", "SA_ONSTACK". Tabii bu bayrakları kullanmak istemiyorsak, "0" değerini de atayabiliriz. Bu bayraklardan, -> "SA_RESETHAND" : İlgili sinyan fonksiyonu çalıştırıldığında, sinyal otomatik olarak varsayılana çekilir. Varsayılan durumda bu bayrak "set" EDİLMEMİŞTİR. Bu bayrak "AT&T" semantiğini uygulayabilmek için bulundurulmuştur. -> "SA_RESTART" : Bu bayrak "set" edilirse yavaş POSIX fonksiyonları, yani onların çağırdığı sistem fonksiyonları, sırasında bir "sinyal" oluşması durumunda ilgili sinyal fonksiyonu çağrılır. Ancak fonksiyon başarısız olmaz çünkü arka plandaki sistem fonksiyonunun "restart" edilmesi çekirdek tarafından otomatik olarak yapılmaktadır. Bir diğer deyişle programın akışı ilgili sistem fonksiyonu ilgili sinyalin gelmesi durumunda başarısız olmayacaktır. -> "SA_SIGINFO" : Sinyal fonksiyonu için "sigaction" yapısının "sa_handler" elemanı değil "sa_sigaction" elemanı dikkate alınmaktadır. Varsayılan durumda yapının "sa_handler" elemanı kullanılmaktadır. Bu konunun detaylarına "Gerçek Zamanlı Sinyaller (Realtime Signals)" konusu sırasında değinilecektir. -> "SA_NODEFER" : Anımsanacağı üzere varsayılan durumda bir sinyal fonksiyonu çalışırken, o sinyal bloke edilmektedir. Bu bayrak kullanılırsa, o sinyal de artık bloke olmayacaktır. Bu durumda ilgili sinyal fonksiyonu "recursive" biçimde çağrılabilmektedir. -> "SA_NOCLDWAIT" : Bu bayrak "SIGCHLD" sinyali için anlamlıdır. Otomatik olarak "zombie process" oluşmasını engellemek için kullanılır. Bu bayrak "set" edildiğinde ilgili prosesin sonlanması ile kaynakları da "wait" fonksiyonlarını beklemeden serbest bırakılır. Dolayısıyla programcı artık "wait" işlemi yapmayacaktır. -> "SA_NOCLDSTOP" : Bu bayrak belirtildiğinde, alt proses durdurulduğunda ya da yeniden çalışmaya devam ettirildiğinde, üst prosese "SIGCHLD" sinyalini göndermeyecektir. -> "SA_ONSTACK" : İç içe sinyal oluştuğunda taşma problemleri gerçekleşebilmektedir. Bunun için alternatif "stack" kullanımı da mümkündür. İşte bu bayrak ilgili sinyal fonksiyonu çalışırken alternatif "stack" alanının kullanılacağını belirtmektedir. Tabii bu alternatif "stack" alanının da "sigaltstack" POSIX fonksiyonu ile "set" edilmesi gerekmektedir. Şimdi de bu fonksiyonun kullanımına ilişkin örnekler verelim: * Örnek 1, Aşağıdaki örnekte ilgili sinyal oluştuğunda ve ilgili sinyal fonksiyonu çalıştığı sürece, başka bir sinyal bloke edilmeyecektir. İşlem sonunda "restart" olmayacaktır. #include #include #include #include void sigalarm_handler(int sig_no); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 1 2 3 4 ALARM 5 6 7 8 9 */ struct sigaction sa; sa.sa_handler = sigalarm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if(sigaction(SIGALRM, &sa, NULL) == -1) exit_sys("sigaction"); alarm(5); for(int i = 0; i < 10; ++i){ printf("%d\n", i); sleep(1); } } void sigalarm_handler(int sig_no) { printf("ALARM\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Anımsanacağı üzere "glibc" kütüphanesindeki "signal" POSIX fonksiyonu belli bir versiyondan sonra "sigaction" isimli sistem fonksiyonu çağrılarak yazılmıştır. Dolayısıyla yeni sürümlerde sinyali işin başında varsayılan aksiyona çekmiyor, sinyal fonksiyonu çalıştığı sürece aynı sinyali bloke ediyor ve otomatik "restart" işlemi de yapıyor. İşte aşağıda bu işlemleri gerçekleştiren bir "signal" fonksiyon implementasyonu kullanılmıştır. #include #include #include #include void (*my_signal(int sig, void (*handler)(int)))(int); void sigalarm_handler(int sig_no); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 1 2 3 4 ALARM 5 6 7 8 9 */ if(my_signal(SIGALRM, sigalarm_handler) == SIG_ERR) exit_sys("my_signal"); alarm(5); for(int i = 0; i < 10; ++i){ printf("%d\n", i); sleep(1); } } void (*my_signal(int sig, void (*handler)(int)))(int) { struct sigaction sa, sa_old; sa.sa_handler = sigalarm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if(sigaction(SIGALRM, &sa, &sa_old) == -1) return SIG_ERR; return sa_old.sa_handler; } void sigalarm_handler(int sig_no) { printf("ALARM\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "kill" Fonksiyonu: Şimdi de bir prosese sinyal gönderilme mekanizmasını inceleyelim. Bunun için POSIX sistemlerinde "kill" isminde bir fonksiyon bulundurulmaktadır. Fonksiyonun prototipi şu şekildedir: #include int kill(pid_t pid, int sig); Fonksiyonun birinci parametresi hedef prosesin ID değerini, ikinci parametresi ise gönderilecek sinyalin numarasını almaktadır. Başarı durumunda "0", hata durumunda "-1" değerine geri dönmeketedir. Fonksiyonun birinci parametresinin şöyle detayları vardır: -> Eğer bu parametre sıfırdan büyük ise sinyal yalnızca belirtilen ID değerine sahip prosese gönderilmektedir. -> Eğer bu parametreye sıfır değerini geçersek, sinyal, sinyali gönderen prosesin grup ID değeri ile aynı proses grup ID değerine sahip olan bütün proseslere gönderilecektir. Tabii burada o proses grubuna sinyal gönderme yetkisinin de olması gerek. Yani biz kendi proses grubumuzdaki bütün proseslere bu biçimde sinyal gönderebiliriz. -> Eğer bu parametre "-1" geçilirse, sinyal, sinyal gönderme hakkı olan bütün proseslere gönderilecektir. -> Eğer bu parametreye sıfırdan küçük bir değer girilirse, girilen değerin mutlak değeri bir proses grup ID olarak kabul edilir ve bu ID değerine sahip olanların hepsine sinyal gönderilir. Tabii burada o proses grubuna sinyal gönderme yetkisinin de olması gerekmektedir. Burada belirtilen proses grup kavramı ilerleyen paragraflarda ele alınacaktır. Genellikle bu fonksiyon ile bir prosesin sonlandırılması için sinyaller gönderilir ancak başka amaçlarla da sinyallerin gönderilmesi söz konusu olabilmektedir. Burada başka bir prosese sinyal gönderebilmek için prosesimizin ya "appropriate priviledged" olması ya da gerçek kullancı veya etkin kullanıcı ID değerimizin sinyali alacak olan prosesin gerçek ya da "Saklanmış Kullanıcı ID (saved-set user id)" değerine eşit olması gerekmektedir. Buradaki Saklanmış Kullanıcı ID değerine ilerleyen paragraflarda da ele alınacaktır. Özetle; -> Bizler ancak kendi proseslerimize sinyal gönderebiliriz. Herhangi bir prosese sinyal gönderebilmek için etkin kullanıcı id değerimizin "0" olması Şimdi de bu konuyla ilgili örneklere bakalım: * Örnek 1, Aşağıdaki örnekte ilk olarak "slave" programı çalıştırıldı. Daha sonra başka bir terminalden "ps -u" komutu çalıştırılarak bu "slave" prosesinin ID değeri öğrenildi. Devamında yine aynı terminal üzerinden "master" programı çalıştırıldı ve "slave" programının ID'si ile gönderilmek istenen sinyal komut satırı olarak "master" programına gönderilmiştir. /* master.c */ #include #include #include #include void exit_sys(const char* msg); typedef struct tagSIGNAL_INFO{ const char* name; int sig; } SIGNAL_INFO; SIGNAL_INFO g_signal_info[] = { {"SIGINT", SIGINT}, {"SIGTERM", SIGTERM}, {"SIGKILL", SIGKILL}, {NULL, 0} }; int main(int argc, char** argv) { /* # OUTPUT # $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ kill SIGTEMP 13269 */ if(argc != 3){ fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } pid_t pid = (pid_t)atol(argv[2]); for(int i = 0; g_signal_info[i].name != NULL; ++i) if(!strcmp(argv[1], g_signal_info[i].name)) if(kill(pid, g_signal_info[i].sig) == -1) exit_sys("kill"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* slave.c */ #include #include #include #include void sigint_handler(int sno); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 //... 36 37 SIGINT handler is running!.. 38 39 40 41 SIGINT handler is running!.. 42 SIGINT handler is running!.. 43 SIGINT handler is running!.. 44 SIGINT handler is running!.. 45 SIGINT handler is running!.. 46 SIGINT handler is running!.. 47 48 //... 55 Terminated */ struct sigaction sa, sa_old; sa.sa_handler = sigint_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGINT, &sa, &sa_old) == -1) exit_sys("sigaction"); for(int i = 0; i < 60; ++i){ printf("%d\n", i); sleep(1); } return 0; } void sigint_handler(int sno) { printf("SIGINT handler is running!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan sinyal numaralarından o sinyale ilişkin yazı elde edebileceğimiz bir fonksiyon da POSIX standartlarında bulundurulmuştur. Bu fonksiyon "strsignal" isimli fonksiyondur. Fonksiyonun prototipi aşağıdaki gibidir: #include char *strsignal(int signum); Ayrıca "glibc" kütüphanesinde de POSIX standartlarında bulunmayan iki fonksiyon daha bulunmaktadır. Bunlar "sigdescr_np" ve "sigabbrev_np" isimli fonksiyonlardır. Fonksiyonların prototipleri de aşağıdaki gibidir: #include const char *sigdescr_np(int sig); const char *sigabbrev_np(int sig); Fakat bu iki fonksiyonu kullanmadan evvel kaynak dosyasının yukarısında "_GNU_SOURCE" sembolik sabitini tanımlamalıyız. Bu üç fonksiyonun kullanımına ilişkin örnek aşağıdaki gibidir: * Örnek 1, #define _GNU_SOURCE #include #include #include int main() { /* # OUTPUT # Terminated TERM Terminated */ printf("%s\n", strsignal(SIGTERM)); printf("%s\n", sigabbrev_np(SIGTERM)); printf("%s\n", sigdescr_np(SIGTERM)); } Son olarak "kill" fonksiyonu ile bir prosese "0" numaralı sinyali göndererek o prosesin hala bulunup bulunmadığını, yani sonlanmamış olduğunu test edebiliriz. Aslında "0" numaralı bir sinyal mevcut değildir fakat bu değer bu amaçla kullanılmaktadır. Aslında "0" numaralı sinyal prosese hiç gönderilmemektedir. "kill" fonksiyonu ile "0" numaralı sinyali prosese gönderdiğimizde fonksiyon başarılı oluyorsa, o prosesin sistemde bulunduğu anlarız. Fakat fonksiyon bir şekilde başarısız oluyorsa ve "errno" değişkeninin değeri de "ESRCH" değerini alıyorsa, ilgili prosesin sistemde bulunmadığını anlarız. Eğer "errno" değişkeni değer olarak "EPERM" değerini almışsa, ilgili prosesin sistemde bulunduğunu fakat uygun önceliğe sahip olmadığını anlarız. Özetle; if(kill(pid, 0) == -1 && errno == ESRCH){ /* Proses Sonlanmış */ } else { /* Proses Sonlanmamış */ } >> "kill" kabuk komutu: Pekala bir prosese komut satırından da sinyal gönderebiliriz. Bunun için "kill" isminde kabuk komutu bulundurulmaktadır ki zaten bu fonksiyon da "kill" fonksiyonu kullanılarak, yukarıda bizim yaptığımıza benzer biçimde, yazılmıştır. Bu komut ile bir prosese sinyal gönderilirken o sinyalin isminin başındaki "SIG" ismi belirtilmemelidir. Örneğin, kill -USR1 12767 komutunu çalıştırdığımız zaman ID numarası 12767 olan prosese "SIGUSR1" sinyali gönderilecektir. Pek tabii buradaki sinyal ismi yerine o sinyalin numarasını da belirtebiliriz fakat sinyal numaralarının taşınabilir olmadığını, sistemden sisteme hangi sinyale ilişkin olduğunu da UNUTMAYINIZ. Bu "kill" komutu sinyal ismi ya da numarası kullanmadan belirtilmeden kullanılırsa, varsayılan durumda, "SIGTERM" sinyalini göndermektedir. Örneğin, kill 12801 komutunu çalıştırdığımız vakit "12801" ID numaralı prosese "SIGTERM" sinyali gönderilecektir. Bu sinyal bir prosesi sonlandırmak için kullanılmaktadır. Bu sinyal türü "ignore" edilebilir, proses tarafından bloke edilebilir ya da bu sinyal için bir "handler" fonksiyonu "set" edilebilir. Bir prosesi sonlandırmak için kullanılan ikinci sinyal ise "SIGKILL" sinyalidir. Bu sinyalin "SIGTERM" sinyalinden farkı ise onun gibi "ignore" ve bloke EDİLEMEZ ve bu sinyal için "handler" fonksiyon "set" EDİLEMEZ oluşudur. Eğer "sigaction" ile "SIGKILL" sinyali için bir "handler" tanımlanmaya çalışılırsa fonksiyon başarısız olur ve "errno" değeri "EINVAL" değerini alır. Bu durumda bir prosesi garantili sonlandırmak için "SIGKILL" sinyali gönderilmelidir. Örneğin, kill -KILL 12801 komutu "12801" ID numaralı prosesi sonlandıracaktır. >> "raise" fonksiyonu: Bir prosesin kendisine sinyal göndermesi için kullanılan bir fonksiyondur. "kill" fonksiyonuna nazaran senkron bir fonksiyondur. Fonksiyonun prototipi şu şekildedir: #include int raise(int sig); Fonksiyon argüman olarak gönderilecek sinyalin numarasını parametre olarak alır. Başarı durumunda "0", hata durumunda ise sıfır dışı bir değere geri döner. Bu fonksiyon aynı zamanda Standart C fonksiyonudur. Fakat C Standartlarında sinyal olgusundan bahsedilmiş ve hiç bir ayrıntısına girilmemiştir. * Örnek 1, #include #include #include #include void signal_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 1 2 3 4 5 A signal occured!.. 6 ... */ struct sigaction sa, sa_old; sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, &sa_old) == -1) exit_sys("sigaction"); for(int i = 0; i < 60; ++i){ printf("%d\n", i); if(i == 5) raise(SIGUSR1); sleep(1); } return 0; } void signal_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan POSIX standartlarına göre, çok "thread" li bir ortamda bu fonksiyon hangi "thread" tarafından çağrılmışsa, senkronluğu sağlamak adına, ilgili sinyal "handler" fonksiyonu o "thread" tarafından çalıştırılır. >> Sinyallerin prosesler karşısında bloke edilmeleri: Prosesler bazı sinyalleri kendilerine karşı bloke etmektedir. Böylesi bir durumda o sinyal oluşursa, işletim sistemi bu sinyali o prosese teslim etmez ve askıda bekletir. Eğer proses ilgili sinyale karşı olan blokesini kaldırırsa, işletim sistemi de ilgili sinyali prosese teslim edilir. Ancak sinyaller askıya alındıklarında biriktirilmezler. Yani bir proses bir sinyale kendini bloke etmişse ve bu sırada o sinyal türünden birden fazla sinyal gelmesi halinde, blokenin kaldırılmasının ardından sadece bir defa teslim işlemi gerçekleştirilir. Peki bizler bir "t" anında prosesimizin kendini hangi sinyallere karşı bloke ettiğini nasıl anlarız? UNIX türevi işletim sistemleri her bir proses için bir "signal mask" kümesi tutmaktadır. Bu küme prosesin o anda hangi proseslere karşı kendini bloke ettiğini belirtmektedir. "thread" konusu UNIX türevi işletim sistemlerine girmesiyle her bir "thread" için bir "signal mask" kümesi tutulur olmuştur. Bir sinyalin bu kümeye girmesi durumunda, ilgili proses nezdinde sinyalimiz bloke edilecektir. İşte bu "signal mask" kümesi üzerinde işlem yapabilmek için "sigprocmask" isimli POSIX fonksiyonu kullanılmaktadır. >>> "sigprocmask" fonksiyonu: #include int sigprocmask(int how, const sigset_t * set, sigset_t * oset); Fonksiyonun birinci parametresi "signal mask" kümesi üzerinde ne yapılacağını belirtmektedir. Bu parametre şunlardan birisi olabilir: -> "SIG_BLOCK" : Fonksiyonun ikinci parametresi ile belirtilen sinyalleri, halihazırdaki "signal mask" kümesi içerisine eklemek. -> "SIG_UNBLOCK" : Fonksiyonun ikinci parametresi ile belirtilen sinyalleri, halihazırdaki "signal mask" kümesi içerisinden çıkartmak. -> "SIG_SETMASK" : Fonksiyonun ikinci parametresi ile belirtilen sinyalleri, halihazırdaki "signal mask" kümesi haline getirmek. Fonksiyonun ikinci parametresi ise "sigaction" fonksiyonunda gördüğümüz "sigset_t" türündendir. Anımsanacağı üzere bu tür sinyalleri "bit" düzeyinde ifade etmek için kullanılmaktadır. Fonksiyonun üçüncü parametresi ise prosesin daha önceki "signal mask" kümesinin yerleştirileceği nesneyi belirtmektedir. Aslında ikinci ve üçüncü parametrelere "NULL" değeri de geçilebilir ki bu durumda bu parametreler fonksiyon tarafından kullanılmayacaktır. Fonksiyon başarı durumunda "0", hata durumunda ise "-1" değerine geri dönmektedir. Son olarak prosesin "signal mask" kümesinde "SIGKILL" ya da "SIGSTOP" sinyalleri bulunsa bile bu sinyalleri bloke edemiyoruz ve "sigprocmask" fonksiyonu başarısız OLMAMAKTADIR. * Örnek 1, Aşağıdaki örnekte "SIGTERM" sinyali beş saniyeliğine proses nezdinde bloke edilmiştir. Bu beş saniye içerisinde dışarıdan "SIGTERM" sinyali gelmesi halinde sinyal teslim edilmeyecek, blokenin kalması ile birlikte teslim edilecektir. #include #include #include #include void exit_sys(const char* msg); int main() { sigset_t sset, old_sset; sigemptyset(&sset); sigaddset(&sset, SIGTERM); // Blokesi istenen sinyaller bir küme haline getirildi. if(sigprocmask(SIG_BLOCK, &sset, &old_sset) == -1) // Daha sonra prosesin "signal mask" kümesine eklendi. exit_sys("sigprocmask"); // "SIGTERM" bu noktada blokelidir. printf("sleep for 15 seconds while SIGERM is being blocked\n"); sleep(5); // APPROACH - I if(sigprocmask(SIG_UNBLOCK, &sset, NULL) == -1) // Daha sonra prosesin "signal mask" kümesinden çıkartıldı. exit_sys("sigprocmask"); // APPROACH - II // if(sigprocmask(SIG_SETMASK, &old_sset, NULL) == -1) // Daha sonra prosesin "signal mask" kümesinden çıkartıldı. // exit_sys("sigprocmask"); // "SIGTERM" bu noktada blokesizdir. printf("program continues running!..\n"); sleep(5); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan o an askıda olan sinyallerin kümesini de elde edebiliriz. Bunun için "sigpending" isimli POSIX fonksiyonu kullanılmaktadır. >>> "sigpending" fonksiyonu: #include int sigpending(sigset_t *set); Fonksiyon askıdaki sinyalleri, argüman olarak geçtiğimiz "sigset_t" nesnesi içerisine yerleştirmektedir. Bu aşamada programcı belli bir sinyalin bu kümede olup olmadığını, yukarıda detayları işlenen "sigismember" fonksiyonu ile, öğrenebilir. Fakat "sigpending" fonksiyonu pek kullanılan bir fonksiyon da değildir. >> "pause" fonksiyonu: Belli bir sinyal oluşana kadar ilgili prosesi blokede bekletmek içindir. Fonksiyonun prototipi şu şekildedir: #include int pause(void); Fonksiyon sadece "-1" değerine geri dönmektedir ve "errno" değişkeni yalnızca "EINTR" değerini almaktadır. Dolayısıyla fonksiyonun başarısını kontrol etmeye lüzum yoktur. Burada şu noktalara dikkat etmeliyiz: -> Sinyal oluştuğunda herhangi bir "handler" fonksiyon "set" edilmemişse, "pause" fonksiyonu zaten geri dönmeyecek ve proses sonlandırılacaktır. -> Eğer böylesi bir fonksiyon "set" edilmişse, önce o fonksiyon çalıştırılmakta daha sonra "pause" geri dönmektedir. Bu fonksiyonu aşağıdaki biçimde şöyle kullanabiliriz: for(;;) pause(); Burada arzu edilen sinyal geldikçe iş yapan, aksi durumda uykuda bekleyen bir akış söz konusudur. Tabii böylesi bir programı sonlandırmanın bir yolu ise sinyal fonksiyonu "set" edilmemiş o sinyali bu prosese göndermek olabilir. * Örnek 1, Aşağıdaki programa "SIGUSR1" sinyali geldikçe programın akışı "for" döngüsünden çıkıp "sig_handler" fonksiyonuna girecek, daha sonra tekrardan "for" döngüsüne geri dönecektir. #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); int main() { struct sigaction sa, sa_old; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, &sa_old) == -1) exit_sys("sigaction"); for(;;) pause(); return 0; } void sig_handler(int sno) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "threads" ve "signals" : Sinyaller ilk UNIX sistemlerinden beri var olan kavramlarken, "thread" kavramı 90'lı yıllardan itibaren var olmaya başlamıştır. Dolayısıyla "thread" kavramının olmadığı dönemlerde proseslerin tek bir akışı olduğundan, sinyaller o akışa gönderiliyordu. 90'lı yıllarda "thread" kavramının yaygınlaşması ile sinyal konusunda da bazı revizeler yapılmıştır. Örneğin, hangi "thread" in o sinyal fonksiyonunu çalıştıracağı POSIX standartlarınca işletim sistemini yazanların isteğine bırakılmıştır. Bununla birlikte bir "thread", kendi prosesi içerisindeki başka "thread" e sinyal gönderebilmektedir. Anımsanacağınız üzere "thread" lerin ID değerleri kendi proseslerinde anlamlıdır. Yani başka bir prosesin "thread" ine direkt olarak sinyal gönderememekteyiz. Tabii bir "thread" e sinyal göndermek demek, ilgili sinyal fonksiyonunun o "thread" tarafından çalıştırılmasını sağlamak demektir. İşte bir "thread" ile kendi prosesimizdeki başka bir "thread" e sinyal gönderebilmek için de "pthread_kill" POSIX fonksiyonu bulundurulmuştur. >>> "pthread_kill" fonksiyonu: Fonksiyonun prototipi aşağıdaki gibidir. #include int pthread_kill(pthread_t thread, int sig); Fonksiyonun birinci parametresi hedef "thread" in ID değerini, ikinci parametresi ise gönderilecek sinyalin numarasını almaktadır. Başarı durumunda "0", hata durumunda hata kodunun kendisi ile geri dönmektedir. Tabii ilgili sinyal için bir sinyal fonksiyonu "set" edilmemişse, yalnızca ilgili "thread" değil, bütün proses sonlandırılacaktır. * Örnek 1, Aşağıdaki örnekte "main-thread" diğer "thread" e "SIGUSR1" sinyalini göndermiştir. #include #include #include #include #include #include void* thread_proc(void* param); void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # Main is running: 0 Thread is running: 0 Main is running: 1 Thread is running: 1 ... Main is running: 5 A signal occured!.. Thread is running: 5 Main is running: 6 Thread is running: 6 ... Main is running: 9 Thread is running: 9 */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); pthread_t tid; int result; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); for(int i = 0; i < 10; ++i){ printf("Main is running: %d\n", i); if(i == 5 && (result = pthread_kill(tid, SIGUSR1)) != 0) exit_sys_errno("pthread_kill", result); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 10; ++i){ printf("Thread is running: %d\n", i); sleep(1); } return NULL; } void sig_handler(int sno) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "main-thread" diğer "thread" e "SIGUSR2" sinyalini göndermiştir. Fakat o sinyal için "set" edilmiş bir fonksiyon olmadığı için proses sonlandırılmıştır. #include #include #include #include #include #include void* thread_proc(void* param); void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # Main is running: 0 Thread is running: 0 Main is running: 1 Thread is running: 1 Main is running: 2 Thread is running: 2 Main is running: 3 Thread is running: 3 Main is running: 4 Thread is running: 4 Main is running: 5 */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); pthread_t tid; int result; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); for(int i = 0; i < 10; ++i){ printf("Main is running: %d\n", i); if(i == 5 && (result = pthread_kill(tid, SIGUSR2)) != 0) exit_sys_errno("pthread_kill", result); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 10; ++i){ printf("Thread is running: %d\n", i); sleep(1); } return NULL; } void sig_handler(int sno) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Pekala bizler belli "thread" leri belli sinyallere karşı blokeli hale getirebiliriz. Çünkü UNIX/Linux sistemlerinde her bir "thread" için de bir "signal mask" kümesi bulunmaktadır. Bunun için de bizler "pthread_sigmask" fonksiyonunu kullanmalıyız. >>> "pthread_sigmask" fonksiyonu: Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_sigmask(int how, const sigset_t * set, sigset_t * oset); Fonksiyonun parametrik yapısı "sigprocmask" ile birebir aynıdır. Ancak geri dönüş değeri başarı durumunda "0", hata durumunda ise hata kodunun kendisi biçimindedir. Hangi "thread" bu fonksiyonu çağırmışsa, onun "signal mask" kümesi etkilenecektir. Bu fonksiyonun tipik kullanım biçimi bir sinyalin spesifik bir "thread" tarafından ele alınması şeklindedir. Bunun için o sinyalin ilgili "thread" hariç aynı prosesteki bütün sinyallere karşı bloke edilmesi gerekmektedir. Böylelikle bloke edilmeyen o "thread" ilgili sinyali işleyecektir. Pekala bir prosesin bütün "thread" leri bir sinyale karşı bloke edilmesi demek, o prosesin ilgili sinyale karşı bloke edilmesi demektir. Pek tabii Linux sistemlerinde "kill" fonksiyonunu kullanarak, "gettid" ile "ID" değerini elde ettiğimiz "thread" leri sonlandırabiliriz. Çünkü sinyal adeta o "thread" e gönderilmiş gibi bir etki oluşacaktır. Çünkü Linux sistemlerinde "thread" ler de birer proses olarak ele alınmaktadır. Yani Linux sistemlerinde başka proseslerin "thread" lerine bu yöntemle sinyal gönderebiliriz. Fakat POSIX standartlarınca bunu gerçekleştirmek mümkün değildir. Bu durum Linux-kernel tasarımından dolayıdır. Seyrek olarak programcılar, kritik bir takım işlemler yaparken, sinyalleri "pthread_sigmask" ile bloke edip, daha sonra blokeyi kaldırıp, "pause" ile de ilgili sinyalin gelmesini beklemek isteyebilir. Böylelikle ilgili sinyal gelmediği sürece akış ilerlemeyecektir. Örneğin, pthread_sigmask(); // Kritik Kod // ... pthread_sigmask(); <------- TEHLİKELİ ALAN pause(); // Bu kısma sinyal geldiğinde geçilmek istenmektedir. Burada gerçekleştirilmek istenen şey, şekilden de görüleceği üzere, sinyallerin blokesini kaldırdıktan hemen sonra "pause" ile ilgili sinyalin gelmesini beklemek ve sinyal geldikten sonra programın akışının ilgili kısma geçmesini sağlamaktır. Fakat programın akışı tam da TEHLİKELİ ALAN noktasındayken bir sinyal gelmesi durumunda bu sinyal teslim edilebilir. Dolayısıyla prosesimiz ya sonlanacak ya da ilgili sinyal fonksiyonu işletilecektir. Daha sonrasında da akış "pause" fonksiyonuna girecektir. Beklenen sinyal halihazırda geldiği için tekrar gelmeyeceğinden, programın akışı bu "pause" fonksiyonundan çıkamayacaktır. Dolayısıyla bizler sinyallerin blokesini kaldırma işlemi ile "pause" işlemini atomik olarak gerçekleştirmemiz gerekmektedir. İşte bunun için "sissuspend" fonksiyonunu bulundurulmuştur. >>> "sigsuspend" fonksiyonu: Fonksiyonun prototipi aşağıdaki gibidir: #include int sigsuspend(const sigset_t *sigmask); Fonksiyon parametre olarak yeni "signal mask" kümesini alır, bu kümeyi bu fonksiyonu çağıran "thread" in "signal mask" kümesi yapar ve atomik bir biçimde de "pause" işlemini gerçekleştirir. Böylece yukarıda bahsedilen blokenin açılıp "pause" fonksiyonunda bekleme atomik hale getirilmiş olur. Fonksiyonun geri dönüş değeriyse şunlardan birisidir: -> Eğer proses ilgili sinyal için herhangi bir fonksiyon "set" etmemişse, proses sonlanacağı için, fonksiyon hiç geri dönmeyebilir. -> Eğer bir sinyal fonksiyonu "set" edilmişse, fonksiyon "-1" ile geri döner ve "errno" değişkeni "EINTR" değerini alır. Tabii burada ilgili sinyalin gelmesi ile fonksiyondan çıkılmasıyla birlikte o "thread" in evvelki "signal mask" kümesi yeniden "set" edilir. POSIX standartları bu fonksiyonun sadece ilgili "thread" e ait olan "signal mask" kümesini etkilemektedir. "thread" kavramı olmadan önce ise prosesin "signal mask" kümesini etkilemektedir. Dolayısıyla "thread" lerin kullanılmadığı bir ortamda bu fonksiyonu kullanmamız halinde prosesin "signal mask" kümesi etkilenmekte, "thread" lerle birlikte bu fonksiyonu çağıran "thread" in "signal mask" kümesi etkilenmektedir. O halde "sigsuspend" fonksiyonunun işleyişini şu şekilde de temsili olarak gösterebiliriz: pthread_sigmask(SIG_SETMASK, &sset, &oldset); pause(); pthread_sigmask(SIG_SETMASK, &oldset, &NULL); Son olarak "sigsuspend" fonksiyonunun geri dönüş değerinin kontrol edilmesine gerek yoktur. * Örnek 1, Aşağıdaki program çalışırken bir başka "terminal" üzerinden "kill" komutu ile bu prosese "SIGUSR1" sinyali göndermelisiniz. #include #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { struct sigaction sa; sigset_t sset, old_sset; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); sigfillset(&sset); // Burada bütün sinyaller bloke edilmiştir. if(sigprocmask(SIG_BLOCK, &sset, &old_sset) == -1) exit_sys("sigprocmask"); for(int i = 0; i < 10; ++i){ /* (I) * Programın akışı buradayken bir sinyal geldiğinde ilgili sinyal * askıda bekletilecektir. Böylelikle önem kritik işlemler sırasında * rahatsız edilmemiş olacağız. */ printf("%d\n", i); sleep(1); } /* (II) * Programın akışı bu fonksiyona girmesiyle birlikte ilgili sinyal gelmemişse * programın akışı "suspend" edilecektir. Ta ki beklenen sinyal gelene dek. Eğer * bu bekleme sırasında ilgili sinyal gelirse veya halihazırda askıda bir sinyal * varsa ona ilişkin "handler" fonksiyon çalıştırılacak ve programın akışı da * "sigsuspend" fonksiyonunu tamamlayacaktır. */ sigsuspend(&old_sset); /* (III) * Dolayısıyla programın akışının buraya gelebilmesi için ya askıda bir sinyal olması * ki böylelikle ilgili "handler" fonksiyonunun çağrılacak ya da "suspend" sırasında * bir sinyal gelmesi ve ona ait "handler" fonksiyonunun çağrılması gerekmektedir. */ printf("Ok\n"); return 0; } void sig_handler(int sno) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Aslında "sigsuspend" fonksiyonunun kullanımının nerelerde gerektiği biraz zor anlaşılmaktadır. Çünkü bu fonksiyona ihtiyaç aslında oldukça seyrektir. Genellikle şu tipik senaryoda bu fonksiyona ihtiyaç duymaktayız: -> Programcı başka bir prosesten bir sinyal beklemekte ve ancak o sinyal geldiğinde koduna devam etmek istemektedir. Fakat bu sinyali beklemeden evvel sinyalleri bloke ederek bazı önem-kritik işlemler de yapmak isteyebilir. (Dolayısıyla bu süre zarfında beklenen sinyal gelirse, sinyal askıya alınacaktır.) Programcı daha sonra sinyalleri açarak "pause" işlemi ile diğer prosesten gelecek sinyalini bekleyecektir. Eğer "sigsuspend" fonksiyonu olmasaydı, sinyalleri açtığımız nokta ile "pause" yapmak istediğimiz nokta arasında beklenen sinyalin gelmesi durumunda artık o sinyale ilişkin "handler" fonksiyonu işletilecektir. Benzer şey sinyalleri açmadan evvel beklenen sinyalin gelmesi durumunda da geçerlidir. Programın akışı ilgili "handler" fonksiyonundan çıkıp "pause" fonksiyonuna gireceğinden, sonsuz bekleme oluşacaktır. Çünkü beklenen sinyal zaten gelmiştir. Buradan da görüleceği üzere bu fonksiyonu: -> Ya önem-kritik işlemler sırasında rahatsız edilmemek için kullanıyor. -> Ya bir işi yapmak için bir sinyalin gelmesini bekliyorsak kullanıyoruz. Pek tabii bir prosesin diğer prosesin işini yapmasını beklemesi, prosesler arası haberleşme yöntemleri ile de mümkündür. Ancak bu tür durumlar için sinyallerin kullanılması daha pratiktir. Bu yüzdendir ki prosesler arasında haberleşme yöntemi olarak sinyalleri de kullanabiliriz. Son olarak şunu da belirtmekte fayda vardır; sinyal fonksiyonu içerisinde "errno" değişkeninin değerini değiştirme potansiyeline sahip bir fonksiyon çağrısı varsa, bu durum sorunlara yol açabilir. Anımsanacağı üzere bazı POSIX fonksiyonları başarı durumunda da "errno" değişkenini değiştirebilmektedir. Buradaki sorun ise şu şekilde açıklanabilir: void signal_handler(int sig_no) { if(some_posix_func() == -1){ <--- (I) : Tam bu noktada bir sinyal geldiğini varsayalım. perror("some_posix_func"); exit(EXIT_FAILURE); } } Buradaki "(I)" noktasında bir sinyal gelmesi durumunda "errno" değişkeninin yeni değeri "some_posix_func" fonksiyonuna göre belirlenecektir. Fakat bizim beklentimiz, "signal_handler" fonksiyonu tarafından bunun belirlenmesidir. Dolayısıyla bizler yanlış değeri ekrana yazdırmış olacağız. Bunun için "errno" değerini işin başında bir değerde saklayıp, işin sonunda bu değer ile yeniden "set" işlemi yapmamız gerekmektedir. Şöyleki: void signal_handler(int sig_no) { int temp_errno = errno; // Bir takım işlemler... errno = temp_errno; } Aşağıda bu konuyla ilgili bir örnek verilmiştir: * Örnek 1, Aşağıdaki örnekte ise hata kodu yanlış bastırılmıştır. Çünkü "raise" fonksiyonunun çağrılma sebebi, ilgili dosyanın bulunamamasıdır. Dolayısıyla ekrana bu hata kodu yazdırılmalıdır. Fakat "errno" değişkeni ilgili sinyal fonksiyonunda yeniden "set" edildiği için hata kod mesajı da değişecektir. #include #include #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # A signal occured!.. open: Operation not permitted */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); int fd; if((fd = open("file_not_found", O_RDONLY)) == -1){ raise(SIGUSR1); exit_sys("open"); } printf("Ok\n"); return 0; } void sig_handler(int sno) { printf("A signal occured!..\n"); kill(1, SIGKILL); /* kill will fail */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte olması gereken hata kodu yazdırılmıştır. Çünkü ilgili dosya bulunamadığı için "raise" fonksiyonu çağrılacaktır. Bu durumda da hata kodunun ilgili dosyanın bulunamadığına ilişkin olmalıdır. #include #include #include #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # A signal occured!.. open: No such file or directory */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); int fd; if((fd = open("file_not_found", O_RDONLY)) == -1){ raise(SIGUSR1); exit_sys("open"); } printf("Ok\n"); return 0; } void sig_handler(int sno) { int temp_errno = errno; printf("A signal occured!..\n"); kill(1, SIGKILL); /* kill will fail */ errno = temp_errno; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >> Sinyal Güvenli Fonksiyon Kavramı ("Asynchronous Signal Safe Functions"): Bizler bir fonksiyonun içerisinde olduğumuzu, bu noktada bir sinyal geldiğini ve bu durumda da ilgili sinyale ilişkin "handler" fonksiyonun çalıştığını düşünelim. İş bu "handler" fonksiyon, sinyal gelmeden içerisinde bulunduğumuz fonksiyonu çağırsa ne olacaktır? Bu duruma iç içe çağırma durumu denmekte ve "thread-safe" durumuna benzemektedir. POSIX standartları böylesi iç içe çağrılabilecek fonksiyonlara ise "async-signal-safe" fonksiyonlar ismini vermektedir. Bütün asenkron sinyal güvenli POSIX fonksiyonların listesi "System Interfaces/General Information/Sinal Concepts" başlığı altında listelenmektedir. Bu listedeki fonksiyonları hem dışarıda hem de "handle" fonksiyonlarda rahatlıkla kullanılabilir. Burada belirtilmeyen fonksiyonları ise ya dışarıda ya da "handle" fonksiyonu içerisinde kullanmalıyız. Öte yandan "async-signal-safe" fonksiyonlar, aynı zamanda "thread-safe" fonksiyonlardır. Fakat her "thread-safe" fonksiyon aynı zamanda "async-signal-safe" fonksiyon DEĞİLDİR. Çünkü "async-signal-safe" fonksiyonlar, "thread-safe" fonksiyonların daha katı biçimidir. ÖZETLE; bir fonksiyonun kesilerek aynı "thread" tarafından yeniden çağrılmasına "async-signal-safe", farklı "thread" ler tarafından olması durumuna ise "thread-safe" denir. (see https://en.wikipedia.org/wiki/Reentrancy_(computing) for more info.) * Örnek 1.0, Ne "thread-safe" ne de "async-signal-safe": int tmp; void swap(int* x, int* y) { tmp = *x; *x = *y; /* Hardware interrupt might invoke isr() here. */ *y = tmp; } void isr() { int x = 1, y = 2; swap(&x, &y); } int main() { //... } * Örnek 1.1, "thread-safe" ama "async-signal-safe" DEĞİL: _Thread_local int tmp; void swap(int* x, int* y) { tmp = *x; *x = *y; /* Hardware interrupt might invoke isr() here. */ *y = tmp; } void isr() { int x = 1, y = 2; swap(&x, &y); } * Örnek 1.2, hem "thread-safe" hem "async-signal-safe": void swap(int* x, int* y) { int tmp; tmp = *x; *x = *y; *y = tmp; /* Hardware interrupt might invoke isr() here. */ } void isr() { int x = 1, y = 2; swap(&x, &y); } Diğer yandan "sig_atomic_t" isimli bir tür daha vardır, C99 ile de C diline eklenmiştir. Bu türden global bir nesne tanımlandığında, bu nesne üzerindeki atama işlemleri atomik bir biçimde gerçekleştirilecektir. Ancak "++", "--" gibi işlemlerin atomik olacağının bir garantisi YOKTUR. Bu tür aynı zamanda "volatile" özelliğini de bazı derleyiciler nezdinde kapsamaktadır. >> Proseslerin Durdurulup Yeniden Çalıştırılması: Burada devreye "SIGSTOP" ve "SIGCONT" sinyalleri devreye girmektedir ki bu sinyaller ilgili prosesi sırasıyla "suspend" eder ve "suspend" halini kaldırır. "SIGSTOP" ile "suspend" edilen bir proses, "SIGCONT" sinyali gelen kadar "suspend" durumunu korur. Bu sinyallerden "SIGSTOP" sinyali, tıpkı "SIGKILL" sinyalinde olduğu gibi, "ignore" ve bloke EDİLEMEZ. Ayrıca "SIGSTOP" sinyali için bir fonksiyon "set" EDİLEMEMEKTEDİR. * Örnek 1, Aşağıdaki programı bir terminalden çalıştırıp, ikinci bir terminal üzerinden de "SIGSTOP" ve "SIGCONT" sinyalleri göndererek durumu gözlemleyebiliriz. #include #include #include void exit_sys(const char* msg); int main() { for(int i = 0; i < 20; ++i){ printf("%d\n", i); sleep(1); } } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "suspend" edilen prosesler, "ps -u" komutu çalıştırıldığında karşımıza çıkan tablonun "STAT" sütununda, "T" harfi ile görülürler. Diğer yandan klavyeden "CTRL+Z" tuşuna basıldığında, terminal aygıt sürücüsü devreye girer ve oturumun ön plan proses grubuna "SIGSTOP" sinyali gönderir. Yani klavyeden bu tuş kombinasyonu yaparak, o an çalışmakta olan programa, "SIGSTOP" sinyali göndertebilmekteyiz. Bu biçimde, kabuk üzerinden durdurulan prosesler, "fg" kabuk komutu ile kaldığı yerden çalışmaya devam ettirilebilmektedir. Bu komut ise aslında ilgili prosese "SIGCONT" sinyalini göndermektedir. Yukarıdaki örnek üzerinde bu kabukları şu şekilde çalıştırabiliriz: $ ./mample 1 2 3 ... <--- Tam bu noktada "CTRL+Z" tuşuna basıldı. [2]+ Durdu ./mample $fg 3 15 16 17 ... Pekiyi bizim prosesimiz bir "child" proses oluşturmuşsa durum nasıl olacaktır? Bu durumda ilgili proses grubundakilerin hepsine bu sinyal gönderilmektedir. (Çünkü "fork" yaptığımız zaman alt proses ve üst proses aynı proses grup içerisinde bulunur. Proses grupları, oturum kavramı gibi konular ileride ele alınacaktır.) * Örnek 1, Aşağıdaki programı çalıştırdıktan sonra "CTRL+Z" tuşuna bastığınız zaman hem alt hem de üst prosesin "suspend" edildiğini göreceksiniz. #include #include #include #include #define COUNT_DOWN 5 void exit_sys(const char* msg); int main() { pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0){ for(int i = COUNT_DOWN; i > 0; --i){ printf("Parent: %d\n", i); sleep(1); } if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else{ for(int i = 0; i < COUNT_DOWN; ++i){ printf("Child: %d\n", i); sleep(1); } _exit(0); } } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan "wait" fonksiyonları ile alt proses beklenirken, bu beklemeden ancak alt proses sonlandığında çıkabilmektedir. Alt prosesin sonlanması ise iki biçimde olabilmektedir. Bunlar "exit" ya da "_exit" fonksiyonlarının çağrılması ve bir sinyal dolayısıyla olmaktadır. Anımsanacağı üzere bizler "wait" fonksiyonlarının "status" parametrelerini "WIFEXITED" ve "WIFSIGNALED" makrolarına sokarak bu durumu anlayabiliyorduk. Zaten prosesin "exit code" bilgisinin oluşabilmesi için onun da normal bir biçimde sonlanmış olması gerekiyordu. Dieğr yandan "waitpid" fonksiyonunun üçüncü parametresine ise "WUNTRACED" ve/veya "WCONTINUED" bayrakları geçilirse bu durumda "waitpid" fonksiyonu alt proses "suspend" edildiğinde ve yeniden çalıştırıldığında da sonlandırılacaktır. Bu durumda da "status" parametresi "WIFSTOPPED" ve "WIFCONTINUED" makrolarına sokularak bu durum anlaşılabilmektedir. >> Bazı önemli sinyallerin incelenmesi: >>> Anımsanacağı üzere proses sonlandırma yöntemlerinden bir diğeri de "abort" fonksiyon çağrısı iledir. Bu çağrıyı, "exit" ile sonlandırma yapamayacak durumlarda yapmalıyız. "abort" çağrısı sonrasında "SIGABRT" sinyalinin oluşmasına neden olur. "SIGABRT" sinyalinin varsayılan davranışı ise, UNIX türevi sistemlerde, "core" dosyasının oluşmasıdır. Böylelikle "debugger" altında iş bu dosyayı inceleyebiliriz. "SIGABRT" sinyali için başka sinyal fonksiyonlar "set" edilmiş olsa bile, o fonksiyonun çalışması bittikten sonra, yine de proses sonlandırılır. Diğer yandan "SIGABRT" sinyali "block" ve "ignore" edilmişse bile, proses yine de sonlandırılır. Bu sinyal ile prosesin sonlandırılmasının engellenmesinin tek yolu, sinyal fonksiyonu içerisinde "long jump" işleminin gerçekleştirilmesidir. * Örnek 1, #include #include #include #include void sigabrt_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 0 1 2 3 4 5 SIGABRT handler */ struct sigaction sa; sa.sa_handler = sigabrt_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGABRT, &sa, NULL) == -1) exit_sys("sigaction"); for (int i = 0; i < 60; ++i) { printf("%d\n", i); sleep(1); if (i == 5) abort(); } return 0; } void sigabrt_handler(int sig) { printf("SIGABRT handler\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>> Diğer yandan UNIX ve türevi sistemlerde işlemci tarafından oluşturulan içsel kesmelerden kaynaklı da proseslere sinyaller gönderilmektedir. Bunlardan en çok karşılaşılanı "SIGSEGV" isimli sinyaldir. "SIGSEGV" sinyali, işletim sistemi tarafından programımıza tahsis edilmemiş bir bölgeye erişmeye çalıştığımız zaman, işletim sistemi tarafından prosese gönderilir ve varsayılan davranışı yine prosesi sonlandırmasıdır. Bu sinyal de "block" ve "ignore" EDİLEMEZLER. Bu sinyal oluştuğunda ve sinyal fonksiyonu da "set" edilmişse, sinyal fonksiyonu çalıştırılır. Ancak çalışması bittiğinde, proses yine sonlandırılır. Bu sinyal ile prosesin sonlandırılmasının engellenmesinin tek yolu, sinyal fonksiyonu içerisinde "long jump" işleminin gerçekleştirilmesidir. * Örnek 1, Aşağıdaki program Linux sistemlerinde çalıştırılmıştır. Bu sistemlerde eğer sinyal fonksiyonu içerisinde "exit" çağrısı yapılmamışsa, aynı sinyal yeniden oluşturulur, aynı sinyal fonksiyonu yeniden çağrılır. Fakat bazı UNIX türevi sistemlerde sinyal fonksiyonunun sonlanmasını takiben proses de sonlandırılmaktadır. #include #include #include #include void sigsegv_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # SIGSEGV handler SIGSEGV handler SIGSEGV handler SIGSEGV handler ... */ struct sigaction sa; sa.sa_handler = sigsegv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGSEGV, &sa, NULL) == -1) exit_sys("sigaction"); char* ptr = (char*)0x123456789; // Programımız için tahsis edilmemiş bir alana erişimden kaynaklı // sinyal gönderilmiştir. putchar(*ptr); return 0; } void sigsegv_handler(int sig) { printf("SIGSEGV handler\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>> Bir diğer önemli sinyal de "SIGCHLD" isimli sinyaldir. Bu sinyal proses oluşturulan bölümde bahsetmiştik. Şöyleki; UNIX ve türevi sistemlerde alt proses sonlanırken, üst prosese bu sinyali göndermektedir. Eskiden bu sinyalin ismi "SIGCLD" ismindeydi ve artık POSIX standartları "SIGCLD" sinyalini desteklememektedir. İşte "zombie process" oluşumunu engellemenin bir yolu da bu sinyale ilişkin bir fonksiyon tanımlayıp, bu fonksiyon çağrısı sırasında "wait" çağrısı yapmaktır. Böylelikle alt proses sonlandığında ilgili sinyal fonksiyonu da çağrılacağından, "zombie process" oluşumu engellenmiş olacaktır. Tabii şu noktaya da dikkat etmeliyiz; -> Bu sinyal geldiğinde, sinyal fonksiyonu çalıştırılırken, birden fazla alt proses de o sırada sonlanmış olabilir. İşte bunlardan gönderilen sinyaller biriktirilemeyeceği için, programın akışı sinyal fonksiyonundan çıktığında, askıda olan o sinyallerden sadece bir tanesi işlenir. Diğerleri ıskarta edilir. İşte bunun çözümü ise sinyal fonksiyonu içerisindeki "wait" çağrısını bir döngü içerisinde yapmaktır. "wait" çağrısının blokeye yol açmaması için de "WNOHANG" değerini kullanmamız gerekmektedir, aksi halde bloke oluşacaktır. Diğer yandan bu sinyalin varsayılan aksiyonu, sinyalin "ignore" edilmesidir. Yani bu sinyal için herhangi bir şey yapmamışsak, sinyal yine oluşacak ancak işletim sistemi tarafından sinyal "ignore" edilecektir. * Örnek 1, #include #include #include #include #include #include void sig_handler_func(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Parent Process Child Process SIGCHLD handler */ struct sigaction sa; sa.sa_handler = sig_handler_func; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { printf("Parent Process\n"); //... pause(); } else { printf("Child Process\n"); //... _Exit(100); } return 0; } void sig_handler_func(int sig) { printf("SIGCHLD handler\n"); int errno_temp = errno; // Eğer beklenecek hiç alt proses yoksa "-1" ile, // "WNOHANG" uygulanmış ancak henüz bir alt proses // sonlanmamışsa "0" değerine geri döner. Aksi halde // proses ID değeri ile geri döner. Bu döngüyle birlikte // sonlanan bütün alt prosesler beklenmiş olacaktır. while (waitpid(-1, NULL, WNOHANG) > 0) ; errno = errno_temp; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include #include void sig_handler_func(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Parent Process Child Process SIGCHLD handler :> Parent Process Child Process SIGCHLD handler :> Parent Process Child Process SIGCHLD handler :> */ struct sigaction sa; sa.sa_handler = sig_handler_func; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid; for (int i = 0; i < 3; ++i) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { printf("Parent Process\n"); //... getchar(); // :> } else { printf("Child Process\n"); //... _Exit(100); } } return 0; } void sig_handler_func(int sig) { printf("SIGCHLD handler\n"); int errno_temp = errno; while (waitpid(-1, NULL, WNOHANG) > 0) ; errno = errno_temp; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include #include #include void sig_handler_func(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Child Process Press ENTER to continue... Child Process Child Process SIGCHLD handler SIGCHLD handler */ struct sigaction sa; sa.sa_handler = sig_handler_func; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid; for (int i = 0; i < 3; ++i) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { printf("Child Process\n"); usleep(rand() % 100000); _exit(EXIT_SUCCESS); } } printf("Press ENTER to continue...\n"); getchar(); return 0; } void sig_handler_func(int sig) { printf("SIGCHLD handler\n"); int errno_temp = errno; while (waitpid(-1, NULL, WNOHANG) > 0) ; errno = errno_temp; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan bu "SIGCHLD" sinyalinde şöyle de bir semantik vardır; anımsanacağı üzere "thread" leri "detach" ettiğimiz vakit, onların "exit code" değerini ıskarta ediyor ve bellekte kapladığı alan otomatik olarak boşaltılıyordu. Aynı işlevi bu sinyalde de yapabiliriz. Şöyleki; -> Bu sinyalin varsayılan işlevini, "fork" çağrısından evvel, açıkça "ignore" etmemiz durumunda, alt proses sonlandığında kaynakları otomatik olarak boşaltılır. Her ne kadar zaten varsayılan davranış "ignore" olsa da biz yine açıkça "ignore" etmeliyiz. Eğer açıkça "ignore" etme işlemi "fork" işleminden sonra yapılırsa, sonucun ne olacağı işletim sisteminden işletim sistemine göre değişmektedir. Ancak ilgili sinyal fonksiyonunda artık "wait" uygulayamayız. İşte bu yöntem de "zombie process" oluşumunu engellemenin bir diğer yoludur. Diğer yandan POSIX standartlarınca "sigaction" fonksiyonunda "SA_NOCLDWAIT" bayrağı da bulundurulmaktadır. Bu bayrak yalnız "SIGCHLD" sinyali için kullanılabilir. Programcı isterse bu bayrağı kullanarak da "zombie process" oluşumunun önüne geçebilir. Ancak bu bayrağın kullanılması durumunda, halihazırda "SIGCHLD" sinyali için "set" edilen fonksiyonun çağrılıp çağrılmayacağı işletim sistemine bağlıdır. * Örnek 1, #include #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Press ENTER to continue... Child Process Child Process Child Process */ struct sigaction sa; sa.sa_handler = SIG_IGN; sa.sa_flags = SA_RESTART; if(sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid; for (int i = 0; i < 3; ++i) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { printf("Child Process\n"); usleep(rand() % 100000); _exit(EXIT_SUCCESS); } } printf("Press ENTER to continue...\n"); getchar(); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Son olarak yukarıda detaylarına değinilen sinyallere ek olarak "SIGUSR1" ve "SIGUSR2" sinyalleri, programcılar kendi uygulamalarında kullansın diye, bulundurulmaktadır. Bu sinyalleri prosesler arası haberleşme amacıyla kullanabiliriz. Bu sinyallerin varsayılan eylemi ise prosesin sonlandırılmasıdır. >> Gerçek Zamanlı Sinyaller: Normal sinyaller bir takım dezavantajlara sahiptirler. Örneğin, sinyallerin biriktilememesi, sinyaller arasında önceliğin olmaması vs. Dolayısıyla 90'lı yıllarda "realtime extension" adı altında eklenmişlerdir. Fakat bu tip sinyallere isim verilmemiş, numarası "[SIGRTMIN,SIGRTMAX]" arasında olan numaralar verilmiştir. Gerçek Zamanlı Sinyallerin Normal Sinyallerden farkı şunlardır; -> Gerçek Zamanlı olanlar kuyruklanmaktadır ve o sayı kadarkiler ele alınır. Halbuki Normal sinyallerde bir sayaç mekanizması yoktur. Dolayısıyla o sinyalden kaç tane oluşmuş olursa olsun, yalnızca bir tanesi ele alınacaktır. Fakat Gerçek Zamanlı sinyallerde ise on tanesi de ele alınacaktır. -> Gerçek Zamanlı sinyallerde bir bilgi de sinyalle birlikte iliştirilebilmektedir. Bu bilgi bir "int" değer de olabilir, bir adres bilgisi de. Adres bilgisi kullanılması durumunda kullanılan adresin o proses nezdinde ANLAMLI OLMASI GEREKİR. Bir diğer ifadeyle "shared memory" kullanmamız gerekmektedir. -> Gerçek Zamanlı sinyallerde yine bir öncelik kavramı da vardır. Küçük numaralı sinyal yüksek öncelik belirtmektedir. -> Gerçek Zamanlı sinyal göndermek için "kill" değil, "sigqueue" kullanmalıyız. "kill" kullanılması durumunda kuyruklama işleminin yapılıp yapılmayacağı işletim sistemine bağlıdır. -> "set" işlemi için "signal" DEĞİL, "sigaction" kullanmalıyız. Bu fonksiyonun bazı parametreleri Gerçek Zamanlı sinyaller içindir. Dolayısıyla ilgili yapının "sa_handler" elemanı yerine "sa_sigaction" isimli elemanını kullanmalıyız. Yine bununla birlikte ilgili yapının "sa_flags" elemanına "SA_SIGINFO" bayrağını da "bit-wise OR" işlemiyle geçmeliyiz. Şöyleki; struct sigaction sa; //... sa.sa_sigaction = signal_handler_func; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; Dolayısıyla kullanılacak sinyal "handle" fonksiyonunun da prototipi aşağıdaki gibi olmak zorundadır; void signal_handler_func(int, siginfo_t*, void*); Prototipin birinci parametresi beklenen sinyal numarasıdır. İkinci parametresi ise "siginfo_t" türünden bir adres bilgisi olup, beklenen sinyale ilişkin ayrıntılı bilgileri içerir. POSIX standartlarınca "siginfo_t" türü aşağıdaki gibidir; siginfo_t { int si_signo; // Oluşan sinyalin numarası. int si_code; // Signal code. int si_errno; // O andaki "errno" değeri. pid_t si_pid; // Sinyali gönderen prosesin "Process ID" değeridir. uid_t si_uid; // Sinyali gönderen prosesin "Real User ID" değeridir. void *si_addr; // Address of faulting instruction. int si_status; // Exit value or signal. long si_band; // Band event for SIGPOLL. union sigval si_value; // Signal value. }; Prototipin üçüncü parametresi çok da önemli değildir. Gerçek Zamanlı sinyallerin numaralarının "[SIGRTMIN,SIGRTMAX]" arasında olduğunu belirtmiştik. POSIX standartlarında ise bir sistemin desteklemesi gereken asgari Gerçek Zamanlı sinyal adedi ise "_POSIX_RTSIG_MAX" sembolik sabitiyle belirtilmiştir ki değeri ise "8" dir. Dolayısıyla bir UNIX türevi sistem en az sekiz adet Gerçek Zamanlı sinyal oluşturabilmelidir. Pekala sistemimizdeki desteklenen Gerçek Zamanlı sinyal adedini ise "limits.h" başlık dosyasındaki "RTSIG_MAX" sembolik sabiti üzerinden de öğrenebiliriz. Ancak yine "RTSIG_MAX" sembolik sabitinin tanımlanması bir zorunluluk değildir, "sysconf" fonksiyonuna "_SC_RTSIG_MAX" değerini geçerek öğrenebiliriz. Bu konunun detaylarına ileride işlenecek olan Sistem Limitleri konusunda ele alınacaktır. Öte yandan Gerçek Zamanlı sinyallerin kuyruklanabilir olduğunu söylemiştik. Yani aynı sinyal birden fazla kez oluştuğunda, bu sinyaller işletim sistemi tarafından saklanmaktadır. Pekiyi bu kuyruğun uzunluk bilgisi nedir? Bu bilgi de yine sistemden sisteme değişiklik göstermektedir. Fakat POSIX standartları, bir sistemin desteklemesi gereken azami kuyruk uzunluğunu "_POSIX_SIGQUEUE_MAX" sembolik sabitiyle, "limits.h" içerisinde, belirtmiştir. Ancak bu sembolik sabit toplam kuyruk uzunluğu olup, sinyal özelinde oluşturulan kuyruk uzunluğu DEĞİLDİR. Tabii o sistemdeki gerçek kuyruk uzunluk bilgisini de "SIGQUEUE_MAX" sembolik sabitiyle de öğrenebiliriz. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # SIGRTMIN: 34 SIGRTMAX: 64 SIGRTMAX - SIGRTMIN: 30 RTSIG_MAX: 32 _POSIX_SIGQUEUE_MAX: 32 */ printf("SIGRTMIN: %d\n", SIGRTMIN); printf("SIGRTMAX: %d\n", SIGRTMAX); printf("SIGRTMAX - SIGRTMIN: %d\n", SIGRTMAX - SIGRTMIN); printf("RTSIG_MAX: %d\n", RTSIG_MAX); printf("_POSIX_SIGQUEUE_MAX: %d\n", _POSIX_SIGQUEUE_MAX); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi yukarıda belirtilen fakat tanımlanması bir zorunluluk olmayan sembolik sabitler konusu nedir? -> Belirli bir sistem limiti düşünelim. Bu limit, "boot" edildikten sonra hiç değiştirilmeyecek olsun. Bu değerin "define" edilmesinde herhangi bir sakınca bulunmamaktadır. Diğer yandan "config" edilebilir bir limit düşünelim. Dolayısıyla "boot" sürecinden sonra bunun değeri değişebilir. İşte böylesi değerleri de "define" etmeye lüzum görülmemiştir. POSIX standartları da, bu tip değerleri ortak paydada buluşturabilmek için, sistem çalışırken değeri değişebilecek olanlara "define" zorunluluğu GETİRMEMİŞTİR. O anki değerini alabilmek için de "sysconf" isimli fonksiyon geliştirilmiştir. Konunun detaylarına ileride işlenecek olan "System Limits" konusunda tekrar değinilecektir. * Örnek 1, #include #include #include #include #include int set_sigqueue_max(void); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # SIGRTMIN: 34 SIGRTMAX: 64 SIGRTMAX - SIGRTMIN: 30 RTSIG_MAX: 32 _POSIX_SIGQUEUE_MAX: 32 SIGQUEUE_MAX: 28421 */ printf("SIGRTMIN: %d\n", SIGRTMIN); printf("SIGRTMAX: %d\n", SIGRTMAX); printf("SIGRTMAX - SIGRTMIN: %d\n", SIGRTMAX - SIGRTMIN); printf("RTSIG_MAX: %d\n", RTSIG_MAX); printf("_POSIX_SIGQUEUE_MAX: %d\n", _POSIX_SIGQUEUE_MAX); printf("SIGQUEUE_MAX: %d\n", set_sigqueue_max()); return 0; } int set_sigqueue_max(void) { int queue_max; #ifdef SIGQUEUE_MAX queue_max = SIGQUEUE_MAX; #else queue_max = sysconf(_SC_SIGQUEUE_MAX); #endif return queue_max; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Gerçek Zamanlı sinyalleri göndermek için "sigqueue" fonksiyonu çağrılır. Prototipi aşağıdaki gibidir; #include int sigqueue(pid_t pid, int signo, union sigval value); Fonksiyonun birinci parametresi, sinyalin gönderileceği prosesin ID değeridir. İkinci parametre, gönderilecek sinyalin numarasıdır. Bu numara için "SIGRTMIN + n" kalıbını kullanmalıyız. Buradaki "n" ibaresi ise kaç numaralı sinyali gönderdiğimizi belirtmektedir. Üçüncü parametre ise sinyale iliştirilecek ek bilgiyi belirtmektedir. Bu parametre ise aşağıdaki gibi tanımlanmıştır: union sigval { int sival_int; void* sival_ptr; }; "union" olmasından dolayı yapının ilgili elemanlarından sadece bir tanesini kullanabiliriz. Tabii adres bilgisi kullanılacaksa, adres bilgisinin hedef proses nezdinde anlamlı olması gerekmektedir. Fonksiyon başarı durumunda "0", hata durumunda "-1" değerine geri döner. Tabii bu fonksiyonla sinyal gönderebilmek için, "kill" fonksiyonunda olduğu gibi, sinyali gönderen prosesin; -> "Gerçek/Etkin Kullanıcı ID" değerinin sinyali alan prosesin "Gerçek/Saklı Kullanıcı ID" değerine eşit olması -> Uygun önceliğe sahip olması gerekmektedir. Bu fonksiyonla; -> Normal sinyaller gönderebiliriz ancak bu normal sinyaller KUYRUKLANAMAYACAKTIR. -> Gerçek Zamanlı gönderdiğimiz sinyalleri, yukarıda prototipi paylaşılan "signal_handler_func" isimli fonksiyon kullanarak yakalamak zorunda değiliz. Normal sinyalleri yakalamk için kullanılan "handler" fonksiyonları da kullanabiliriz. Fakat sinyale iliştirilen ek bilgiyi ELDE EDEMEYİZ. -> "kill" fonksiyonunda olduğu gibi, "process group" a sinyal gönderebilme yetimiz YOKTUR. Diğer yandan; -> "kill" fonksiyonuyla gönderilen normal sinyali, yukarıdaki 3 parametreli "handler" fonksiyon ile yakalayabiliriz fakat iliştirilen değeri elde edemeyiz. -> "kill" fonksiyonuyla Gerçek Zamanlı sinyal de gönderebiliriz fakat bu işlemin semantiği açıkça belirtilmediğinden, yakalanan sinyaller kuyruklanmayabilir. Ek olarak, "kill" isimli kabuk komutunu "-q" / "--queue" seçeneği ile birlikte kullanırak da Gerçek Zamanlı sinyal gönderebiliriz. Gerçek Zamanlı sinyalleri tam manasıyla alabilmek için artık yukarıda "signal_handler_func" ismiyle paylaşılan fonksiyon prototipini kendi "signal handler" fonksiyonumuzda kullanmalı ve "SA_SIGINFO" bayrağını da "sa_flags" elemanına "bit-wise OR" ile geçmeliyiz. * Örnek 1.0, Gerçek Zamanlı sinyallerin tam manasıyla alınmasına ilişkin bir örnektir. Aşağıdaki iki programı da iki farklı terminal üzerinden çalıştırılması gerekmektedir. // Gerçek Zamanlı sinyalin beklenmesi #include #include #include #include void sig_handler(int, siginfo_t*, void*); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # sig_handler: 100 :> */ struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); printf("waiting for signals...\n"); for(;;) pause(); return 0; } void sig_handler(int, siginfo_t* info, void*) { printf("sig_handler: %d\n", info->si_int); /* "si_int" is Linux specific. */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Gerçek Zamanlı sinyalin gönderilmesi #include #include #include #include void exit_sys(const char* msg); // ./sq // ./sq 45545 10 100 int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } union sigval sv; sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, Aşağıdaki örnekte ise sinyal bekleyen proses 34 numaralı sinyali beklemektedir. Dolayısıyla beklenmeyen bir sinyal gönderilmesi halinde, o sinyalin varsayılan davranışı uygulanacaktır. // Gerçek Zamanlı sinyalin beklenmesi #include #include #include #include void sig_handler(int, siginfo_t*, void*); void exit_sys(const char* msg); // ./sample 34 int main(int argc, char** argv) { /* # OUTPUT # 34 sig_handler: 123 :> */ if (argc != 2) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(atoi(argv[1]), &sa, NULL) == -1) exit_sys("sigaction"); printf("waiting for signals...\n"); for(;;) pause(); return 0; } void sig_handler(int signo, siginfo_t* info, void*) { printf("%d sig_handler: %d\n", signo, info->si_int); /* "si_int" is Linux specific. */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Gerçek Zamanlı sinyalin gönderilmesi #include #include #include #include void exit_sys(const char* msg); // .sq // ./sq 45545 34 123 int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } union sigval sv; sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0, Aşağıdaki örnekte ise gönderilen sinyallerin bir kuyrukta bekletildiği görülmüştür. "FIFO" sırası gözetilmiştir. // Gerçek Zamanlı sinyalin beklenmesi #include #include #include #include void sig_handler(int, siginfo_t*, void*); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # 34 sig_handler 123 34 sig_handler 456 34 sig_handler 789 */ if (argc != 2) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(atoi(argv[1]), &sa, NULL) == -1) exit_sys("sigaction"); sigset_s ss; sigfillset(&ss); if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Sleeps for 10 seconds...\n"); sleep(10); sigfillset(&ss); if (sigprocmask(SIG_UNBLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Waiting for a signal...\n"); for (;;) pause(); return 0; } void sig_handler(int signo, siginfo_t* info, void*) { printf("%d sig_handler: %d\n", signo, info->si_int); /* "si_int" is Linux specific. */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Gerçek Zamanlı sinyalin gönderilmesi #include #include #include #include void exit_sys(const char* msg); // ./sq // ./sq 46398 34 123 // ./sq 46398 34 456 // ./sq 46398 34 789 int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } union sigval sv; sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.1, Aşağıdaki örnekte ise gerçek zamanlı olmayan sinyallerin kuyruklanmadığı görülmüştür. // Gerçek Zamanlı sinyalin beklenmesi #include #include #include #include void sig_handler(int, siginfo_t*, void*); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # 10 sig_handler 12 */ if (argc != 2) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(atoi(argv[1]), &sa, NULL) == -1) exit_sys("sigaction"); sigset_s ss; sigfillset(&ss); sigdelset(&ss, SIGINT); // "Ctrl+C" yaptığımız zaman programı sonlandırabilmek için. if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Sleeps for 10 seconds...\n"); sleep(10); sigfillset(&ss); if (sigprocmask(SIG_UNBLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Waiting for a signal...\n"); for (;;) pause(); return 0; } void sig_handler(int signo, siginfo_t* info, void*) { printf("%d sig_handler: %d\n", signo, info->si_int); /* "si_int" is Linux specific. */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Gerçek Zamanlı sinyalin gönderilmesi #include #include #include #include void exit_sys(const char* msg); // ./sq // ./sq 46398 10 12 // ./sq 46398 10 13 // ./sq 46398 10 14 // ./sq 46398 10 15 // ./sq 46398 10 16 // ./sq 46398 10 17 int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } union sigval sv; sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> Bütün bunların yanı sıra POSIX standartlarında sinyalleri senkron bir biçimde beklemek için "sigwait" ve "sigwaitinfo" isimli iki fonksiyon bulundurulur. Yani sinyalin işlenmesi tamamlanmalı ki esas akışımız yoluna devam edebilsin. >>> "sigwait" & "sigwaitinfo": Fonksiyonların prototipleri aşağıdaki gibidir; #include int sigwait(const sigset_t * set, int * sig); int sigwaitinfo(const sigset_t * set, siginfo_t * info); Fonksiyonların birinci parametresi beklemesini istediğimiz sinyalleri belirtmek için kullanılır. Bu küme normal sinyaller içerebildiği gibi Gerçek Zamanlı sinyalleri de içerebilir. Burada arka planda bir "bit" dizisi bulundurulur ve ilgili sinyallere karşı gelen ilgili indeksteki "bit" değeri "1" yapılır. Böylelikle hangi "bit" değerleri "1" ise onlar beklenir. Beklenen sinyalin gelmesi durumunda da o sinyali ilgili kümeden çıkartır. Tabii bu bekleme ile kastedilen, bu fonksiyonları çağıran "thread" in blokede bekletilmesidir. Hangi sinyalin işlenmesi bittiyse, onun bilgilerini da ikinci parametredeki adrese yazar. Bu iki fonksiyon arasındaki tek fark "sigwait" fonksiyonunun oluşan sinyalin sadece numarasını vermesi, "sigwaitinfo" nun ise daha çok bilgiyi vermesidir. Başarı durumunda fonksiyonlar "0", hata durumunda ise "sigwait" fonksiyonu hata kodunun kendisine dönerken "sigwait" ise "-1" e geri dönüp "errno" değişkenini uygun değere çeker. POSIX standartlarınca sadece "EINVAL" değeri tanımlanmıştır. Diğer yandan fonksiyonun birinci parametresine geçtiğimiz sinyal kümesinde bulunan Gerçek Zamanlı sinyaller kuyruklanabilir olduğundan, kuyruktaki yalnızca ilk sinyalin bilgilerini elde edebiliriz. Velevki Gerçek Zamanlı ve normal sinyallerden birden fazla oluşması durumunda, yani "pending" durumda hem normal hem de Gerçek Zamanlı sinyaller varsa, bunlardan hangisinin bilgilerinin elde edileceği POSIX standartlarınca tanımlanmamıştır, yani "unspecified". Öte yandan fonksiyonun birinci parametresinde belirtilen kümedeki sinyallerin, daha önceden bloke edilmiş olmaları gerekmektedir. POSIX standartlarınca bloke edilmeden iş bu "sigwait" & "sigwaitinfo" çağrılarının "Tanımsız Davranış" oluşturacağı belirtilmiştir. Son olarak bloke edilen sinyaller yine iş bu fonksiyon çağrıları tarafından otomatik bir biçimde "unblock" EDİLMEMEKTEDİR. Dolayısıyla bizler bloke ettiğimiz gibi blokeyi de kaldırmalıyız. * Örnek 1.0, İkinci bir terminal programı üzerinden "SIGUSR1" sinyali "receiver" prosesine gönderilmiştir. // Signal Sender: "14248", "receiver" prosesinin ID'sidir. "kill -USR1 14248" // Sinal Receiver: #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Waiting for signals... Signal-10 occured!.. */ sigset_t ss; sigaddset(&ss, SIGINT); sigaddset(&ss, SIGUSR1); // Block the signals: if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Waiting for signals...\n"); int result, signal_result; if ((result = sigwait(&ss, &signal_result)) != 0) { fprintf(stderr, "sigwait: %s\n", strerror(result)); exit(EXIT_FAILURE); } /* Start: Sinyalin İşlendiği Kısım */ printf("Signal-%d occured!..\n", signal_result); /* End: Sinyalin İşlendiği Kısım */ // Unblock the signals: if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, Kendi terminali üzerinden "SIGINT" sinyali, "Ctrl+C" yapılarak, "receiver" prosesine gönderilmiştir. // Sinal Receiver: #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Waiting for signals... Signal-2 occured!.. */ sigset_t ss; sigaddset(&ss, SIGINT); sigaddset(&ss, SIGUSR1); // Block the signals: if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Waiting for signals...\n"); int result, signal_result; if ((result = sigwait(&ss, &signal_result)) != 0) { fprintf(stderr, "sigwait: %s\n", strerror(result)); exit(EXIT_FAILURE); } /* Start: Sinyalin İşlendiği Kısım */ printf("Signal-%d occured!..\n", signal_result); /* End: Sinyalin İşlendiği Kısım */ // Unblock the signals: if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Görüldüğü gibi bizler sinyalleri bloke ettikten sonra, "sigwait" & "sigwaitinfo" çağrıları ile aslında o sinyalleri de işlemiş olduk. Beklediğimiz sinyaller için bir "handler" fonksiyon kullanmadık da, ana akışı kullandık. Böylelikle senkron bir biçimde yapmış olduk. Daha önceki örneklerde yapılış biçimleri asenkron biçimdeydi. > Hatırlatıcı Notlar: >> "select" fonksiyonu "BSD" sistemleri için tasarlanmışken, "poll" fonksiyonu "AT&T" sistemleri için tasarlanmış fakat daha sonra ikisi de POSIX çatısı altına alınmıştır.