Tüm eğitimler
Rehber Linux / Performans C asyncio

epoll · select · poll —
I/O multiplexing.

binlerce bağlantıyı tek thread'de yönetmek — select'in sınırlarından epoll'un edge-triggered moduna.

00 Giriş: C10K problemi

10.000 eşzamanlı bağlantıyı tek sunucuda yönetmek — 1999'da Dan Kegel'in tanımladığı bu problem, modern event-driven mimarilerin temelini attı.

Blocking I/O modeli ve maliyeti

Klasik yaklaşım: her gelen bağlantı için bir thread veya process oluştur. Bu model küçük ölçekte çalışır. 10.000 bağlantıda durum şöyle:

  10.000 bağlantı
       │
       ├─ 10.000 thread × ~8 MB stack  =  ~80 GB RAM  (Linux default)
       ├─ 10.000 thread × context switch  =  CPU thrash
       └─ Scheduler O(n) → her tick 10.000 thread seçim yapar
    

Gerçek dünya sınırı çok daha düşük: Linux default stack boyutu 8 MB, ama RSS pratikte 64–128 KB civarında kalır. Yine de 10.000 thread'de context switch maliyeti ciddi bir tavan oluşturur.

Non-blocking I/O + multiplexing

Çözüm, tek thread içinde birden fazla file descriptor'ı aynı anda izlemek: hangisi okumaya hazır, hangisi yazmaya hazır bilgisini OS kernel'den al, sadece hazır olanları işle.

  tek thread
       │
       ├─ fd_1 (socket) ──┐
       ├─ fd_2 (socket) ──┤── kernel'e ver: "hangisi hazır?"
       ├─ fd_3 (pipe)   ──┤
       └─ fd_N (...)    ──┘
                           │
                     [select/poll/epoll]
                           │
                    hazır fd listesi → işle → tekrar bekle
    

nginx vs Apache modeli

SunucuModel10K bağlantıda
Apache (prefork)Thread/process per connection10.000 thread — ağır, yavaş
nginxEvent-driven, epollTek thread per core — hafif, hızlı
Node.jsEvent loop (libuv + epoll)Tek thread, callback tabanlı

Non-blocking mod: fcntl

nonblocking.c
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

/* non-blocking read örneği */
char buf[4096];
ssize_t n = read(fd, buf, sizeof(buf));
if (n == -1) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        /* veri yok — daha sonra tekrar dene */
    } else {
        /* gerçek hata */
    }
}
NOT

Non-blocking I/O tek başına yeterli değildir: hangi fd'nin hazır olduğunu bilmek için multiplexing API (select/poll/epoll) gerekir. Aksi hâlde tüm fd'leri tek tek EAGAIN alana kadar döndürmek (busy-wait) CPU'yu %100 kullanır.

01 select()

POSIX standardı, her platformda çalışır — ama FD_SETSIZE=1024 hard limiti onu modern sistemler için yetersiz kılar.

API

select-api.c
#include <sys/select.h>

/* fd_set işlemleri */
FD_ZERO(&set);          /* seti sıfırla */
FD_SET(fd, &set);       /* fd'yi sete ekle */
FD_CLR(fd, &set);       /* fd'yi setten çıkar */
FD_ISSET(fd, &set);     /* fd sette mi? */

/* imza */
int select(int nfds,                  /* izlenecek en büyük fd + 1 */
           fd_set *readfds,            /* okuma izlenenler */
           fd_set *writefds,           /* yazma izlenenler */
           fd_set *exceptfds,          /* istisna (nadiren kullanılır) */
           struct timeval *timeout);   /* NULL = sonsuz bekle */

Tam çalışan örnek: stdin okuma ile timeout

select-stdin.c
/* gcc -o select-stdin select-stdin.c */
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>

int main(void) {
    fd_set readfds;
    struct timeval tv;
    char buf[256];

    while (1) {
        /* DİKKAT: select() her çağrıda fd_set'i modifiye eder — her seferinde yeniden doldur */
        FD_ZERO(&readfds);
        FD_SET(STDIN_FILENO, &readfds);

        /* 3 saniye timeout */
        tv.tv_sec  = 3;
        tv.tv_usec = 0;

        int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);

        if (ret == -1) {
            perror("select");
            break;
        } else if (ret == 0) {
            printf("timeout — giriş yok\n");
        } else if (FD_ISSET(STDIN_FILENO, &readfds)) {
            ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
            if (n <= 0) break;
            buf[n] = '\0';
            printf("okunan: %s", buf);
        }
    }
    return 0;
}

