Geçen yıllar süresince internet dünyasında popülasyonun artmasıyla birlikte akıllara takılan soruların sayısı da arttı. Bunlardan en can alıcı olanı ise “bu kadar kullanıcıya daha az maliyetli yapılarla nasıl hizmet verilecek?” sorusu idi. Mali konulara takılmadan düşündüğümüzde bile bu kadar sunucuyu nereye koyacağımızı bilemiyorduk. Örneğin standart programlama mantığında birbirinin mantıksal kopyaları olan birden fazla değişkeni denetlemek için bir loop kurulur ve içerisinde her seferinde koşul sağlanmış mı diye denetlenmesi gerekir. Bu da fazlasıyla işlem yükünü beraber getirmekte. Kabaca örnek vemek gerekirse; bir FTP sunucuyla hizmet verdiğinizi düşünün. Protokol içerisindeki gelen giden paketleri bir şekilde yorumlamanız gerekir. bu iş için socket açılır. 1 kullanıcı bağlandı, bağlantı socketi denetlenmeye başlandı, iş dosya almaya geldi ve socket üzerinden veri akışı sağlandı. Buraya kadar herşey normal. peki ya 1 değil 10,000 kullanıcı dosya transferi gerçekleştirmek isteseydi ne yapacaktık? Sonuç düz mantıkla yine meşhur loop mantığına gidiyor. Bir loop açılıyor ve işlemcinin sınırları zorlanarak açılan soketler denetlenmeye çalışılıyor. Diyelim ki sorunumuzu bu şekilde aştığımızı düşündük ama madalyonun diğer tarafında sunucunun asıl yapması gereken işler var. Diske erişim, yetki denetimi, kota denetimi, dosya yazma/okuma. Bukadar işi birarada yapabilmek gerekliydi ve yapıldı. ama oldukça maaliyetli ve pratik olmayan çözümlerle yapıldı. Sunucuyu ve işlemci gücünü boşa harcadık. Peki ya işletim sistemi bize yardımcı olamaz mıydı? evet olmalıydı ve oldu. Hadi biraz daha derinlere inelim.Nonblocking modda tüm network handle’larındaki bekleyen veriyi öğrenmek için select() veya poll() kullanabilirsiniz. Çok kullanılan favori bir yöntemdir. Kernel programınıza hazırdaki bağlantıyı bildirir ve işinizi hem hızlı halletmenizi sağlar hem de işlemciye daha az efor sarfettirir. level-triggered bilgisayar donanım dizaynı alanında edge-triggered mantığına göre karşıt bir mantıktır. Bu terim Jonathon Lemon tarafından BSDCON 2000 konferansında duyurulmuştu.Edge-triggered poll mantığı Linux 2.6 kerneli ile değiştirilip epoll mantığına dönüldü. benzer bir şekilde FreeBSD’de de kqueue kullanılmaya başlandı. Birden fazla kez aynı dataya erişilmek istendiğinde ise minimum fiziksel hafıza kullanarak yapılabilmesi için zero-copy ortaya çıkarıldı. IO-Lite ile daha önceden kullanılan verileri user level’da ayırıp hazır tutar ve bir sonraki kullanımda hızlı bir şekilde erişimini sağlar. fakat henüz geliştirilmeye mahkum. Birçok benchmark testinde epoll() kullanan uygulamaların poll() ve select() kullanan uygulamalara göre kontrol edebildiği dosya descriptor sayısının açık ara daha fazla olduğunu ve harcadığı zamanın da daha kısa olduğunu ortaya koymaktadır.

