Bloklamayan (Non-Blocking) I/O ve fcntl ile Arabellek ve Zaman Aşımı Yönetimi

Varsayılan yapılandırmada bir ağ soketi üzerinde read() veya recv() çağrıldığında, işletim sistemi arabelleklerine karşı taraftan en az bir bayt veri gelene kadar çağıran iş parçacığı (thread) askıya alınır (sleep/block durumuna geçer). Benzer şekilde write() çağrıldığında, çekirdeğin giden veri tamponu (send buffer) doluysa boş yer açılana kadar işlem kilitlenir.

Tek veya birkaç istemciyle konuşurken bu davranış anlaşılırdır; ancak yüksek hacimli ağ uygulamalarında tek bir yavaş istemcinin tüm sistemi kilitlemesini engellemek için soketleri bloklamayan (non-blocking) kipte çalıştırmak şarttır.

Bu rehberde; fcntl ile soket bayraklarının kontrolünü, EAGAIN / EWOULDBLOCK hata yönetimini ve soket çekirdek arabelleklerinin boyutlandırılmasını inceliyoruz.


1. Soketi Non-Blocking Moda Almak: fcntl

Linux altında bir dosya tanımlayıcısının (soketin) çalışma modunu değiştirmek için fcntl (file control) sistem çağrısı kullanılır:

#include <fcntl.h>
#include <unistd.h>
#include <iostream>

bool set_socket_non_blocking(int fd) {
    // 1. Mevcut bayrakları oku
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL hatasi");
        return false;
    }

    // 2. O_NONBLOCK bayrağını ekleyerek güncelle
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL hatasi");
        return false;
    }

    return true;
}

Modern Linux çekirdeklerinde (2.6.27+) soket oluşturulurken veya accept4 çağrılırken doğrudan bayrak verilerek fazladan bir sistem çağrısından kaçınmak da mümkündür:

// Doğrudan non-blocking soket açmak
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

2. Non-Blocking Okuma: EAGAIN ve EWOULDBLOCK Yönetimi

Soket non-blocking moda alındığında recv() çağrısının davranışı kökten değişir:

  • Eğer arabellekte veri varsa, okunan bayt sayısı döner (> 0).
  • Karşı taraf bağlantıyı düzgünce kapattıysa (EOF), sıfır döner (== 0).
  • Eğer henüz hiçbir veri gelmemişse, fonksiyon beklemez; anında -1 döner ve errno değişkenini EAGAIN veya EWOULDBLOCK olarak ayarlar.
#include <sys/socket.h>
#include <cerrno>

enum class ReadStatus {
    SUCCESS,
    WOULD_BLOCK,
    CLOSED,
    ERROR
};

ReadStatus safe_nonblocking_read(int fd, char* buffer, size_t max_len, ssize_t& out_bytes) {
    out_bytes = recv(fd, buffer, max_len, 0);

    if (out_bytes > 0) {
        return ReadStatus::SUCCESS;
    }

    if (out_bytes == 0) {
        // İstemci bağlantıyı sonlandırdı (FIN paketi)
        return ReadStatus::CLOSED;
    }

    // out_bytes == -1 durumu
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // Hata değildir; veri henüz yolda
        return ReadStatus::WOULD_BLOCK;
    }

    // Gerçek bir ağ veya soket hatası
    return ReadStatus::ERROR;
}

3. Soket Zaman Aşımları (Timeouts): SORCVTIMEO ve SOSNDTIMEO

Tüm mimariyi asenkron bir olay döngüsüne geçirmeden önce, basit bloklayan soketlerin sonsuza kadar beklemesini önlemek için çekirdek seviyesinde zaman aşımı (timeout) tanımlanabilir:

#include <sys/time.h>

void set_socket_timeouts(int fd, int seconds) {
    struct timeval tv{};
    tv.tv_sec = seconds;
    tv.tv_usec = 0;

    // Okuma zaman aşımı
    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
    // Yazma zaman aşımı
    setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
}

Belirlenen süre içinde veri gelmezse recv() -1 döner ve errno EAGAIN olur.


4. Çekirdek Arabellek Boyutlandırması: SORCVBUF ve SOSNDBUF

Yüksek bant genişliği tüketen sistemlerde çekirdeğin soket için ayırdığı tampon boyutunu ayarlamak paket düşmelerini (packet drop) engeller:

void tune_socket_buffers(int fd, int rcv_size, int snd_size) {
    // Alım tamponu (Receive Buffer)
    setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcv_size, sizeof(rcv_size));
    // Gönderim tamponu (Send Buffer)
    setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &snd_size, sizeof(snd_size));
}

Önemli Not: Linux çekirdeği, bellek yönetimini optimize etmek amacıyla setsockopt ile verdiğiniz değerin yaklaşık iki katını tahsis eder (net.core.rmem_max sınırına tabi olarak).


5. Çıkarımlar

Soketleri non-blocking yapmak sistemin donmasını engeller ancak yeni bir problem doğurur: Veri gelip gelmediğini sürekli bir while(true) döngüsüyle sormak (busy-polling) CPU’yu %100 yük altına sokar.

İşlemciyi boş yere yormadan, sadece soket üzerinde gerçek bir veri hazır olduğunda çekirdeğin bizi uyarmasını sağlamak için serinin üçüncü bölümünde Linux’un en güçlü I/O mekanizması olan epoll mimarisini ele alacağız.