FD_SETSIZE=1024 limiti

fd_set kernel space'e kopyalanan sabit boyutlu bir bitmask'tır. Boyutu derleme zamanında FD_SETSIZE sabiti ile belirlenir — çoğu sistemde 1024. Stack üzerinde allocate edilir:

bash
# FD_SETSIZE değerini kontrol et
grep 'FD_SETSIZE' /usr/include/bits/typesizes.h
# #define FD_SETSIZE 1024

# fd sayısını artırmak için derleme zamanında override (taşınabilir değil)
gcc -DFD_SETSIZE=4096 myserver.c -o myserver
# ama bu kernel ABI'ını kırar — önerilmez
DİKKAT — FD_SETSIZE tuzağı

fd numarası 1024 veya daha yüksek bir socket accept edilirse ve bu fd, FD_SET ile fd_set'e eklenirse, buffer overflow oluşur — tanımsız davranış. select() 1024'ten büyük fd numarasıyla çağrılırsa kernel hata döner. 100'den fazla eşzamanlı bağlantı bekliyorsan poll veya epoll kullan.

select() tuzakları

fd_set her çağrıda sıfırlanmalıselect() dönüşte fd_set içindeki hazır olmayan fd'leri temizler. Döngünün başında mutlaka FD_ZERO + FD_SET yap.
timeval her çağrıda yenilenmeliLinux, select() sonrası timeval'i kalan süre ile günceller. POSIX bunu garanti etmez — her döngüde yeniden ata.
nfds = max_fd + 1select() 0'dan nfds-1'e kadar kontrol eder. max fd'yi takip et ve +1 ekle.

02 poll()

poll(), select()'in FD_SETSIZE kısıtını ortadan kaldırır — izlenecek fd listesi dinamik dizide taşınır.

API

poll-api.c
#include <poll.h>

struct pollfd {
    int   fd;       /* izlenecek fd */
    short events;   /* izlenecek olaylar (giriş) */
    short revents;  /* gerçekleşen olaylar (çıkış — kernel doldurur) */
};

int poll(struct pollfd *fds,   /* fd dizisi */
         nfds_t nfds,           /* dizi eleman sayısı */
         int timeout);          /* ms cinsinden, -1 = sonsuz */

events / revents bayrakları

POLLINOkuma verisi mevcut (read() bloklamaz).
POLLOUTYazma mümkün (write() bloklamaz).
POLLERRHata durumu. Sadece revents'te görünür.
POLLHUPKarşı taraf bağlantıyı kapattı (hangup). Sadece revents'te.
POLLNVALGeçersiz fd (kapalı). Sadece revents'te.
POLLRDHUPKarşı taraf write tarafını kapattı (Linux uzantısı). _GNU_SOURCE gerektirir.

Tam örnek: çoklu fd izleme

poll-multi.c
/* gcc -o poll-multi poll-multi.c */
#include <stdio.h>
#include <poll.h>
#include <unistd.h>
#include <string.h>

int main(void) {
    /* stdin ve bir pipe fd'sini aynı anda izle */
    int pipefd[2];
    pipe(pipefd);

    struct pollfd fds[2] = {
        { .fd = STDIN_FILENO, .events = POLLIN },
        { .fd = pipefd[0],   .events = POLLIN },
    };

    /* pipe'a asenkron veri yaz (gerçek senaryoda başka thread/process) */
    write(pipefd[1], "pipe verisi\n", 12);

    int ret = poll(fds, 2, 3000); /* 3 saniye timeout */
    if (ret <= 0) {
        puts(ret == 0 ? "timeout" : "poll hatası");
        return 1;
    }

    char buf[256];
    for (int i = 0; i < 2; i++) {
        if (fds[i].revents & POLLIN) {
            ssize_t n = read(fds[i].fd, buf, sizeof(buf) - 1);
            buf[n] = '\0';
            printf("fd[%d] okunan: %s", i, buf);
        }
        if (fds[i].revents & POLLHUP) {
            printf("fd[%d] kapandı\n", i);
        }
    }

    close(pipefd[0]); close(pipefd[1]);
    return 0;
}