Yukarıdaki grafiği mikrosaniye bazında incelediğimizde epoll ve kqueue ile poll ve select arasındaki farkı açıkca görebiliyoruz. Kullanılan dosya descriptor artışında select ve poll aşırı yavaşlamaya başlıyor ve düzenli olarak bu yavaşlama artıyor. Epoll ve kqueue kullanımında ise harcanan süre kullanılan dosya descriptor artarken sabitleniyor. Poll ve select 25,000 dosya descriptor denetiminde her dosya descriptor için 10000 mikrosaniyenin üstünde zaman harcarken, poll ve kqueue 100 mikrosaniyenin altında seyrediyor. her dosya descriptordaki veri alışverişine bu süre eklendiğine göre, hatırı sayılır miktarda bir hız farkı olduğu ortadadır.Epoll kullanarak basit bir single-threaded ve non-blocking server uygulaması yapalım ve birden çok I/O denetleyelim:#include #include #include #include #include #include #include #include #include #include #include static void SystemFatal(const char* message);static bool ReadFromFile(int fd);static const unsigned short kPort = 8080;int main(int argc, char* argv[]) {int server = socket(AF_INET, SOCK_STREAM, 0);if (server == -1) {SystemFatal(“socket”);}
SO_REUSEADDR ile kullanılmayan portları daha sonra kullanabilmeyi garanti edelim.int arg = 1;if (setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &arg,sizeof(arg)) == -1) {SystemFatal(“setsockopt”);}
non-blocking ayarlamalarımızı yapıyoruz. if (fcntl(server, F_SETFL, O_NONBLOCK | fcntl(server, F_GETFL, 0)) == -1) {SystemFatal(“fcntl”);}
Server’ı seçtiğimiz porta bind ediyoruz. struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_addr.s_addr = htonl(INADDR_ANY);addr.sin_port = htons(kPort);if (bind(server, (struct sockaddr*) &addr, sizeof(addr)) == -1) {SystemFatal(“bind”);}
Bağlantıları dinlemeye başlıyoruz if (listen(server, SOMAXCONN) == -1) {SystemFatal(“listen”);}
file descriptor üzerinde epoll yaratalım. const int max_events = 16;int epoll_fd = epoll_create(max_events);if (epoll_fd == -1) {SystemFatal(“epoll_create”);}
file destriptorları epoll event loop içine dahil ediyoruz. struct epoll_event event;event.events = EPOLLIN | EPOLLERR | EPOLLHUP | EPOLLET;event.data.fd = server;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server, &event) == -1) {SystemFatal(“epoll_ctl”);}
Yarattığımız epoll event loop’u çalıştırıyoruz. while (true) {struct epoll_event events[max_events];int num_fds = epoll_wait(epoll_fd, events, max_events, -1);for (int i = 0; i < num_fds; i++) {
Durum 1: Hata denetimi if (events[i].events & (EPOLLHUP | EPOLLERR)) {fputs(“epoll: EPOLLERR”, stderr);close(events[i].data.fd);continue;}assert(events[i].events & EPOLLIN);
Durum 2: Sunucu yeni bir bağlantı verirse kabul ediyoruz. if (events[i].data.fd == server) {struct sockaddr remote_addr;socklen_t addr_size = sizeof(remote_addr);int connection = accept(server, &remote_addr, &addr_size);if (connection == -1) {if (errno != EAGAIN && errno != EWOULDBLOCK) {perror(“accept”);}continue;}
Bağlantıyı non-blocking olarak ayarlıyoruz.if (fcntl(connection, F_SETFL, O_NONBLOCK |cntl(connection, F_GETFL, 0)) == -1) {SystemFatal(“fcntl”);}
Bağlantıyı daha önce yarattığımız epoll loop’a ekliyoruz. event.data.fd = connection;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, connection,&event) == -1) {SystemFatal(“epoll_ctl”);}continue;}
Durum 3: Herhangi bir connectiondan bilgi akışı varsa ve dosya descriptor kapanırsa yarattığımız epolldan otomatikman kalkıyor. if (!ReadFromFile(events[i].data.fd)) {close(events[i].data.fd);}}}close(server);}
Verilen dosya descriptor’dan tekrar block edilmeden önce veya data akışı bitene kadar dataları çekmeye başlıyoruz. (EAGAIN)Eğer bu işlem sırasında beklenmeyen bir durum oluşursa hata verip işlemi sonlandırıyoruz.static bool ReadFromFile(int fd) {while (true) {char buffer[1024];int bytes = read(fd, buffer, sizeof(buffer));if (bytes == -1) {if (errno == EAGAIN) {return true;} else {perror(“read”);return false;}}if (bytes == 0) {return false;}
Çektiğimiz datayı stdout kullanarak ekrana basıyoruz. if (write(1, buffer, bytes) == -1) {SystemFatal(“write”);}}}
Oluşan hataları hata numaralarına göre ekrana basıyoruz.static void SystemFatal(const char* message) {perror(message);abort();}