Tüm eğitimler
TEKNİK REHBER GÖMÜLÜ LİNUX IO_URING 2026

io_uring
Linux Async I/O

Kernel-user paylaşımlı ring buffer ile sıfır syscall overhead'li asenkron I/O — dosya, ağ ve depolama operasyonlarını paralel çalıştır.

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.

MekanizmaDosyaSyscall overheadSınırı
Blocking I/OEvetEvetHer op'taThread başına bir op
epollHayırEvetHer event'teDosya I/O yok
Linux AIOO_DIRECTHayırio_submit başınaTampon I/O yok, ağ yok
io_uringEvetEvetToplu veya sıfırNeredeyse 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

Toplu submitBirden fazla SQE hazırla, tek io_uring_submit() ile gönder — N op için 1 syscall
SQPOLL moduKernel thread SQ'yu sürekli polling yapar; submission için hiç syscall gerekmez
IORING_FEAT_FAST_POLLAğ I/O için hızlı kernel-side polling; epoll'dan daha düşük latency
Fixed files/buffersfd ve buffer'lar bir kez register edilir; her op'ta kernel'a kopyalanmaz

Kernel versiyonu gereksinimleri

ÖzellikMinimum Kernel
Temel io_uring (dosya I/O)5.1
IORING_OP_ACCEPT, SEND, RECV5.5
IORING_OP_OPENAT, STATX5.6
SQPOLL modu iyileştirmeleri5.11
Multi-shot accept5.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.

C — io_uring_setup (düşük seviyeli, liburing olmadan)
#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.

C — SQE alanları (struct io_uring_sqe)
/* 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ı

C — CQE yapısı (struct io_uring_cqe)
/* 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

bash — liburing kurulumu
# 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

C — io_uring_queue_init ve cleanup
#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ı

FonksiyonAçı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
C — kernel özellik desteği kontrolü
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

C — IORING_OP_READ ile async dosya okuma
#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

C — READV/WRITEV ile scatter-gather I/O
#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

C — async fsync
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ı

BayrakAnlamı
IOSQE_FIXED_FILEfd, registered file tablosundan index olarak yorumlanır
IOSQE_IO_DRAINTüm önceki op'lar tamamlanmadan bu başlamaz
IOSQE_IO_LINKSonraki SQE, bu tamamlanmadan başlamaz (zincir)
IOSQE_IO_HARDLINKIO_LINK gibi ama hata durumunda da devam eder
IOSQE_ASYNCHer zaman async olarak çalıştır (kernel thread'de)
IOSQE_BUFFER_SELECTKernel 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ı

C — CQE alma yöntemleri
#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

C — io_uring_for_each_cqe ile batch 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

C — CQE hata işleme
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.

C — registered files kullanımı
#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.

C — registered buffers kullanımı
#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

C — io_uring tabanlı TCP echo server (iskelet)
#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.

C — multi-shot accept
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)

C — async connect ve send_zc
/* 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.

Özellikepollio_uring
Dosya I/OHayır (pollable fd'ler)Evet (her türlü fd)
Network I/OEvetEvet (5.5+)
Syscall/op sayısı2 (epoll_wait + read)0-1 (SQE+CQE paylaşımlı bellek)
Kernel desteği2.5.44'ten itibaren5.1+
TaşınabilirlikLinux only, geniş destekLinux only, yeni kernel gerekir
Kod karmaşıklığıDüşük-ortaOrta-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ı

bash — basit benchmark karşılaştırması
# 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

io_uring tercih etDosya + ağ I/O karışık; yüksek IOPS gereksinimi; NVMe, SSD; batch işleme; kernel 5.5+ garantili
epoll tercih etYalnızca ağ I/O; eski kernel desteği gerekli; basit event loop; inotify/timerfd gibi pollable fd'ler
Embedded içinKernel 5.1+ garantili ise io_uring; aksi takdirde epoll + blocking file I/O yeterlidir
SQPOLL dikkatSQPOLL idle dönemde dahi CPU kullanır; bataryalı embedded cihazlarda dikkatli kullanın

Latency profili

C — io_uring latency ölçümü
#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

C — parallel_read.c: 100 dosya eş zamanlı
#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;
}
bash — derleme ve çalıştırma
# 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

bash — ARM64 Buildroot entegrasyonu
# 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ı

bash — güvenlik konfigürasyonu
# 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ı