select'e göre avantajlar ve sınırlar

Özellikselectpoll
fd limitiFD_SETSIZE=1024Sınırsız (nfds adet)
fd_set kopyalamaHer çağrıda kernel'e kopyalapollfd dizisi kopyalanır ama daha temiz
Tarama karmaşıklığıO(n) — nfds kadar kontrolO(n) — nfds kadar kontrol
TaşınabilirlikPOSIX, her OSPOSIX, her OS
Yeniden doldurmaHer çağrıda FD_ZERO + FD_SET zorunluevents değişmezse sadece revents sıfırlanır
NOT

poll()'un select'e göre temel avantajı FD_SETSIZE kısıtının olmamasıdır. Ama her iki API'da da kernel, her çağrıda tüm fd listesini O(n) karmaşıklıkla tarar. 1000+ fd için bu her çağrıda ciddi bir overhead demektir — bu yüzden epoll gerekir.

03 epoll temelleri

epoll, kernel içinde fd'leri bir red-black tree'de tutar — sadece hazır olanları döner, tüm listeyi taramaz. O(1) notification.

API

epoll-api.c
#include <sys/epoll.h>

/* epoll instance oluştur — epfd döner (bir fd) */
int epfd = epoll_create1(0);
/* EPOLL_CLOEXEC flag'i: exec() sonrası fd otomatik kapanır */
int epfd = epoll_create1(EPOLL_CLOEXEC);

/* fd'yi epoll'a ekle / değiştir / çıkar */
struct epoll_event ev;
ev.events   = EPOLLIN;  /* izlenecek olaylar */
ev.data.fd  = fd;       /* geri dönüşte tanımlama için veri */

epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);  /* ekle */
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);  /* güncelle */
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); /* çıkar */

/* hazır olayları bekle */
struct epoll_event events[64];
int n = epoll_wait(epfd, events, 64, -1); /* -1 = sonsuz bekle */
/* n: hazır fd sayısı, events[0..n-1] dolu */

struct epoll_event

epoll-event-struct.c
struct epoll_event {
    uint32_t events;   /* EPOLLIN, EPOLLOUT, EPOLLERR, EPOLLHUP, EPOLLET ... */
    epoll_data_t data; /* kullanıcı verisi — union */
};

typedef union epoll_data {
    void    *ptr;  /* struct pointer — bağlam taşıma için */
    int      fd;   /* sadece fd numarası */
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

epoll olayları

EPOLLINOkuma mevcut — read() bloklamaz.
EPOLLOUTYazma mümkün — write() bloklamaz.
EPOLLERRHata durumu. Otomatik izlenir, events'te belirtmek gerekmez.
EPOLLHUPHangup — karşı taraf kapandı. Otomatik izlenir.
EPOLLETEdge-triggered mod. Bölüm 04'te detay.
EPOLLONESHOTOlay bir kez tetiklenir, sonra fd deaktive olur. epoll_ctl MOD ile yeniden etkinleştir.
EPOLLRDHUPKarşı taraf write tarafını kapattı (half-close). _GNU_SOURCE gerekir.
EPOLLEXCLUSIVEBirden fazla thread aynı fd'yi izliyorsa sadece birine bildir (thundering herd önleme).

Echo server skeleton

epoll-echo-skeleton.c
/* gcc -o epoll-echo epoll-echo-skeleton.c */
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define PORT      8080
#define MAX_EVENTS 64

static int make_listen_socket(int port);
static int set_nonblocking(int fd);

int main(void) {
    int listen_fd = make_listen_socket(PORT);
    set_nonblocking(listen_fd);

    int epfd = epoll_create1(EPOLL_CLOEXEC);

    struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

    struct epoll_event events[MAX_EVENTS];

    printf("dinleniyor: port %d\n", PORT);

    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;

            if (fd == listen_fd) {
                /* yeni bağlantı kabul et */
                int conn = accept(listen_fd, NULL, NULL);
                if (conn == -1) continue;
                set_nonblocking(conn);
                ev.events  = EPOLLIN;
                ev.data.fd = conn;
                epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev);
            } else {
                /* istemciden veri */
                char buf[4096];
                ssize_t r = read(fd, buf, sizeof(buf));
                if (r <= 0) {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                } else {
                    write(fd, buf, r); /* echo */
                }
            }
        }
    }
}

