0Pricing
C Academy · Lektion

Blockierendes und nicht blockierendes I/O

Warum Event Loops wichtig sind.

Blockierendes und nicht blockierendes I/O ist eine kostenlose C Academy-Lektion auf CoddyKit. Dies ist Lektion 1 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des C Academy-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der C Academy-Kurs umfasst insgesamt 4 Lektionen.

Was Blockieren bedeutet

Ein blockierender Systemaufruf hält den aufrufenden Thread an, bis der Vorgang fortgesetzt werden kann. Wenn Sie recv() auf einem Socket ohne Daten aufrufen, parkt der Kernel Ihren Thread, bis Bytes eintreffen.

Das ist einfach nachzuvollziehen: eine Verbindung, ein Thread, geradliniger Code. Die Kosten zeigen sich, sobald Sie gleichzeitig Tausende Clients bedienen müssen.

ssize_t n = recv(fd, buf, sizeof buf, 0);
/* thread sleeps here until data or error */
if (n > 0) handle(buf, n);

Das Skalierungsproblem

Bei blockierender I/O blockiert ein festhängender Client den gesamten Thread. Die klassische Lösung ist ein Thread (oder Prozess) pro Verbindung.

Das funktioniert bis zu einem gewissen Punkt. 10.000 Threads bedeuten jedoch 10.000 Stacks, häufige Kontextwechsel und zusätzlichen Scheduler-Aufwand. Dies ist das bekannte C10k-Problem, das Server zu ereignisgesteuerten Designs drängte.

Nicht blockierender Modus

Ein nicht blockierender Socket schläft nie. Wenn ein Aufruf nicht sofort abgeschlossen werden kann, gibt er umgehend -1 zurück und setzt errno auf EAGAIN oder EWOULDBLOCK.

Ihr Code muss nun später einen neuen Versuch unternehmen. So kann ein einzelner Thread viele Sockets verwalten, ohne bei einem davon festzuhängen.

ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    /* no data right now, try again later */
}

O_NONBLOCK mit fcntl setzen

Sie schalten einen Deskriptor auf nicht blockierend um, indem Sie das Flag O_NONBLOCK mit fcntl() hinzufügen. Lesen Sie immer zuerst die aktuellen Flags und führen Sie dann eine bitweise ODER-Verknüpfung mit dem Bit durch, damit Sie keine anderen Einstellungen überschreiben.

Dieselbe Hilfsfunktion wird gleichermaßen für Listening-Sockets, angenommene Client-Sockets und Pipes verwendet.

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);
}

Partielle Lesevorgänge behandeln

Nicht blockierende I/O macht partielle Vorgänge zum Normalfall. Ein recv() kann weniger Bytes als angefordert zurückgeben, und send() kann nur einen Teil Ihres Puffers akzeptieren.

Sie müssen verfolgen, wie viel Sie gesendet oder empfangen haben, und dort fortfahren. Gehen Sie nie davon aus, dass ein Aufruf alle Bytes überträgt.

size_t sent = 0;
while (sent < len) {
    ssize_t w = send(fd, buf + sent, len - sent, 0);
    if (w < 0) { if (errno == EAGAIN) break; else return -1; }
    sent += w;
}

Busy-Waiting ist falsch

Die naive Verwendung nicht blockierender Sockets besteht darin, alle Sockets in einer Schleife ständig erneut zu versuchen. Dieses Busy-Waiting verbraucht 100 % CPU, selbst wenn nichts passiert.

Eigentlich möchten wir den Kernel fragen: "Sagen Sie mir, welche Deskriptoren bereit sind, und lassen Sie mich bis dahin schlafen." Genau das ermöglicht die Benachrichtigung über Bereitschaft.

Benachrichtigung über Bereitschaft

I/O-Multiplexing ermöglicht es einem Thread, gleichzeitig auf viele Deskriptoren zu warten und nur aufzuwachen, wenn mindestens einer bereit ist. Der Kernel übernimmt die Überwachung für Sie.

