Event Loop İçinde Kullanım
Sinyaller üzerinden çalışan asenkron callback fonksiyonları yönetmek bazen güçleşebilir. Özellikle uygulama kodu büyüdükçe, sinyal callback fonksiyonu içerisinde fazla işlem yapmak da riskli olduğu için çok fazla global değişken kullanımıyla karşı karşıya kalabiliriz. Ayrıca kullandığımız timer sayısı arttıkça (yüzlerce olduğunu düşünün) asıl uygulama kodunun normal akışının sürekli kesintiye uğramasının da getireceği kayıpları dikkate almalıyız.
Linux çekirdeğinin 2.6.25 versiyonuyla birlikte timer bildirimleri için sinyal ve thread kullanımına ek olarak, dosya betimleyicisi (file descriptor API) üzerinden kullanım desteği de eklenmiştir.
Bu model, her bir timer için ayrı bir dosya betimleyici (file descriptor) olması esasına dayanır. Elimizde callback fonksiyonları yerine dosya betimleyicileri olduğunda, select()
, poll()
, epoll()
gibi event-loop mekanizmaları ile dosya betimleyicinin hazır olup olmadığını test edebilir, okumaya hazır durumda ise (timer zamanı gelmiş demektir) ilgili fonksiyonu çalıştırabiliriz.
Bunun en büyük avantajı, sadece timer için olanları değil, uygulamamızın kullandığı diğer tüm dosya betimleyicilerini aynı event-loop içerisinde dinlemenin (soketler, seri port, inotify ile izlediklerimiz vb.) asenkron çalışmanın getirdiği ek yükleri ortadan kaldırması ve işlemlerin kontrollü biçimde serileştirilerek yapılmasının sağlanmasıdır.
Timer kullanımının bu yeni hali henüz POSIX tarafından standardize edilmemiş olduğundan, platformlar arası taşınabilir kod yazmak isteyenlerin bu durumda dikkat etmesi gereklidir.
Oluşturma: timerfd_create
İlk fonksiyonumuz timer oluşturup dosya betimleyici dönen timerfd_create()
olup prototipi aşağıdaki gibidir:
#include <sys/timerfd.h>
int timerfd_create(int clockid , int flags)
Görüldüğü gibi timer_create()
fonksiyonuna nazaran kullanımı daha basittir zira sinyal üzerinden geri bildirim nedeniyle yapılması gereken ek ayarlamalara ihtiyaç kalmamıştır.
Clock tipi olarak önceki yöntemlere benzer şekilde CLOCK_REALTIME ve CLOCK_MONOTONIC kullanılabilir.
Flags parametresi 0 olarak geçilebileceği gibi aşağıdaki değerler de kullanılabilir:
Flag | Açıklama |
---|---|
TFD_NONBLOCK | Açılacak dosya betimleyici üzerinde O_NONBLOCK bayrağını aktifleştir, non-blocking modda çalışmaya izin ver |
TFD_CLOEXEC | Açılacak dosya betimleyici üzerinde FD_CLOEXEC (close-on-exec) bayrağını aktifleştir. Multi-threaded uygulamalarda exec() fonksiyon ailesinden bir çağrı yapıldığını otomatik ve atomik biçimde dosyayı kapatır |
Başarılı bir çağrı sonrasında edindiğimiz dosya betimleyicinin geleneksel dosya betimleyicilerden farkı yoktur. Dolayısıyla timer ile işimiz bittiğinde yok etmek için özel ek bir fonksiyon bulunmaz. Elimizdeki dosya betimleyici üzerinden dosyaları kapatmakta kullandığımız close()
fonksiyonunu çağırmak yeterli olacaktır.
Ayarlama: timerfd_settime
Elimizde bir timerfd_create
sonrası elde edilmiş dosya betimleyici var ise, zamanlayıcı kurmak veya kalan zamanı öğrenmek için prototipleri aşağıda verilen timerfd_settime
ve timerfd_gettime
fonksiyonları kullanılır.
#include <sys/timerfd.h>
int timerfd_settime(int fd, int flags,
const struct itimerspec * new_value, struct itimerspec * old_value)
int timerfd_gettime(int fd, struct itimerspec * curr_value);
FD_CLOEXEC Kullanımının Önemi
Uygulama kodunuz içerisinde fork()
ile yeni bir process başlatıp ardından exec()
ile yeni kodu yükleyip çalıştırdığınızı düşünelim. Linux process modelinde fork edilerek oluşturulan yeni çocuk process, kendisinin parent process'inden açık dosya betimleyicilerin de bir kopyasını almış olur.
Bu sebeple parent process içerisinde timerfd_create
ile açılmış olan dosya betimleyicileri var ise, exec
edilen yeni process içerisinde de zamanlayıcıda ayarlı süre dolduğunda bildirim mekanizmaları devreye girecektir.
Bu istenmeyen etkiden kurtulmanın yolu, yeni çalıştırılacak process'in öncelikle bu şekilde açık dosya betimleyicilerini kapatmasıdır. Ancak bu model, multi-threaded uygulamalarda çeşitli yarış durumu (race condition) problemlerine yol açabilmektedir.
Kesin çözümün sağlanması için, fork
+ exec
modelinde FD_CLOEXEC
bayrağını aktifleştirmek gereklidir. Bu bayrak ile açılmış olan dosyalar, ayrı bir process oluştuğunda kopyalanmak yerine yarış durumu problemi üretmeyecek şekilde otomatik ve atomik biçimde kapatılırlar.
Dosya Betimleyicinin Okunması
Oluşturulan ve zamanlayıcısı ayarlanan dosya betimleyiciler, süre dolumu gerçekleştiğinde okunabilir duruma geçerler (read-notification).
Bir event-loop kullanıyorsanız bir çok dosya betimleyicinin bu durumunu aynı anda kontrol edebilir ve hızlı haberdar olabilirsiniz.
Timer'ın ilgili olduğu dosya betimleyiciden okunabilir olduğu bilgisi geldiğinde, 8 byte uzunluğunda bir okuma yapılması gereklidir.
Okuma işleminin sonucunda unsigned int64
tipinde bir veri gelecektir. Bu noktada okuma işlemi gerçekleştirilmez ise, event-loop kontrol fonksiyonuna dönüldüğünde uygulama anında tekrar uyandırılacak ve sonsuz döngüye girilecektir.
Örnek Uygulama
Önceki bölümde POSIX timer API ile gerçekleştirmiş olduğumuz uygulamayı bu defa event-loop ve timerfd
kullanarak yapalım. Ek olarak 5 farklı timer kullanacağız. Herhangi bir timer bildirimi geldiğinde, dosya betimleyicisi üzerinden ilgili veri yapısına hızlıca ulaşabilmek için basit bir hash-table implementasyonu olan uthash da kullanacağız.
uthash kaynak kodu ve dokümantasyonu için: https://github.com/troydhanson/uthash
Aşağıdaki örnek kodu timerfd.c
adıyla kaydedip derleyiniz.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
#include <sys/timerfd.h>
#include <inttypes.h>
#include <errno.h>
#include <string.h>
#include "../common/debug.h"
#include "../common/uthash.h"
struct person {
int timerfd;
int no;
char name[32];
UT_hash_handle hh;
};
int main ()
{
unsigned int remaining = 3;
struct person *p;
struct person *p_tmp;
struct itimerspec new_value;
fd_set readfs;
fd_set masterfs;
int maxfd = 0;
int ret;
int i;
struct person *persons = NULL;
FD_ZERO(&masterfs);
new_value.it_value.tv_sec = 0;
new_value.it_value.tv_nsec = 500 * 1000 * 1000;
new_value.it_interval.tv_sec = 0;
new_value.it_interval.tv_nsec = 900 * 1000 * 1000;
for (i = 0; i < 5; i++) {
p = calloc(1, sizeof(struct person));
p->no = i;
snprintf(p->name, sizeof(p->name), "Name : %d", i);
p->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
if (timerfd_settime(p->timerfd, 0, &new_value, NULL) < 0) {
errorf("timerfd_settime error: %s", strerror(errno));
continue;
}
HASH_ADD_INT(persons, timerfd, p);
FD_SET(p->timerfd, &masterfs);
}
for (i = 0; i < 1024; i++) if (FD_ISSET(i, &masterfs)) maxfd = i;
while (remaining > 0) {
struct timeval tout = { 1, 0};
select_again:
readfs = masterfs;
ret = select(maxfd + 1, &readfs, NULL, NULL, &tout);
if (ret < 0) {
if (errno == EINTR) {
debugf("select interrupted by signal");
goto select_again;
} else {
errorf("select error: %s", strerror(errno));
}
} else if (ret > 0) {
for (i = 0; i <= maxfd && ret; i++) {
if (FD_ISSET(i, &readfs)) {
uint64_t howmany;
read(i, &howmany, sizeof(uint64_t));
HASH_FIND_INT(persons, &i, p);
if (p == NULL) {
errorf("this shouldn't happen")
} else {
debugf("read fd: %d, val: %" PRIu64 ", name: %s", i, howmany, p->name);
}
ret--;
}
}
goto select_again;
} else {
// select timeout
remaining--; // decrement 1 seconds
}
}
HASH_ITER(hh, persons, p, p_tmp) {
HASH_DEL(persons, p);
free(p);
}
return 0;
}
Örnek uygulamamızda event-loop için select()
yapısını kullandık. Eğer dinleyeceğimiz dosya betimleyici sayısı daha yüksek olsaydı poll()
veya epoll()
kullanmamız gerekecekti. Alıştırma yapmak için buradaki örneği epoll()
ile çalışan bir event-loop'a dönüştürmeyi deneyebilirsiniz.
Önceki timer bölümlerinde sinyallerle bildirilen timer bildirimleri uygulamamızı asenkron kesintilere uğratıyordu. Aynı durumun bu örneğimiz için geçerli olup olmadığını, modelin avantajlı ve varsa dezavantajlı olduğu senaryoların neler olduğunu düşünmeye çalışınız.
Uygulamamız çalıştığında beklendiği gibi bir çıktı üretmektedir:
$ gcc -o timerfd timerfd.c -lrt $ ./timerfd debug: read fd: 3, val: 1, name: Name : 0 (main timerfd.c:80) debug: read fd: 4, val: 1, name: Name : 1 (main timerfd.c:80) debug: read fd: 5, val: 1, name: Name : 2 (main timerfd.c:80) debug: read fd: 6, val: 1, name: Name : 3 (main timerfd.c:80) debug: read fd: 7, val: 1, name: Name : 4 (main timerfd.c:80) debug: read fd: 3, val: 1, name: Name : 0 (main timerfd.c:80) debug: read fd: 4, val: 1, name: Name : 1 (main timerfd.c:80) debug: read fd: 5, val: 1, name: Name : 2 (main timerfd.c:80) debug: read fd: 6, val: 1, name: Name : 3 (main timerfd.c:80) debug: read fd: 7, val: 1, name: Name : 4 (main timerfd.c:80) debug: read fd: 3, val: 1, name: Name : 0 (main timerfd.c:80) debug: read fd: 4, val: 1, name: Name : 1 (main timerfd.c:80) debug: read fd: 5, val: 1, name: Name : 2 (main timerfd.c:80) debug: read fd: 6, val: 1, name: Name : 3 (main timerfd.c:80) debug: read fd: 7, val: 1, name: Name : 4 (main timerfd.c:80)