static int make_listen_socket(int port) {
    int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(port),
    };
    bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(fd, SOMAXCONN);
    return fd;
}

static int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

04 Level-triggered vs Edge-triggered

epoll'un iki çalışma modu arasındaki fark, veri buffer'ında ne zaman bildirim yapıldığıdır.

Level-triggered (LT) — varsayılan

Kernel buffer'da okunmamış veri olduğu sürece, her epoll_wait çağrısında EPOLLIN döner. "Seviye" — buffer dolu kaldıkça tetikler:

  100 byte veri geldi
  epoll_wait → EPOLLIN  ✓
  50 byte oku → 50 byte kaldı
  epoll_wait → EPOLLIN  ✓  (hâlâ veri var)
  50 byte oku → 0 byte kaldı
  epoll_wait → bekle  (veri yok)
    

Edge-triggered (ET) — EPOLLET

Kernel, sadece yeni veri geldiğinde (buffer boşken dolduğunda = kenar değişimi) bir kez EPOLLIN üretir. Veri okunmadan bir sonraki epoll_wait'e geçilirse bildirim kaybolur:

  100 byte veri geldi
  epoll_wait → EPOLLIN  ✓  (kenar: boş→dolu geçişi)
  50 byte oku → 50 byte kaldı
  epoll_wait → bekle  ✗  (kenar değişmedi — hâlâ dolu)
  50 byte daha oku → 0 byte
  epoll_wait → bekle  (boş — yeni veri bekleniyor)
    

ET zorunluluğu: EAGAIN'e kadar oku

ET modunda bir bildirim aldığında, o fd'deki tüm veriyi bitirene kadar döngüde okumak zorundasın. Aksi hâlde bir sonraki epoll_wait bildirim üretmez ve veri kaybolmaz ama asla işlenmez:

et-recv-loop.c
/* ET modunda EPOLLIN geldiğinde çağrılır */
void handle_epollin_et(int fd) {
    char buf[4096];
    ssize_t total = 0;

    while (1) {
        ssize_t n = read(fd, buf, sizeof(buf));

        if (n == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                /* buffer boşaldı — döngüyü bitir, epoll_wait'e dön */
                break;
            }
            /* gerçek hata */
            perror("read");
            break;
        }
        if (n == 0) {
            /* bağlantı kapandı (EOF) */
            close(fd);
            break;
        }

        total += n;
        /* veriyi işle */
        write(fd, buf, n); /* echo */
    }
    printf("toplam okunan: %zd byte\n", total);
}

/* epoll_ctl ile ET modunda ekle */
struct epoll_event ev = {
    .events  = EPOLLIN | EPOLLET,  /* edge-triggered */
    .data.fd = client_fd,
};
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
NOT

ET daha yüksek performans sağlar çünkü aynı verisi için tekrar tekrar epoll_wait→işle döngüsüne girmez. Ama doğru implement etmek daha zordur: fd mutlaka non-blocking olmalı, EAGAIN alana kadar döngü kırılmamalı. Hata: ET + blocking fd kombinasyonu kesinlikle deadlock'a yol açar.

05 epoll_data_t ile bağlam taşıma

epoll_data_t union'ı ile her fd'ye bağlam verisi atanır — bağlantı state'ini pointer olarak taşıyarak çok istemcili yönetim mümkün olur.

data.fd vs data.ptr

data.fd sadece fd numarasını tutar. Birden fazla bağlantı varsa her bağlantının buffer'ını, durumunu ve protokol state makinesini takip etmek için data.ptr kullan:

conn-state.c
#include <sys/epoll.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUF_SIZE 8192

/* bağlantı başına state yapısı */
typedef struct {
    int    fd;
    char   buf[BUF_SIZE];
    int    buf_len;
    int    bytes_sent;
    int    state;    /* 0=reading, 1=writing, 2=closing */
} conn_state_t;

conn_state_t* conn_new(int fd) {
    conn_state_t *c = calloc(1, sizeof(conn_state_t));
    c->fd = fd;
    return c;
}

void conn_free(conn_state_t *c) {
    if (c) { close(c->fd); free(c); }
}

