select() -- Eşzamanlı G/Ç Çoğullama
Önceki İleri Teknikler Sonraki
select() -- Eşzamanlı G/Ç Çoğullama
Biraz garip olmakla birlikte bu işlevin epey faydalı olduğu söylenebilir. Şu durumu ele alalım: siz bir sunucu programsınız ve bir yandan gelen bağlantıları dinlerken öte yandan da açık olan bağlantılardan akmakta olan verileri okumak istiyorsunuz.
Sorun değil, diyorsunuz, bir accept() ve birkaç tane de recv() işimizi görür. Ağır ol dostum! Ya çağırdığın accept() işlevi bloklayan durumda ise? O zaman aynı anda recv() ile nasıl oluyor da veri okumayı düşünebiliyorsun? "O halde bloklamayan soketleri kullanırım!" Hiç yakıştıramadım senin gibi bir programcıya! İşlemci zamanını deliler gibi harcamak mı istiyorsun? E peki nasıl yapacağız öyleyse?
select() aynı anda birden fazla soketi gözetleme imkânı sunar. Bununla kalmaz aynı zamanda hangi soketin okumak için hazır olduğunu, hangisine yazabileceğiniz, hangisinde istisnai durumlar oluştuğunu da söyler.
Laf kalabalığını kesip hemen işleve geçiyorum, select():
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int numfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout); 
Bu işlev dosya tanımlayıcı kümelerini gözlemler. Özel olarak ilgilendikleri ise readfds, writefds ve exceptfds'dir. Mesela standart girdiden ve sockfd gibi bir soket tanımlayıcıdan veri okuyup okuyamayacağınızı merak ediyorsanız tek yapmanız gereken 0 ve sockfd'yi readfds kümesine eklemek. numfds parametresi azami dosya tanımlayıcı artı bir olarak ayarlanmalıdır. Bu örnekte sockfd+1 olmalıdır. Çünkü açıktır ki yukarıdaki değer standart girdiden (0) daha büyük olacaktır.
select() çalıştırıldıktan sonra readfds seçmiş olduğunuz dosya tanımlayıcılardan hangisinin okumak için hazır olduğunu yansıtacak şekilde güncellenir. Bunları aşağıdaki FD_ISSET() makrosu ile test edebilirsiniz.
Daha fazla ilerlemeden bu kümeler ile nasıl başa çıkacağınızı anlatayım. Her küme fd_set türündedir. Bu tür üzerinde aşağıdaki makroları kullanabilirsiniz:
  • FD_ZERO(fd_set *set) -- dosya tanımlayıcı kümesini temizler.
  • FD_SET(int fd, fd_set *set) -- kümeye fd'yi ekler.
  • FD_CLR(int fd, fd_set *set) -- fd'yi kümeden çıkarır.
  • FD_ISSET(int fd, fd_set *set) -- fd'ni küme içinde olup olmadığına bakar.
Son olarak, nedir bu struct timeval? Bazen birilerinin size veri göndermesini sonsuza dek beklemek istemezsiniz. Belki de her 96 saniyede bir ekrana "Hala çalışıyor..." mesajı basmak istersiniz (program o esnada bir şey yapmıyor olsa bile). Bu zaman yapısı sizin bir sonlandırma süresi ("timeout period") belirlemenizi sağlar. Eğer bu süre geçildi ise ve select() hala hazır bir dosya tanımlayıcı bulamadı ise o zaman işlev (select) döner ve böylece de siz de işinize devam edebilirsiniz.
struct timeval yapısının elemanları aşağıdaki gibidir:
struct timeval {
    int tv_sec;     // seconds
    int tv_usec;    // microseconds
}; 
Tek yapmanız gereken tv_sec'i kaç saniye bekleneceğine ayarlamak ve tv_usec'i de kaç mikrosaniye bekleneceğine ayarlamak. Evet, doğru okudunuz, mikrosaniye, milisaniye değil. Bir milisaniye içinde 1,000 mikrosaniye vardır ve bir saniye içinde de 1,000 milisaniye vardır. Yani bir saniye içinde 1,000,000 mikrosaniye vardır. Tamam da neden "usec"? Buradaki "u" harfinin Yunan alfabesindeki Mü harfine benzediği düşünülmüştür ve bu yüzden "mikro"yu temsilen kullanılmıştır. Bunlara ek olarak işlev döndüğünde timeout ne kadar zaman kaldığını gösterecek şekilde güncellenmiş olabilir. Bu hangi tür UNIX kullandığınıza bağlıdır.
Vay canına! Mikrosaniye hassasiyetinde bir zamanlayıcımız var! Gene de siz buna çok güvenmeyin. Standart Unix zamandilimi yaklaşık 100 milisaniyedir, yani struct timeval yapısını nasıl ayarlarsanız ayarlayın en az bu kadar beklemek durumunda kalabilirsiniz.
Meraklısına not: Eğer struct timeval yapısındaki değişkeninizin alanlarınız 0 yaparsanız, select() işlevi anında sonlanacak ve böyle kümenizin içindeki tüm dosya tanımlayıcıları taramış olacaktır. Eğer timeout parametresini NULL yaparsanız bu sefer de işlev asla zaman aşımına uğramayacak ve ilk dosya tanımlayıcı hazır olana dek bekleyecektir. Son olarak: Eğer belli bir küme için beklemek sorun teşkil etmiyorsa o zaman select() işlevini çağırırken onu NULL olarak ayarlayabilirsiniz.
Aşağıdaki kod parçası standart girdiden bir veri gelmesi için 2.5 saniye bekler:
/*
** select.c -- bir select() örneği
*/

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define STDIN 0  // standart girdi için dosya tanımlayıcı

