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
| Sunucu | Model | 10K bağlantıda |
|---|---|---|
| Apache (prefork) | Thread/process per connection | 10.000 thread — ağır, yavaş |
| nginx | Event-driven, epoll | Tek thread per core — hafif, hızlı |
| Node.js | Event loop (libuv + epoll) | Tek thread, callback tabanlı |
Non-blocking mod: fcntl
#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 */
}
}
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
#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
/* 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:
# 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
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ı
02 poll()
poll(), select()'in FD_SETSIZE kısıtını ortadan kaldırır — izlenecek fd listesi dinamik dizide taşınır.
API
#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ı
_GNU_SOURCE gerektirir.Tam örnek: çoklu fd izleme
/* 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
| Özellik | select | poll |
|---|---|---|
| fd limiti | FD_SETSIZE=1024 | Sınırsız (nfds adet) |
| fd_set kopyalama | Her çağrıda kernel'e kopyala | pollfd dizisi kopyalanır ama daha temiz |
| Tarama karmaşıklığı | O(n) — nfds kadar kontrol | O(n) — nfds kadar kontrol |
| Taşınabilirlik | POSIX, her OS | POSIX, her OS |
| Yeniden doldurma | Her çağrıda FD_ZERO + FD_SET zorunlu | events değişmezse sadece revents sıfırlanır |
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
#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
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ı
_GNU_SOURCE gerekir.Echo server skeleton
/* 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 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);
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:
#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);
}
}
}
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.
/* 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)
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
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())
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ı
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:
/* 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:
/* 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 |
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'ı