/* yeni bağlantıyı epoll'a ptr ile ekle */
void add_conn(int epfd, int client_fd) {
    conn_state_t *c = conn_new(client_fd);
    struct epoll_event ev = {
        .events   = EPOLLIN | EPOLLET,
        .data.ptr = c,   /* fd yerine struct pointer */
    };
    epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
}

/* event loop içinde bağlamı geri al */
void process_event(struct epoll_event *ev) {
    conn_state_t *c = (struct conn_state_t *)ev->data.ptr;

    if (ev->events & EPOLLIN) {
        ssize_t n = read(c->fd, c->buf + c->buf_len,
                          BUF_SIZE - c->buf_len);
        if (n > 0) {
            c->buf_len += n;
            /* protokol işleme buraya gelir */
        } else if (n == 0 || (n == -1 && errno != EAGAIN)) {
            conn_free(c);
        }
    }
}
DİKKAT — double free

conn_state_t pointer'ını hem EPOLLERR hem EPOLLHUP aldığında iki kez free etmemeye dikkat et. Hemen epoll_ctl DEL + close çağır, pointer'ı NULL'a ata. Aynı fd'ye ait birden fazla event, tek epoll_wait döngüsünde gelebilir.

06 Tam accept loop implementasyonu

Gerçek bir multi-client echo server — listen socket, accept4, hata yönetimi ve bağlantı temizliği ile birlikte.

epoll-server.c
/* gcc -o epoll-server epoll-server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT       8080
#define MAX_EVENTS 128
#define BUF_SIZE   4096

static int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

static int create_listen_socket() {
    int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (fd == -1) { perror("socket"); exit(1); }

    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family      = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port        = htons(PORT),
    };
    if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("bind"); exit(1);
    }
    if (listen(fd, SOMAXCONN) == -1) {
        perror("listen"); exit(1);
    }
    return fd;
}

static void handle_accept(int epfd, int listen_fd) {
    while (1) {
        struct sockaddr_in client_addr;
        socklen_t addrlen = sizeof(client_addr);

        /* accept4: atomik olarak non-blocking + cloexec ayarla */
        int conn = accept4(listen_fd,
                            (struct sockaddr*)&client_addr,
                            &addrlen,
                            SOCK_NONBLOCK | SOCK_CLOEXEC);

        if (conn == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                break;  /* bekleyen bağlantı kalmadı */
            if (errno == EINTR)
                continue;  /* sinyal tarafından kesildi, tekrar dene */
            perror("accept4");
            break;
        }

        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
        printf("yeni bağlantı: %s:%d (fd=%d)\n",
               ip, ntohs(client_addr.sin_port), conn);

        struct epoll_event ev = {
            .events  = EPOLLIN | EPOLLET | EPOLLRDHUP,
            .data.fd = conn,
        };
        if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev) == -1) {
            perror("epoll_ctl add conn");
            close(conn);
        }
    }
}

static void handle_client(int epfd, struct epoll_event *ev) {
    int fd = ev->data.fd;

    if (ev->events & (EPOLLHUP | EPOLLRDHUP | EPOLLERR)) {
        printf("bağlantı kapandı: fd=%d\n", fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        close(fd);
        return;
    }

    if (ev->events & EPOLLIN) {
        char buf[BUF_SIZE];
        while (1) {
            ssize_t n = read(fd, buf, sizeof(buf));
            if (n == -1) {
                if (errno == EAGAIN || errno == EWOULDBLOCK)
                    break;  /* buffer boşaldı */
                if (errno == ECONNRESET) {
                    printf("bağlantı sıfırlandı: fd=%d\n", fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                    return;
                }
                perror("read"); break;
            }
            if (n == 0) {
                /* EOF — bağlantı düzgün kapandı */
                epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
                return;
            }
            /* echo: okunanı geri yaz */
            ssize_t sent = 0;
            while (sent < n) {
                ssize_t w = write(fd, buf + sent, n - sent);
                if (w <= 0) break;
                sent += w;
            }
        }
    }
}

int main(void) {
    int listen_fd = create_listen_socket();
    set_nonblocking(listen_fd);

    int epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) { perror("epoll_create1"); exit(1); }

    struct epoll_event ev = {
        .events  = EPOLLIN | EPOLLET,
        .data.fd = listen_fd,
    };
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

    struct epoll_event events[MAX_EVENTS];
    printf("epoll echo server başladı, port %d\n", PORT);

    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (n == -1) {
            if (errno == EINTR) continue; /* sinyal */
            perror("epoll_wait"); break;
        }
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == listen_fd)
                handle_accept(epfd, listen_fd);
            else
                handle_client(epfd, &events[i]);
        }
    }
    close(epfd);
    close(listen_fd);
    return 0;
}