int main(void)
{
    struct timeval tv;
    fd_set readfds;

    tv.tv_sec = 2;
    tv.tv_usec = 500000;

    FD_ZERO(&readfds);
    FD_SET(STDIN, &readfds);

    // writefds ve exceptfds ile ilgilenmiyoruz:
    select(STDIN+1, &readfds, NULL, NULL, &tv);

    if (FD_ISSET(STDIN, &readfds))
        printf("Bir tusa basildi!\n");
    else
        printf("Zaman doldu.\n");

    return 0;
} 
Üzerinde çalıştığınız uçbirim türüne bağlı olarak bir tuşa bastıktan sonra bunun algılanması için ENTER tuşuna basmanız gerekebilir. Aksi takdirde "Zaman doldu." mesajı alırsınız.
Şimdi bazılarınız bir veripaketi soketi üzerinden veri beklemek için bu yöntemin mükemmel bir yöntem olduğunu düşünebilir -- evet haklısınız: bu yötem gerçekten de mükemmel olabilir . Ancak bazı UNIX türevleri select'i bu şekilde kullanabilirken bazıları kullanamaz. Bu yüzden de pratik olarak kullanmaya başlamadan önce sisteminizde bu konu ile ilgili man sayfalarını okuyun.
Bazı UNIX türevleri de struct timeval yapısındaki değişkeninizi, zamanaşımına ne kadar süre kaldığını gösterecek şekilde güncelleyebilir. Bazıları ise bunu yapmaz. Eğer taşınabilir programlar yazmak istiyorsanız buna güvenmeyin. (gettimeofday() işlevinden faydalanın. Saçma geliyor değil mi evet ama böyle ben ne yapabilirim.)
Peki ya okuma kümesindeki soketlerden biri bağlantıyı kesmiş ise? Bu durumda select() bu soket için "okumaya hazır" mesajı verir ve siz recv() ile okumaya kalktığınızda da recv() size 0 değerini döndürür. Böylece istemcinin bağlantıyı kesmiş olduğunu anlarsınız.
Meraklısına select() ile ilgili bir bilgi daha: eğer listen() ile dinlemekte olan bir soketiniz varsa bu soketin dosya tanımlayıcısını readfds kümesine yerleştirerek yeni bir bağlantı olup olmadığını öğrenebilirsiniz.
İşte dostlarım, süper güçlü select() işlevinin özeti bu kadar.
Ancak gelen yoğun istek üzerine yukarıdaki bilgileri pratiğe dökeceğimiz bir örnek sizi bekliyor.
Bu program basit bir çok kullanıcılı "chat" sunucu olarak davranır. Derledikten sonra bunu bir pencerede çalıştırın ve ardından telnet ile programa bağlanın ("telnet  hostname  9034"). Farklı farklı pencerelerden programa aynı anda birden fazla sayıda bağlantı açabilirsiniz. Bir kere bağlanıp da bulunduğunuz telnet ortamından bir şeyler yazıp yolladığınızda, mesajınız diğer bağlı telnet pencerelerinde de görünmeli.
/*
** selectserver.c -- keyifli bir cok kullanicili chat sunucu
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 9034   // dinledigimiz port

int main(void)
{
    fd_set master;    // na dosya tanimlayici listesi
    fd_set read_fds;  // select() icin gecici dosya tanimlayici listesi
    struct sockaddr_in myaddr;     // sunucu adresi
    struct sockaddr_in remoteaddr; // istemci adresi
    int fdmax;        // azami dosya tanimlayici numarası
    int listener;     // dinlenen soket tanımlayıcı
    int newfd;        // yeni accept()lenecek soket tanımlayıcı
    char buf[256];    // istemci verisi için tampon
    int nbytes;
    int yes=1;        // setsockopt() SO_REUSEADDR için, aşağıda
    int addrlen;
    int i, j;

    FD_ZERO(&master);    // ana listeyi ve gecici listeyi temizle
    FD_ZERO(&read_fds);

    // dinleyiciyi yarat
    if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // "adres zaten kullanımda" mesajından kurtul
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes,
                                                        sizeof(int)) == -1) {
        perror("setsockopt");
        exit(1);
    }

    // bind
    myaddr.sin_family = AF_INET;
    myaddr.sin_addr.s_addr = INADDR_ANY;
    myaddr.sin_port = htons(PORT);
    memset(&(myaddr.sin_zero), '\0', 8);
    if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1) {
        perror("bind");
        exit(1);
    }

    // listen
    if (listen(listener, 10) == -1) {
        perror("listen");
        exit(1);
    }

    // dinleyici soketi ana listeye ekle
    FD_SET(listener, &master);

    // en büyük dosya tanimlayicisi hatirla
    fdmax = listener; // so far, it's this one

    // ana döngü
    for(;;) {
        read_fds = master; // copy it
        if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
            perror("select");
            exit(1);
        }

        // mevcut baglantilari tarayip okumaya hazir olanlari tespit et
        for(i = 0; i <= fdmax; i++) {
            if (FD_ISSET(i, &read_fds)) { // bir tane yakaladik!!
                if (i == listener) {
                    // handle new connections
                    addrlen = sizeof(remoteaddr);
                    if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr,
                                                             &addrlen)) == -1) {
                        perror("accept");
                    } else {
                        FD_SET(newfd, &master); // ana listeye ekle
                        if (newfd > fdmax) {    // azami miktarı güncelle
                            fdmax = newfd;
                        }
                        printf("selectserver: new connection from %s on "
                            "socket %d\n", inet_ntoa(remoteaddr.sin_addr), newfd);
                    }
                } else {
                    // istemciden gelen veri icin gerekeni yap
                    if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) {
                        // bir hata var ya da istemci baglantiyi kesti
                        if (nbytes == 0) {
                            // baglanti kesilmis
                            printf("selectserver: socket %d hung up\n", i);
                        } else {
                            perror("recv");
                        }
                        close(i); // bye!
                        FD_CLR(i, &master); // ana listeden cikar
                    } else {
                        // istemciden bir miktar veri geldi
                        for(j = 0; j <= fdmax; j++) {
                            // gelen veriyi herkese yolla!
                            if (FD_ISSET(j, &master)) {
                                // dinleyici ve kendimiz haric
                                if (j != listener && j != i) {
                                    if (send(j, buf, nbytes, 0) == -1) {
                                        perror("send");
                                    }
                                }
                            }
                        }
                    }
                } // O KADAR CIRKIN ki!
            }
        }
    }

    return 0;
} 
Lütfen dikkat edin: Yukarıdaki kod içinde iki dosya tanımlayıcı listem var: master ve read_fds. Birincisi yani, master o esnada bağlı olan tüm soket tanımlayıcılarını tutuyor ve buna ek olarak bir de dinleme görevini üstlenmiş olan soketi de bünyesinde barındırıyor.
master diye bir liste oluşturmamın sebebi select() işlevinin sizin ona verdiğiniz soket listesini değiştirmesi ve hangilerinin okunmaya hazır olduğunu gösterecek şekilde güncellemesi. select() çağrıları arasında bağlantı kümesinin bozulmamış bir kopyasına ihtiyacım olduğu için böyle bir şey yaptım. Yani son anda master kümesini read_fds kümesine kopyalıyor ve select() işlevini çağırıyorum.
İyi de bu aynı zamanda her yeni bağlantı talebinde bu bağlantıyı master listesine eklemem gerektiği anlamına gelmiyor mu? Tabii ki! Ve her bağlantı kesilmesinde de kesilen bağlantı ile ilgili tanımlayıcıyı da master listesinden çıkarmam gerekmiyor mu? Elbette!.
Farkında iseniz listener soketinin ne zaman hazır olduğunu kontrol ediyorum. Hazır olduğunda bu demek oluyor ki yeni bir bağlantı talebi var ve ben de bunu görünce bağlantıyı accept() ile kabul ediyor ve master listesine ekliyorum. Benzer şekilde bir istemci bağlantısı hazır olduğunda ve recv() işlevi 0 döndürdüğünde anlıyorum ki istemci bağlantıyı kapatmış ve bu yüzden onu master listesinden çıkarmalıyım.
Ancak eğer istemci recv() işlevi ile sıfırdan farklı bir değer döndürdüğünde de biliyorum ki okunmuş olan bir veri yığını var. Bu veriyi alıyorum ve sonra da master listesi üzerindeki elemanlar üzerinden tek tek dolaşıp almış olduğum veriyi diğer istemcilere yolluyorum.
Ve dostlarım işte size süper güçlü select() işlevinin o kadar da basit olmayan açıklaması.
Önceki Üst Ana Başlık Sonraki
Bloklama Başlangıç Sorunlu send() Durumları
Bir Linux Kitaplığı Sayfası