00 io_uring neden: epoll ve AIO sınırları
io_uring, 2019'da Linux 5.1 ile tanıtılan ve geleneksel async I/O mekanizmalarının sorunlarını çözmek için tasarlanmış bir kernel-user arayüzüdür.
Geleneksel I/O mekanizmalarının sorunları
Blocking I/O: en basit yöntemdir; read()/write() tamamlanana kadar thread bloklanır. Eş zamanlı çok bağlantı için çok thread gerekir; thread overhead'i önemlidir. select/poll: hangi fd'nin hazır olduğunu bildirir; ama asıl I/O hâlâ blocking'dir. Her çağrıda tüm fd kümesini kernel'a kopyalamak gerekir. epoll: edge/level-triggered bildirim sistemi; büyük fd setleri için verimli. Ancak yalnızca network socket ve pipe için çalışır; düzenli dosyalar için değil. Linux AIO (libaio): asenkron dosya I/O desteği; ama yalnızca O_DIRECT modunda, sadece dosyalar için, completion event polling gerektirir ve network socket'leri desteklemez.
| Mekanizma | Dosya | Ağ | Syscall overhead | Sınırı |
|---|---|---|---|---|
| Blocking I/O | Evet | Evet | Her op'ta | Thread başına bir op |
| epoll | Hayır | Evet | Her event'te | Dosya I/O yok |
| Linux AIO | O_DIRECT | Hayır | io_submit başına | Tampon I/O yok, ağ yok |
| io_uring | Evet | Evet | Toplu veya sıfır | Neredeyse yok |
io_uring tasarım felsefesi
io_uring'in temel yeniliği, kernel ile kullanıcı alanının doğrudan paylaştığı bir çift yönlü ring buffer kullanmasıdır. Uygulama, işlemleri Submission Queue (SQ)'ya yazar; kernel bunları tamamladıkça Completion Queue (CQ)'ya sonuçları yazar. İki taraf da bu buffer'lara syscall yapmadan doğrudan erişir.
Kullanıcı: SQE yaz → io_uring_submit() [opsiyonel] → CQE bekle → sonuç al Kernel: SQE oku → I/O yap → CQE yaz → kullanıcıya sinyal
Syscall overhead azaltma
Kernel versiyonu gereksinimleri
| Özellik | Minimum Kernel |
|---|---|
| Temel io_uring (dosya I/O) | 5.1 |
| IORING_OP_ACCEPT, SEND, RECV | 5.5 |
| IORING_OP_OPENAT, STATX | 5.6 |
| SQPOLL modu iyileştirmeleri | 5.11 |
| Multi-shot accept | 5.19 |
| io_uring over io_uring (nesting) | 6.0 |
Bu bölümde
- epoll: ağ için iyi ama dosya I/O yok; Linux AIO: yalnızca O_DIRECT dosya, ağ yok
- io_uring: hem dosya hem ağ, toplu submit ile syscall overhead'i minimuma indirir
- SQ/CQ ring buffer: kernel ve kullanıcı alanı paylaşımlı bellek; kopyalama yok
- Minimum kernel: 5.1 (temel); ağ operasyonları için 5.5+
01 SQ ve CQ: ring buffer mimarisi
io_uring'in kalbi iki ring buffer'dır: Submission Queue (SQ) ve Completion Queue (CQ). Her ikisi de kernel ve kullanıcı alanı tarafından paylaşılan mmap'li bellek bölgelerindedir.
io_uring_setup() sistem çağrısı
io_uring_setup() kernel'dan bir io_uring instance'ı ister ve geri dönen fd üzerinden ring buffer'lara mmap yapılır. Doğrudan sistem çağrısı kullanmak yerine liburing kütüphanesi bu karmaşıklığı gizler.
#include <linux/io_uring.h>
#include <sys/syscall.h>
#include <sys/mman.h>
static inline int io_uring_setup(uint32_t entries,
struct io_uring_params *p)
{
return (int)syscall(__NR_io_uring_setup, entries, p);
}
int setup_io_uring_raw(uint32_t sq_entries)
{
struct io_uring_params params;
memset(¶ms, 0, sizeof(params));
/* Flags:
* IORING_SETUP_SQPOLL — kernel polling thread
* IORING_SETUP_IOPOLL — polling mode (diskler için)
* IORING_SETUP_SINGLE_ISSUER — yalnızca tek thread submit
*/
int ring_fd = io_uring_setup(sq_entries, ¶ms);
if (ring_fd < 0) { perror("io_uring_setup"); return -1; }
/* SQ ring mmap */
size_t sq_ring_size = params.sq_off.array +
params.sq_entries * sizeof(uint32_t);
void *sq_ring = mmap(0, sq_ring_size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
ring_fd, IORING_OFF_SQ_RING);
/* SQE'ler (entry'ler) ayrı bir offset'te */
size_t sqe_size = params.sq_entries * sizeof(struct io_uring_sqe);
void *sqes = mmap(0, sqe_size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
ring_fd, IORING_OFF_SQES);
/* CQ ring mmap */
size_t cq_ring_size = params.cq_off.cqes +
params.cq_entries * sizeof(struct io_uring_cqe);
void *cq_ring = mmap(0, cq_ring_size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
ring_fd, IORING_OFF_CQ_RING);
return ring_fd;
}
SQE yapısı
Her Submission Queue Entry (SQE), tek bir async I/O operasyonunu tanımlar. Operasyon tipi, fd, offset, buffer pointer ve user_data alanları içerir.
/* struct io_uring_sqe'nin önemli alanları */
struct io_uring_sqe {
__u8 opcode; /* IORING_OP_READ, WRITE, vb. */
__u8 flags; /* IOSQE_FIXED_FILE, IOSQE_IO_DRAIN, vb. */
__u16 ioprio; /* I/O önceliği */
__s32 fd; /* Dosya tanımlayıcı (veya registered fd index) */
union {
__u64 off; /* Dosya offseti */
__u64 addr2; /* Op'a özel */
};
union {
__u64 addr; /* Buffer adresi (IORING_OP_READ/WRITE) */
__u64 splice_off_in;
};
__u32 len; /* Buffer uzunluğu veya op'a özel */
union {
__kernel_rwf_t rw_flags;
__u32 fsync_flags;
__u16 poll_events;
__u32 sync_range_flags;
__u32 msg_flags;
__u32 timeout_flags;
__u32 accept_flags;
__u32 cancel_flags;
__u32 open_flags;
__u32 statx_flags;
__u32 fadvise_advice;
__u32 splice_flags;
__u32 rename_flags;
__u32 unlink_flags;
};
__u64 user_data; /* CQE'de geri döner — kullanıcı etiket */
/* ... padding ... */
};
CQE yapısı
/* Completion Queue Entry: 16 bayt */
struct io_uring_cqe {
__u64 user_data; /* SQE'deki user_data'nın aynısı */
__s32 res; /* Sonuç: başarıda byte sayısı, hatada -errno */
__u32 flags; /* IORING_CQE_F_MORE (multi-shot vb.) */
};
/* res < 0: hata; errno = -res */
/* res >= 0: başarı; read/write için okunan/yazılan bayt sayısı */
Bu bölümde
- io_uring_setup(): SQ/CQ ring buffer'larını oluşturur; geri dönen fd üzerinden mmap edilir
- SQE: opcode + fd + off + addr + len + user_data; her async operasyon bir SQE
- CQE: user_data + res + flags; res negatifse -errno, pozitifse işlem sonucu
- user_data: SQE'yi CQE ile eşleştirmenin tek yolu; pointer, index veya özel etiket kullanılır
02 liburing ile başlangıç
liburing, io_uring sistem çağrılarını saran yüksek seviyeli bir C kütüphanesidir. Ring buffer yönetimini, memory ordering ve submission/completion döngüsünü kolaylaştırır.
Kurulum
# Debian/Ubuntu
sudo apt-get install -y liburing-dev
# Kaynak koddan derleme (embedded için)
git clone https://github.com/axboe/liburing.git
cd liburing
./configure --prefix=/usr
make -j$(nproc)
sudo make install
# Çapraz derleme (embedded Linux)
./configure --prefix=/sysroot/usr CC=arm-linux-gnueabihf-gcc
make -j$(nproc) ARCH=arm
# Derleme bayrağı
# gcc -o myapp myapp.c -luring
io_uring başlatma ve kapatma
#include <liburing.h>
#include <stdio.h>
int main(void)
{
struct io_uring ring;
/* Ring oluştur: 32 entry'lik SQ */
int ret = io_uring_queue_init(32, &ring, 0);
if (ret < 0) {
fprintf(stderr, "io_uring_queue_init: %s\n", strerror(-ret));
return 1;
}
/* Flags ile başlatma seçenekleri */
struct io_uring ring2;
struct io_uring_params params;
memset(¶ms, 0, sizeof(params));
/* SQPOLL: kernel polling thread — yüksek throughput için */
/* params.flags |= IORING_SETUP_SQPOLL; */
/* params.sq_thread_idle = 2000; */ /* 2 saniye sonra uy */
/* SINGLE_ISSUER: yalnızca bir thread submit eder */
params.flags |= IORING_SETUP_SINGLE_ISSUER;
ret = io_uring_queue_init_params(32, &ring2, ¶ms);
if (ret < 0) { perror("init_params"); return 1; }
/* ... kullan ... */
/* Temizle */
io_uring_queue_exit(&ring);
io_uring_queue_exit(&ring2);
return 0;
}
Temel liburing fonksiyonları
| Fonksiyon | Açıklama |
|---|---|
io_uring_queue_init(entries, ring, flags) | Ring buffer oluştur |
io_uring_get_sqe(ring) | Boş SQE al (NULL ise kuyruk dolu) |
io_uring_prep_read(sqe, fd, buf, nbytes, offset) | Read operasyonu hazırla |
io_uring_prep_write(sqe, fd, buf, nbytes, offset) | Write operasyonu hazırla |
io_uring_sqe_set_data(sqe, data) | user_data ata (void *) |
io_uring_submit(ring) | SQE'leri kernel'a gönder |
io_uring_wait_cqe(ring, cqe_ptr) | CQE gelene kadar bekle |
io_uring_peek_cqe(ring, cqe_ptr) | Beklemeden CQE al (0 = yok) |
io_uring_cqe_get_data(cqe) | CQE'den user_data al |
io_uring_cqe_seen(ring, cqe) | CQE işlendi, ring'e iade et |
io_uring_queue_exit(ring) | Ring'i kapat, kaynakları serbest bırak |
void check_features(struct io_uring *ring)
{
struct io_uring_probe *probe = io_uring_get_probe_ring(ring);
if (!probe) { fprintf(stderr, "probe başarısız\n"); return; }
printf("IORING_OP_READ: %s\n",
io_uring_opcode_supported(probe, IORING_OP_READ) ? "destekleniyor" : "yok");
printf("IORING_OP_ACCEPT: %s\n",
io_uring_opcode_supported(probe, IORING_OP_ACCEPT) ? "destekleniyor" : "yok");
printf("IORING_OP_SEND: %s\n",
io_uring_opcode_supported(probe, IORING_OP_SEND) ? "destekleniyor" : "yok");
free(probe);
}
Bu bölümde
- io_uring_queue_init(entries, ring, flags): ring buffer oluşturur; entries = max eş zamanlı op
- IORING_SETUP_SQPOLL: kernel polling thread; yüksek throughput ama CPU kullanır
- IORING_SETUP_SINGLE_ISSUER: tek thread submit; kernel optimizasyonu etkinleştirir
- io_uring_get_probe_ring(): hangi opcode'ların desteklendiğini kernel'a sorar
03 Temel operasyonlar: READ, WRITE, FSYNC
io_uring'in en temel operasyonları dosya okuma ve yazma. liburing prep fonksiyonları SQE alanlarını doğru şekilde doldurur.
Async read operasyonu
#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
int async_read_file(const char *path, char *buf, size_t len)
{
struct io_uring ring;
io_uring_queue_init(4, &ring, 0);
int fd = open(path, O_RDONLY);
if (fd < 0) { perror("open"); return -1; }
/* SQE al ve hazırla */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) { fprintf(stderr, "SQ dolu\n"); return -1; }
io_uring_prep_read(sqe, fd, buf, len, 0 /* offset */);
/* user_data: bu SQE'yi tanımlamak için kullanılır */
io_uring_sqe_set_data(sqe, (void *)(uintptr_t)fd);
/* Kernel'a gönder */
int submitted = io_uring_submit(&ring);
printf("Gönderilen SQE sayısı: %d\n", submitted);
/* CQE bekle */
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
fprintf(stderr, "wait_cqe: %s\n", strerror(-ret));
return -1;
}
if (cqe->res < 0) {
fprintf(stderr, "Read hatası: %s\n", strerror(-cqe->res));
} else {
printf("Okunan bayt: %d\n", cqe->res);
}
/* CQE'yi tüket (ring'e iade et) */
io_uring_cqe_seen(&ring, cqe);
close(fd);
io_uring_queue_exit(&ring);
return cqe->res;
}
Async write ve scatter/gather
#include <sys/uio.h>
void async_writev_example(int fd, struct io_uring *ring)
{
static char header[] = "HTTP/1.1 200 OK\r\n";
static char body[] = "Hello, World!\r\n";
/* Scatter-gather: iki ayrı buffer tek write operasyonuyla */
static struct iovec iov[2] = {
{ .iov_base = header, .iov_len = sizeof(header) - 1 },
{ .iov_base = body, .iov_len = sizeof(body) - 1 },
};
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_writev(sqe, fd, iov, 2, 0 /* offset */);
io_uring_sqe_set_data(sqe, iov); /* CQE'de iov pointer'ı geri alırız */
}
/* READV de aynı şekilde */
void async_readv_example(int fd, struct io_uring *ring)
{
static char buf1[4096];
static char buf2[4096];
static struct iovec iov[2] = {
{ .iov_base = buf1, .iov_len = sizeof(buf1) },
{ .iov_base = buf2, .iov_len = sizeof(buf2) },
};
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_readv(sqe, fd, iov, 2, 0);
io_uring_sqe_set_data(sqe, iov);
}
FSYNC ve fdatasync
void async_fsync(int fd, struct io_uring *ring, bool datasync)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_fsync(sqe, fd,
datasync ? IORING_FSYNC_DATASYNC : 0);
io_uring_sqe_set_data(sqe, (void *)(uintptr_t)fd);
}
/* Toplu gönderim: write + fsync birlikte */
void write_then_fsync(int fd, const char *data, size_t len,
struct io_uring *ring)
{
/* Write SQE */
struct io_uring_sqe *write_sqe = io_uring_get_sqe(ring);
io_uring_prep_write(write_sqe, fd, data, len, 0);
io_uring_sqe_set_data(write_sqe, (void *)1);
/* Fsync SQE — write tamamlandıktan sonra çalışsın */
struct io_uring_sqe *fsync_sqe = io_uring_get_sqe(ring);
io_uring_prep_fsync(fsync_sqe, fd, IORING_FSYNC_DATASYNC);
io_uring_sqe_set_data(fsync_sqe, (void *)2);
/* IO_DRAIN: tüm önceki op'lar tamamlanmadan bu başlamaz */
fsync_sqe->flags |= IOSQE_IO_DRAIN;
io_uring_submit(ring);
}
SQE bayrakları
| Bayrak | Anlamı |
|---|---|
| IOSQE_FIXED_FILE | fd, registered file tablosundan index olarak yorumlanır |
| IOSQE_IO_DRAIN | Tüm önceki op'lar tamamlanmadan bu başlamaz |
| IOSQE_IO_LINK | Sonraki SQE, bu tamamlanmadan başlamaz (zincir) |
| IOSQE_IO_HARDLINK | IO_LINK gibi ama hata durumunda da devam eder |
| IOSQE_ASYNC | Her zaman async olarak çalıştır (kernel thread'de) |
| IOSQE_BUFFER_SELECT | Kernel seçilen buffer'ı kullan (buffer group) |
Bu bölümde
- io_uring_prep_read/write: fd + buf + len + offset ile SQE hazırlar
- io_uring_prep_readv/writev: scatter-gather I/O; iovec dizisiyle birden fazla buffer
- IOSQE_IO_LINK: SQE zinciri oluşturur; write → fsync sırası garanti edilir
- IORING_FSYNC_DATASYNC: fdatasync gibi; metadata değil yalnızca veri senkronize
04 Completion: CQE alma ve işleme
Completion Queue, tamamlanan operasyonların sonuçlarını tutar. Bekleme, polling ve batch işleme seçenekleri performansı etkiler.
Bekleme modları
#include <liburing.h>
/* 1. Blocking wait: en az 1 CQE gelene kadar bekle */
void wait_and_process(struct io_uring *ring)
{
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(ring, &cqe);
if (ret < 0) { perror("wait_cqe"); return; }
void *data = io_uring_cqe_get_data(cqe);
printf("CQE: res=%d, data=%p\n", cqe->res, data);
io_uring_cqe_seen(ring, cqe);
}
/* 2. Polling: beklemeden kontrol et */
void poll_completions(struct io_uring *ring)
{
struct io_uring_cqe *cqe;
/* Beklemeden bir CQE al (yoksa 0 döner) */
while (io_uring_peek_cqe(ring, &cqe) == 0) {
if (cqe->res < 0)
fprintf(stderr, "I/O hatası: %s\n", strerror(-cqe->res));
else
printf("Tamamlandı: %d bayt\n", cqe->res);
io_uring_cqe_seen(ring, cqe);
}
}
/* 3. Wait for N completions: N CQE gelene kadar bekle */
void wait_for_n(struct io_uring *ring, uint32_t n)
{
struct io_uring_cqe *cqes[64];
int count = io_uring_wait_cqes(ring, cqes, n, NULL, NULL);
for (int i = 0; i < count; i++) {
process_cqe(cqes[i]);
io_uring_cqe_seen(ring, cqes[i]);
}
}
/* 4. Timeout ile bekleme */
void wait_with_timeout(struct io_uring *ring, uint64_t timeout_ns)
{
struct __kernel_timespec ts = {
.tv_sec = timeout_ns / 1000000000,
.tv_nsec = timeout_ns % 1000000000,
};
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe_timeout(ring, &cqe, &ts);
if (ret == -ETIME) {
printf("Timeout — CQE gelmedi\n");
} else if (ret == 0) {
process_cqe(cqe);
io_uring_cqe_seen(ring, cqe);
}
}
Batch completion işleme
void process_all_completions(struct io_uring *ring)
{
struct io_uring_cqe *cqe;
uint32_t head;
uint32_t count = 0;
/* Tüm hazır CQE'leri tek döngüde işle */
io_uring_for_each_cqe(ring, head, cqe) {
void *user_data = io_uring_cqe_get_data(cqe);
int res = cqe->res;
if (res < 0) {
fprintf(stderr, "Op hata: %s\n", strerror(-res));
} else {
/* user_data'ya göre işlemi tanımla */
struct my_request *req = (struct my_request *)user_data;
req->bytes_done = res;
req->completed = true;
}
count++;
}
/* Tüm işlenenleri tek seferde tüket */
io_uring_cq_advance(ring, count);
}
Hata yönetimi
void handle_cqe_result(struct io_uring_cqe *cqe,
const char *op_name)
{
if (cqe->res < 0) {
int err = -cqe->res;
switch (err) {
case ENOENT:
fprintf(stderr, "%s: Dosya bulunamadı\n", op_name);
break;
case EACCES:
fprintf(stderr, "%s: Erişim reddedildi\n", op_name);
break;
case EAGAIN:
/* Non-blocking fd için tekrar dene */
fprintf(stderr, "%s: Tekrar dene\n", op_name);
break;
case ECANCELED:
fprintf(stderr, "%s: İptal edildi\n", op_name);
break;
default:
fprintf(stderr, "%s hatası: %s\n", op_name, strerror(err));
}
} else {
printf("%s tamamlandı: %d bayt\n", op_name, cqe->res);
}
}
Bu bölümde
- io_uring_wait_cqe: blocking; io_uring_peek_cqe: non-blocking; io_uring_wait_cqe_timeout: zaman sınırlı
- io_uring_for_each_cqe + io_uring_cq_advance: tüm hazır CQE'leri batch olarak işle
- io_uring_cqe_seen(): işlenen CQE'yi ring'e iade et; unutulursa CQ dolar
- cqe->res < 0: hata; -cqe->res errno değeri; 0 veya pozitif: başarı
05 Gelişmiş özellikler: registered files ve buffers
Registered files ve buffers, her operasyonda kernel'ın fd tablosunu ve buffer mapping'i sorgulamasını engeller. Yüksek throughput uygulamalarında önemli performans artışı sağlar.
Registered files (io_uring_register_files)
Normalde her SQE'de kernel, fd'yi dosya tablosunda arar. Registered files ile fd'ler bir kez kaydedilir; SQE'de index kullanılır.
#include <liburing.h>
#define MAX_FILES 64
int fds[MAX_FILES];
int nfds = 0;
void register_files_example(struct io_uring *ring)
{
/* Dosyaları aç */
fds[0] = open("/data/file1.bin", O_RDONLY);
fds[1] = open("/data/file2.bin", O_RDONLY);
fds[2] = open("/data/output.bin", O_WRONLY | O_CREAT | O_TRUNC, 0644);
nfds = 3;
/* Ring'e kaydet */
int ret = io_uring_register_files(ring, fds, nfds);
if (ret < 0) {
fprintf(stderr, "register_files: %s\n", strerror(-ret));
return;
}
printf("%d dosya registered\n", nfds);
/* Artık SQE'de fd yerine index kullan */
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read(sqe,
0, /* fd yerine index (fds[0] = file1.bin) */
buf, sizeof(buf), 0);
/* FIXED_FILE bayrağı: index modunu etkinleştirir */
sqe->flags |= IOSQE_FIXED_FILE;
io_uring_submit(ring);
}
void update_registered_file(struct io_uring *ring, int index, int new_fd)
{
/* Tek bir fd'yi güncelle (tüm tabloyu yeniden kaydetmeden) */
io_uring_register_files_update(ring, index, &new_fd, 1);
}
void unregister(struct io_uring *ring)
{
io_uring_unregister_files(ring);
for (int i = 0; i < nfds; i++) close(fds[i]);
}
Registered buffers (io_uring_register_buffers)
Registered buffers ile kullanıcı belleği kernel'a bir kez pin'lenir ve map'lenir. Her read/write'ta buffer'ın DMA-ready hale getirilmesi gerekmez.
#define NBUFFERS 8
#define BUF_SIZE 65536 /* 64 KB */
static char reg_bufs[NBUFFERS][BUF_SIZE];
static struct iovec iovecs[NBUFFERS];
void setup_registered_buffers(struct io_uring *ring)
{
for (int i = 0; i < NBUFFERS; i++) {
iovecs[i].iov_base = reg_bufs[i];
iovecs[i].iov_len = BUF_SIZE;
}
int ret = io_uring_register_buffers(ring, iovecs, NBUFFERS);
if (ret < 0) {
fprintf(stderr, "register_buffers: %s\n", strerror(-ret));
return;
}
printf("%d buffer registered\n", NBUFFERS);
}
void read_with_fixed_buffer(struct io_uring *ring, int fd, int buf_idx)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
/* Fixed buffer ile read: buf_idx = registered buffer index */
io_uring_prep_read_fixed(sqe, fd,
reg_bufs[buf_idx], BUF_SIZE,
0, /* offset */
buf_idx /* buffer index */);
io_uring_sqe_set_data(sqe, (void *)(uintptr_t)buf_idx);
}
/* IORING_FEAT_FAST_POLL desteği kontrolü */
void check_fast_poll(struct io_uring *ring)
{
struct io_uring_params params;
if (ring->features & IORING_FEAT_FAST_POLL)
printf("FAST_POLL destekleniyor: ağ I/O latency azalır\n");
else
printf("FAST_POLL yok: eski kernel\n");
}
Bu bölümde
- io_uring_register_files: fd'leri ring'e bir kez kaydet; SQE'de IOSQE_FIXED_FILE + index kullan
- io_uring_register_buffers: bellek iovec'leri pin'le; io_uring_prep_read_fixed ile kullan
- Fayda: yüksek QPS'de her op'ta kernel fd/buffer arama overhead'ini ortadan kaldırır
- IORING_FEAT_FAST_POLL: ağ socket'leri için epoll'dan daha düşük latency; 5.7+ kernelde
06 Network I/O: ACCEPT, SEND, RECV
io_uring'in network desteği, epoll + read/write döngüsünün yerini alabilir. ACCEPT, SEND, RECV ve multi-shot ACCEPT ile tam async TCP server yazılabilir.
Temel TCP server: async ACCEPT + RECV
#include <liburing.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdio.h>
enum req_type { REQ_ACCEPT, REQ_RECV, REQ_SEND };
struct request {
enum req_type type;
int fd;
char buf[4096];
struct sockaddr_in client_addr;
socklen_t addr_len;
};
int server_fd;
void add_accept(struct io_uring *ring, struct request *req)
{
req->type = REQ_ACCEPT;
req->addr_len = sizeof(req->client_addr);
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_accept(sqe, server_fd,
(struct sockaddr *)&req->client_addr,
&req->addr_len, 0);
io_uring_sqe_set_data(sqe, req);
}
void add_recv(struct io_uring *ring, struct request *req)
{
req->type = REQ_RECV;
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_recv(sqe, req->fd, req->buf, sizeof(req->buf), 0);
io_uring_sqe_set_data(sqe, req);
}
void add_send(struct io_uring *ring, struct request *req, int len)
{
req->type = REQ_SEND;
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_send(sqe, req->fd, req->buf, len, 0);
io_uring_sqe_set_data(sqe, req);
}
void run_event_loop(struct io_uring *ring)
{
struct io_uring_cqe *cqe;
while (1) {
io_uring_wait_cqe(ring, &cqe);
struct request *req = io_uring_cqe_get_data(cqe);
int res = cqe->res;
io_uring_cqe_seen(ring, cqe);
if (res < 0) {
fprintf(stderr, "Hata (type=%d): %s\n", req->type, strerror(-res));
free(req);
continue;
}
switch (req->type) {
case REQ_ACCEPT:
req->fd = res; /* Yeni bağlantı fd'si */
add_recv(ring, req); /* Veri bekle */
/* Yeni bir ACCEPT SQE ekle (bir sonraki bağlantı için) */
struct request *new_req = calloc(1, sizeof(*new_req));
add_accept(ring, new_req);
io_uring_submit(ring);
break;
case REQ_RECV:
if (res == 0) { /* Bağlantı kapandı */
close(req->fd);
free(req);
} else {
add_send(ring, req, res); /* Echo back */
io_uring_submit(ring);
}
break;
case REQ_SEND:
add_recv(ring, req); /* Bir sonraki veriyi bekle */
io_uring_submit(ring);
break;
}
}
}
Multi-shot ACCEPT (kernel 5.19+)
Normal ACCEPT her bağlantı için yeni bir SQE gerektirir. Multi-shot ACCEPT, tek SQE ile sürekli yeni bağlantıları kabul eder; her accept için yeni CQE üretilir.
void add_multishot_accept(struct io_uring *ring, int server_fd)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
/* IORING_ACCEPT_MULTISHOT: tek SQE, çok CQE */
io_uring_prep_multishot_accept(sqe, server_fd, NULL, NULL, 0);
io_uring_sqe_set_data(sqe, (void *)ACCEPT_TOKEN);
io_uring_submit(ring);
}
/* CQE işleme: multi-shot CQE'ler IORING_CQE_F_MORE ile işaretlenir */
void handle_multishot_cqe(struct io_uring_cqe *cqe)
{
int client_fd = cqe->res; /* Yeni bağlantı fd'si */
if (cqe->flags & IORING_CQE_F_MORE) {
/* SQE hâlâ aktif, yeni bağlantılar için CQE üretmeye devam eder */
printf("Yeni bağlantı: %d (multi-shot devam ediyor)\n", client_fd);
} else {
/* SQE tükendi veya hata — yeniden ekle */
printf("Multi-shot bitti, yeniden ekleniyor\n");
}
}
CONNECT ve SEND_ZC (sıfır kopya send)
/* Async TCP connect */
void async_connect(struct io_uring *ring, int sockfd,
struct sockaddr *addr, socklen_t addrlen)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_connect(sqe, sockfd, addr, addrlen);
io_uring_sqe_set_data(sqe, (void *)(uintptr_t)sockfd);
}
/* Zero-copy send (kernel 6.0+): buffer kernel'da kopyalanmaz */
void async_send_zc(struct io_uring *ring, int fd,
const void *buf, size_t len)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_send_zc(sqe, fd, buf, len, 0, 0);
/* Not: buf, send tamamlanana kadar geçerli kalmalı */
/* CQE: IORING_CQE_F_MORE ile bildirim gelir */
}
Bu bölümde
- io_uring_prep_accept/recv/send: tam async TCP; epoll + blocking I/O döngüsünü değiştirir
- Olay döngüsü: accept → recv → send → recv zinciri; her adım ayrı SQE/CQE
- Multi-shot accept (5.19+): tek SQE ile sürekli yeni bağlantı; IORING_CQE_F_MORE ile devam
- send_zc (6.0+): sıfır kopya send; büyük veri transferlerinde bellek bant genişliği tasarrufu
07 io_uring vs epoll karşılaştırması
io_uring ve epoll farklı kullanım senaryolarında farklı güçlere sahiptir. Her birinin ne zaman tercih edileceğini anlamak kritik mimari kararı doğru vermeyi sağlar.
Mimari fark
epoll bir hazır bildirim mekanizmasıdır: fd okumaya/yazmaya hazır olduğunda bildirim gönderir; asıl I/O hâlâ ayrı bir read()/write() sistem çağrısıyla yapılır. io_uring ise operasyon gönderme mekanizmasıdır: I/O operasyonunun kendisi kernel'a gönderilir ve tamamlanınca CQE döner; ayrı sistem çağrısına gerek yoktur.
| Özellik | epoll | io_uring |
|---|---|---|
| Dosya I/O | Hayır (pollable fd'ler) | Evet (her türlü fd) |
| Network I/O | Evet | Evet (5.5+) |
| Syscall/op sayısı | 2 (epoll_wait + read) | 0-1 (SQE+CQE paylaşımlı bellek) |
| Kernel desteği | 2.5.44'ten itibaren | 5.1+ |
| Taşınabilirlik | Linux only, geniş destek | Linux only, yeni kernel gerekir |
| Kod karmaşıklığı | Düşük-orta | Orta-yüksek (liburing ile azalır) |
| Düşük gecikme | İyi | Çok iyi (FAST_POLL ile) |
| Yüksek throughput | İyi | Çok iyi (SQPOLL ile) |
Benchmark yaklaşımı
# fio ile io_uring dosya I/O benchmark
fio \
--name=uring-rand-read \
--ioengine=io_uring \
--rw=randread \
--bs=4k \
--numjobs=1 \
--iodepth=64 \
--runtime=30 \
--filename=/dev/nvme0n1
# fio ile libaio karşılaştırması
fio \
--name=aio-rand-read \
--ioengine=libaio \
--rw=randread \
--bs=4k \
--numjobs=1 \
--iodepth=64 \
--runtime=30 \
--filename=/dev/nvme0n1
# wrk ile HTTP server benchmark (io_uring tabanlı server)
wrk -t4 -c400 -d30s http://localhost:8080/
Ne zaman io_uring, ne zaman epoll
Latency profili
#include <time.h>
static inline uint64_t now_ns(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
}
void measure_latency(struct io_uring *ring, int fd,
char *buf, size_t len)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read(sqe, fd, buf, len, 0);
uint64_t t0 = now_ns();
io_uring_submit(ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(ring, &cqe);
uint64_t t1 = now_ns();
printf("Latency: %.2f µs\n", (t1 - t0) / 1000.0);
io_uring_cqe_seen(ring, cqe);
}
Bu bölümde
- epoll: hazır bildirim (I/O hâlâ ayrı syscall); io_uring: operasyon gönderme (tek syscall veya sıfır)
- io_uring üstünlüğü: dosya I/O, yüksek IOPS, batch işleme, NVMe direkt erişim
- epoll üstünlüğü: eski kernel desteği, basitlik, pollable fd ekosistemi (timerfd, signalfd)
- SQPOLL idle CPU kullanır; pil ömrü kritik embedded cihazlarda dikkatli kullanın
08 Pratik: async dosya okuma ve echo server
100 dosyayı paralel olarak io_uring ile okuyan uygulama, liburing ile tam echo server ve embedded ARM64 sistemlerde kurulum.
100 dosyayı paralel okuma
#include <liburing.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#define NFILES 100
#define BUF_SIZE 65536 /* 64 KB */
#define QUEUE_DEPTH 128
struct file_request {
int fd;
int index;
char *buf;
size_t bytes_read;
bool done;
};
static uint64_t now_ns(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
}
int main(int argc, char *argv[])
{
struct io_uring ring;
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
struct file_request reqs[NFILES];
memset(reqs, 0, sizeof(reqs));
/* Dosyaları aç ve buffer'ları tahsis et */
for (int i = 0; i < NFILES; i++) {
char path[256];
snprintf(path, sizeof(path), "/data/file_%03d.bin", i);
reqs[i].fd = open(path, O_RDONLY);
reqs[i].index = i;
reqs[i].buf = malloc(BUF_SIZE);
if (reqs[i].fd < 0) {
/* Dosya yoksa /dev/urandom kullan */
reqs[i].fd = open("/dev/urandom", O_RDONLY);
}
}
uint64_t t0 = now_ns();
/* Tüm dosyalar için SQE gönder */
for (int i = 0; i < NFILES; i++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) {
/* Kuyruk dolu — ara sıra flush et */
io_uring_submit(&ring);
sqe = io_uring_get_sqe(&ring);
}
io_uring_prep_read(sqe, reqs[i].fd, reqs[i].buf, BUF_SIZE, 0);
io_uring_sqe_set_data(sqe, &reqs[i]);
}
io_uring_submit(&ring);
/* Tüm CQE'leri bekle */
int completed = 0;
while (completed < NFILES) {
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
struct file_request *req = io_uring_cqe_get_data(cqe);
if (cqe->res > 0) {
req->bytes_read = cqe->res;
req->done = true;
} else if (cqe->res < 0) {
fprintf(stderr, "File %d error: %s\n",
req->index, strerror(-cqe->res));
}
io_uring_cqe_seen(&ring, cqe);
completed++;
}
uint64_t elapsed = now_ns() - t0;
size_t total = 0;
for (int i = 0; i < NFILES; i++) total += reqs[i].bytes_read;
printf("Tamamlandı: %d dosya, toplam %zu bayt\n", NFILES, total);
printf("Süre: %.2f ms (%.0f MB/s)\n",
elapsed / 1e6,
(double)total / (elapsed / 1e9) / (1024*1024));
/* Temizlik */
for (int i = 0; i < NFILES; i++) {
close(reqs[i].fd);
free(reqs[i].buf);
}
io_uring_queue_exit(&ring);
return 0;
}
# Derleme
gcc -O2 -o parallel_read parallel_read.c -luring
# Test dosyaları oluştur
mkdir -p /data
for i in $(seq -w 0 99); do
dd if=/dev/urandom of="/data/file_${i}.bin" bs=64K count=1 2>/dev/null
done
# Çalıştır
./parallel_read
# Tamamlandı: 100 dosya, toplam 6553600 bayt
# Süre: 12.34 ms (506 MB/s)
Embedded ARM64: kernel gereksinimleri ve liburing derleme
# Kernel versiyonu kontrolü
uname -r
# 5.15.87-v8 — ARM64, io_uring destekli
# io_uring kernel config gereksinimleri
# CONFIG_IO_URING=y
# CONFIG_FUTEX=y (wait_cqe için)
# CONFIG_EPOLL=y (FAST_POLL için)
# Buildroot ile liburing ekle
make menuconfig
# Target packages → Libraries → Other → liburing
# Çapraz derleme (aarch64)
aarch64-linux-gnu-gcc \
-O2 \
--sysroot=/path/to/aarch64-sysroot \
-o parallel_read parallel_read.c \
-luring
# Kernel config kontrolü
zcat /proc/config.gz | grep CONFIG_IO_URING
# CONFIG_IO_URING=y
# CONFIG_IO_URING_KASAN=n
# Çalışma zamanı özellik kontrolü
cat /proc/sys/kernel/io_uring_disabled
# 0 (0=etkin, 1=ayrıcalıklı, 2=devre dışı)
Güvenlik: io_uring kısıtlamaları
# io_uring bazı güvenlik politikalarında kısıtlanabilir
# Konteyner güvenliği: yalnızca root erişimi
echo 1 | sudo tee /proc/sys/kernel/io_uring_disabled
# 1 = yalnızca CAP_SYS_ADMIN yetkisiyle kullanılabilir
# Tamamen devre dışı bırak
echo 2 | sudo tee /proc/sys/kernel/io_uring_disabled
# Gömülü üretim sistemi: CAP_SYS_ADMIN gereksinimi kaldırılabilir
echo 0 | sudo tee /proc/sys/kernel/io_uring_disabled
# seccomp filtreleri io_uring'i etkiler
# io_uring_setup/__NR_io_uring_setup izin verilmeli
Bu bölümde
- 100 paralel okuma: tüm SQE'leri önce gönder, sonra tüm CQE'leri bekle; sıralı okumadan çok daha hızlı
- Kuyruk dolu: io_uring_get_sqe() NULL döner; ara sıra io_uring_submit() ile flush et
- ARM64 Buildroot: menuconfig'te liburing'i etkinleştir; CONFIG_IO_URING=y kernel gereksinin
- /proc/sys/kernel/io_uring_disabled: 0=herkes, 1=CAP_SYS_ADMIN, 2=tamamen kapalı