Bu bölümde öğrendikleriniz

  • accept4() ile atomik SOCK_NONBLOCK + SOCK_CLOEXEC ayarlanır
  • Listen socket ET modunda, accept döngüsü EAGAIN'e kadar çalışır
  • EPOLLRDHUP ile half-close durumu temiz yakalanır
  • EINTR kontrolü: signal handler'lar epoll_wait'i keser, döngü devam etmeli
  • ECONNRESET: karşı taraf RST gönderdi — fd hemen temizlenmeli

07 Python asyncio ve epoll bağlantısı

asyncio'nun altında epoll vardır — await bir coroutine'i askıya aldığında, event loop ilgili fd'yi epoll ile izler.

Event loop ve selector katmanı

  asyncio.run(main())
       │
  EventLoop  (DefaultEventLoopPolicy → SelectorEventLoop)
       │
  SelectorEventLoop._selector
       ├─ Linux    → EpollSelector  (selectors.EpollSelector)
       ├─ macOS    → KqueueSelector
       └─ Windows  → SelectSelector (ProactorEventLoop tercih edilir)
    
asyncio-selector.py
import asyncio
import selectors

# çalışan selector tipini doğrula
loop = asyncio.get_event_loop()
print(type(loop._selector))
# <class 'selectors.EpollSelector'>  (Linux'ta)

# EpollSelector'ın altındaki epoll fd'sini al
selector = loop._selector
print(selector._epoll)
# <select.epoll object at 0x...>

asyncio ile TCP server ve client

asyncio-server.py
import asyncio

async def handle_client(reader: asyncio.StreamReader,
                        writer: asyncio.StreamWriter):
    addr = writer.get_extra_info('peername')
    print(f"bağlantı: {addr}")

    try:
        while True:
            # await → coroutine askıya alınır, event loop epoll ile bekler
            data = await reader.read(4096)
            if not data:
                break
            writer.write(data)         # buffer'a yaz
            await writer.drain()       # kernel buffer'ı boşalana kadar bekle
    except (asyncio.CancelledError, ConnectionResetError):
        pass
    finally:
        writer.close()
        await writer.wait_closed()
        print(f"kapatıldı: {addr}")

async def main():
    server = await asyncio.start_server(
        handle_client, '0.0.0.0', 8080
    )
    async with server:
        await server.serve_forever()

asyncio.run(main())
asyncio-client.py
import asyncio

async def tcp_client():
    reader, writer = await asyncio.open_connection('127.0.0.1', 8080)

    writer.write(b"merhaba\n")
    await writer.drain()

    data = await reader.readline()
    print(f"yanıt: {data.decode()!r}")

    writer.close()
    await writer.wait_closed()

asyncio.run(tcp_client())

await → epoll bağlantısı

