epoll Mekanizması ile Yüksek Başarımlı Olay Döngüsü (Event Loop) Mimarisi

İnternet dünyasında binlerce eşzamanlı istemciye tek bir sunucu üzerinden yanıt verme ihtiyacı doğduğunda (tarihte C10K Problemi olarak bilinir), her bağlantıya bir thread açmak ya da eski select() / poll() mekanizmalarını kullanmak yetersiz kaldı. select her kontrolde $O(N)$ karmaşıklıkla tüm dosya tanımlayıcılarını tek tek taramak zorunda kalırken, Linux çekirdeği 2.5.44 sürümünde devrimsel bir çözüm getirdi: epoll.

Bu rehberde; epoll mekanizmasının dahili çalışma mantığını, Seviye Tetiklemeli (Level-Triggered) ile Kenar Tetiklemeli (Edge-Triggered) modlar arasındaki farkı ve C++ ile tek bir thread üzerinde binlerce soketi yöneten temiz bir Olay Döngüsü (Event Loop) tasarımını ele alıyoruz.


1. Neden select ve poll Değil de epoll?

Özellikselect()poll()epoll
Zaman Karmaşıklığı$O(N)$ (Her seferinde tüm liste taranır)$O(N)$ (Tüm liste taranır)$O(1)$ (Yalnızca hazır olaylar döner)
Maksimum Soket SınırıGenellikle 1024 (FD_SETSIZE)Sistem dosya limitiSistem dosya limiti (Milyonlarca)
Kullanıcı/Çekirdek Bellek KopyasıHer çağrıda tüm dizi çekirdeğe kopyalanırHer çağrıda dizi çekirdeğe kopyalanırKayıt bir kez yapılır (Kırmızı-Siyah Ağaç)
Olay BildirimiHangi soketin hazır olduğu bilinmezHangi soketin hazır olduğu bilinmezDoğrudan hazır olanların listesi döner

epoll, çekirdek içerisinde soketleri bir Kırmızı-Siyah Ağaç (Red-Black Tree) üzerinde saklar. Bir sokete veri geldiğinde ağ kartı kesmesi (NIC interrupt) o soketi doğrudan bir Hazır Listesi’ne (Ready List) ekler. Bu sayede epoll_wait çağrıldığında sistemde 100.000 soket olsa bile sadece o an veri gönderen 3 soket anında döner.


2. epoll Sistem Çağrıları

sys/epoll.h kütüphanesi üç temel fonksiyon üzerine kuruludur:

  1. epoll_create1(int flags): Çekirdekte yeni bir epoll nesnesi oluşturur ve bir dosya tanımlayıcısı döner.
  2. *epollctl(int epfd, int op, int fd, struct epollevent event):** İzlenecek soketi ekler (EPOLLCTLADD), günceller (EPOLLCTLMOD) veya çıkarır (EPOLLCTLDEL).
  3. *epollwait(int epfd, struct epollevent events, int maxevents, int timeout):** Olay gerçekleşene kadar bekler.

3. Level-Triggered (LT) vs Edge-Triggered (ET)

  • Level-Triggered (Varsayılan): Arabellekte veri olduğu sürece epoll_wait her çağrıldığında sizi uyarır. Kullanımı kolaydır; gelen veriyi tek seferde bitiremezseniz sonraki döngüde tekrar bildirim alırsınız.
  • Edge-Triggered (EPOLLET): Sadece soketin durumunda yeni bir değişiklik (yeni paket gelişi) olduğunda tek bir kez uyarır.

Kritik Kural:* Edge-triggered modda soket mutlaka non-blocking olmalı ve recv() çağrısı EAGAIN / EWOULDBLOCK dönene kadar bir while döngüsü içinde arabellek tamamen boşaltılmalıdır. Aksi takdirde kalan veri için sistem sizi bir daha asla uyarmaz!


4. C++ ile Basit Bir EventLoop Sınıfı

Olay döngüsünü nesne yönelimli ve RAII uyumlu olarak sarmalayalım:

#include <sys/epoll.h>
#include <unistd.h>
#include <vector>
#include <stdexcept>
#include <iostream>

class EventLoop {
public:
    explicit EventLoop(int max_events = 64) : max_events_(max_events) {
        epoll_fd_ = epoll_create1(EPOLL_CLOEXEC);
        if (epoll_fd_ < 0) {
            throw std::runtime_error("epoll_create1 basarisiz!");
        }
        events_.resize(max_events_);
    }

    ~EventLoop() {
        if (epoll_fd_ >= 0) ::close(epoll_fd_);
    }

    bool add_socket(int fd, uint32_t events) {
        epoll_event ev{};
        ev.events = events;
        ev.data.fd = fd;
        return epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev) == 0;
    }

    bool remove_socket(int fd) {
        return epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, nullptr) == 0;
    }

    template <typename Callback>
    void run(int timeout_ms, Callback&& on_event) {
        int nfds = epoll_wait(epoll_fd_, events_.data(), max_events_, timeout_ms);
        if (nfds < 0) {
            if (errno == EINTR) return; // Sinyal kesmesi (signal interrupt)
            throw std::runtime_error("epoll_wait hatasi!");
        }

        for (int i = 0; i < nfds; ++i) {
            on_event(events_[i].data.fd, events_[i].events);
        }
    }

private:
    int epoll_fd_ = -1;
    int max_events_;
    std::vector<epoll_event> events_;
};

5. Kullanım Örneği: Dinleme ve İstemci Yönetimi

void run_server_loop(int server_fd) {
    EventLoop loop(128);

    // Dinleyici sunucu soketini okuma modunda epoll'e ekle
    loop.add_socket(server_fd, EPOLLIN);

    std::cout << "Olay dongusu baslatildi...\n";

    while (true) {
        loop.run(1000, [&](int fd, uint32_t events) {
            if (fd == server_fd) {
                // Yeni bir istemci baglantisi geldi
                sockaddr_in client_addr{};
                socklen_t len = sizeof(client_addr);
                int client_fd = accept4(server_fd, (struct sockaddr*)&client_addr, &len, SOCK_NONBLOCK);
                if (client_fd >= 0) {
                    loop.add_socket(client_fd, EPOLLIN | EPOLLET); // Edge-triggered istemci
                    std::cout << "Yeni baglanti: FD=" << client_fd << "\n";
                }
            } else if (events & EPOLLIN) {
                // Mevcut istemciden veri geldi
                char buf[512];
                ssize_t n = read(fd, buf, sizeof(buf));
                if (n > 0) {
                    write(fd, buf, n); // Echo (Geri yansıt)
                } else if (n == 0 || (n < 0 && errno != EAGAIN)) {
                    loop.remove_socket(fd);
                    close(fd);
                    std::cout << "Baglanti kapandi: FD=" << fd << "\n";
                }
            }
        });
    }
}

6. Çıkarımlar

epoll, Linux üzerinde Nginx, Redis, Node.js (libuv) ve Envoy gibi modern internet altyapısını sırtlayan devasa yazılımların çekirdeğindeki motorudur.

Tek bir thread üzerinde binlerce soketin olaylarını başarıyla yakaladıktan sonra, sıradaki kritik mühendislik problemi: Gelen parçalanmış TCP akışlarını bellek kopyalamadan nasıl birleştireceğiz? Bir sonraki bölümde Halka Arabellek (Ring Buffer) mimarisini inceleyeceğiz.