Die klassischen Schnittstellen sind select() und poll(). Sie funktionieren, durchsuchen aber bei jedem Aufruf jeden Deskriptor erneut, was bei großer Anzahl teuer wird.

fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);

Warum select und poll nicht skalieren

Sowohl select() als auch poll() sind O(n): Bei jedem Aufruf wird die vollständige Deskriptormenge an den Kernel übergeben, der alle Deskriptoren durchsucht. Anschließend durchsuchen Sie erneut alle, um die bereiten zu finden.

select() ist außerdem auf ungefähr FD_SETSIZE begrenzt (oft 1024). Bei Tausenden von Verbindungen dominiert dieser Overhead.

epoll kommt ins Spiel

epoll ist die skalierbare Antwort von Linux. Sie registrieren einmalig Ihr Interesse an einem Deskriptor, und der Kernel verwaltet intern eine Datenstruktur, die die Bereitschaft verfolgt.

Jeder Wartevorgang liefert nur die tatsächlich bereiten Deskriptoren zurück. Die Kosten skalieren daher mit den aktiven Verbindungen, nicht mit der Gesamtzahl der Verbindungen. Damit liegen sie pro bereitem Ereignis ungefähr bei O(1).

int epfd = epoll_create1(0);
/* register fds once, then wait for ready events */

Nicht blockierend plus epoll

epoll und nicht blockierende Sockets ergänzen sich. epoll teilt Ihnen mit, dass ein Deskriptor bereit ist; nicht blockierende Aufrufe ermöglichen es, ihn vollständig zu leeren, ohne jemals zu warten.

Sie sollten bei Sockets, die Sie an epoll übergeben, immer O_NONBLOCK setzen. Andernfalls könnte ein falsches Aufwachen oder ein partieller Lesevorgang Ihren einzelnen Event-Loop-Thread blockieren.

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

Das mentale Modell

Stellen Sie sich den Server als Schleife vor: In epoll_wait() blockieren, eine kleine Liste bereiter Deskriptoren zurückerhalten, an jedem nicht blockierende Arbeit ausführen und wiederholen.

Der Thread schläft, wenn nichts zu tun ist, und wacht nur bei echter Arbeit auf. Ein Thread kann nun effizient Zehntausende Verbindungen bedienen.

Schnelltest

Testen Sie Ihr Verständnis nicht blockierender Sockets.

Zusammenfassung

Blockierende I/O ist einfach, bindet jedoch pro Verbindung einen Thread und scheitert daher bei großer Skalierung. Nicht blockierende I/O gibt sofort EAGAIN zurück, statt zu schlafen.

Das Abfragen von Sockets in einer engen Schleife verschwendet CPU, daher verwenden wir eine Benachrichtigung über Bereitschaft. select/poll sind O(n); epoll skaliert auf viele Tausend Verbindungen. Als Nächstes richten wir epoll ein.

Häufig gestellte Fragen

Ist die Lektion „Blockierendes und nicht blockierendes I/O“ kostenlos?

Ja — der vollständige Text von „Blockierendes und nicht blockierendes I/O“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des C Academy-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der C Academy-Kurs umfasst insgesamt 4 Lektionen.

Was lerne ich in „Blockierendes und nicht blockierendes I/O“?

Warum Event Loops wichtig sind. Du übst C Academy mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.

Brauche ich Erfahrung, um C Academy zu starten?

Keine Vorkenntnisse erforderlich. C Academy auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 1 von 4.

Wie lange dauert die Lektion „Blockierendes und nicht blockierendes I/O“?

Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.

Kann ich in dieser C Academy-Lektion Code schreiben und ausführen?

Ja. Jede C Academy-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.

Alle Lektionen in diesem Kurs

  1. Blockierendes und nicht blockierendes I/O
  2. epoll einrichten
  3. Die Event Loop
  4. Ein einfacher Echo-Server
← Zurück zu C Academy