asyncio'da await reader.read(4096) çağrısının arka planında olan şu:

  await reader.read(4096)
       │
  SelectorEventLoop._read_ready_cb → fd EPOLLIN bekliyor
       │
  epoll_wait() (blocking — tüm ready event'ler için)
       │
  fd hazır → callback → coroutine resume → read() çağrısı
    
NOT — uvloop

uvloop, asyncio'nun default event loop'unun yerine geçen Cython tabanlı bir alternatiftir. libuv (Node.js'in C event loop kütüphanesi) kullanır ve Linux'ta epoll'u daha verimli şekilde sarmalar. pip install uvloop, ardından asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) — benchmark'larda asyncio'dan 2-4x daha hızlı sonuçlar verir.

08 kqueue, io_uring ve gelecek

epoll Linux'a özgüdür. BSD/macOS'ta kqueue, Linux 5.1+'da ise io_uring daha güçlü bir alternatif sunar.

kqueue — BSD ve macOS

kqueue, FreeBSD 4.1 (2000) ile geldi, macOS/iOS'ta da desteklenir. API epoll'a benzer ama event filter sistemi daha zengindir:

kqueue-example.c
/* macOS / FreeBSD — Linux'ta derlenmez */
#include <sys/event.h>
#include <unistd.h>

int main(void) {
    int kq = kqueue();           /* epoll_create1 gibi */

    struct kevent change;
    /* EV_SET: fd, filter, flags, fflags, data, udata */
    EV_SET(&change, STDIN_FILENO, EVFILT_READ, EV_ADD | EV_ENABLE,
           0, 0, NULL);

    kevent(kq, &change, 1, NULL, 0, NULL);  /* ekle */

    struct kevent events[16];
    int n = kevent(kq, NULL, 0, events, 16, NULL);  /* bekle */

    for (int i = 0; i < n; i++) {
        if (events[i].filter == EVFILT_READ)
            printf("fd %lu okumaya hazır\n", events[i].ident);
    }

    close(kq);
    return 0;
}

io_uring — Linux 5.1+

io_uring, kernel ve user space arasında paylaşılan ring buffer kullanır. Sistem çağrısı overhead'i minimize edilir; batch I/O, zero-copy, async dosya I/O mümkün olur:

io-uring-echo.c
/* gcc -o io-uring-echo io-uring-echo.c -luring */
#include <liburing.h>
#include <stdio.h>
#include <unistd.h>

#define QUEUE_DEPTH 64

int main(void) {
    struct io_uring ring;
    int ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
    if (ret) { perror("io_uring_queue_init"); return 1; }

    char buf[256];

    /* submission queue'ya async read isteği ekle */
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, STDIN_FILENO, buf, sizeof(buf), 0);
    io_uring_sqe_set_data64(sqe, 1); /* kullanıcı veri = 1 */

    io_uring_submit(&ring);  /* tek syscall: tüm pending SQE'leri gönder */

    /* completion queue'dan sonuç al */
    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);

    if (cqe->res > 0) {
        buf[cqe->res] = '\0';
        printf("okunan: %s", buf);
    }

    io_uring_cqe_seen(&ring, cqe);  /* CQE'yi tüketildi olarak işaretle */
    io_uring_queue_exit(&ring);
    return 0;
}

Karşılaştırma tablosu

Özellik select poll epoll kqueue io_uring
OS desteği POSIX POSIX Linux 2.5.44+ BSD / macOS Linux 5.1+
fd limiti FD_SETSIZE (1024) Sınırsız Sınırsız Sınırsız Sınırsız
Notification O(n) tarama O(n) tarama O(1) ready O(1) ready O(1) async
Kernel/user kopyalama Her çağrıda tüm set Her çağrıda dizi Sadece hazır event'ler Sadece hazır event'ler Ring buffer — zero-copy
Dosya I/O desteği Hayır Hayır Hayır Kısmi Evet (async file I/O)
Syscall sayısı (batch) N event = N syscall N event = N syscall N event = 1 epoll_wait N event = 1 kevent N iş = 1 submit
Edge-triggered Hayır Hayır EPOLLET ile evet EV_CLEAR ile evet Doğası gereği async
Öğrenme eğrisi Düşük Düşük Orta Orta Yüksek
NOT — liburing

liburing, io_uring system call'larını sarmalayan C kütüphanesidir. Ax Bhatt'ın Jens Axboe tarafından geliştirilmiştir (kernel io_uring'in yazarı). apt install liburing-dev veya Buildroot/Yocto recipe olarak eklenebilir. io_uring, yüksek throughput depolama I/O, network I/O ve async dosya işlemleri için şu an en performanslı Linux API'dır.

Bu bölümde öğrendikleriniz

  • select(): POSIX ama FD_SETSIZE=1024 hard limiti var, O(n) tarama
  • poll(): fd sayısı sınırsız ama yine O(n); kernel/user kopyalama var
  • epoll: O(1) hazır fd bildirim, Linux-spesifik, LT ve ET mod desteği
  • ET modunda fd non-blocking olmalı, EAGAIN'e kadar döngüde oku
  • data.ptr ile bağlantı state'i epoll event'ine taşınır
  • asyncio, Linux'ta altta EpollSelector kullanır
  • io_uring: ring buffer + zero-copy + async dosya I/O ile geleceğin API'ı