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)

results matching ""

    No results matching ""