Blokujące a nieblokujące I/O
Dowiedzą się Państwo, dlaczego pętle zdarzeń mają znaczenie.
Blokujące a nieblokujące I/O to bezpłatna lekcja C Academy na CoddyKit. To lekcja 1 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej C Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs C Academy zawiera 4 lekcji w sumie.
Co oznacza blokowanie
Wywołanie systemowe blokujące zawiesza wątek wywołujący do chwili, gdy operacja będzie mogła się wykonać. Gdy wywołają Państwo recv() na gnieździe bez danych, jądro wstrzymuje wątek do momentu nadejścia bajtów.
Łatwo to zrozumieć: jedno połączenie, jeden wątek i kod wykonywany liniowo. Koszt pojawia się, gdy trzeba jednocześnie obsługiwać tysiące klientów.
ssize_t n = recv(fd, buf, sizeof buf, 0);
/* thread sleeps here until data or error */
if (n > 0) handle(buf, n);Problem skalowania
W przypadku blokującego wejścia/wyjścia jeden zablokowany klient blokuje cały wątek. Klasycznym rozwiązaniem jest jeden wątek (lub proces) na każde połączenie.
To działa do pewnego momentu, ale 10 000 wątków oznacza 10 000 stosów, kosztowne przełączanie kontekstu i narzut planisty. To słynny problem C10k, który skłonił twórców serwerów do przejścia na projekty sterowane zdarzeniami.
Tryb nieblokujący
Nieblokujące gniazdo nigdy nie zasypia. Jeśli wywołanie nie może zakończyć się natychmiast, od razu zwraca -1 i ustawia errno na EAGAIN lub EWOULDBLOCK.
Od tej chwili kod musi samodzielnie ponawiać próbę później. Dzięki temu jeden wątek może obsługiwać wiele gniazd bez zatrzymywania się na którymkolwiek z nich.
ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
/* no data right now, try again later */
}Ustawianie O_NONBLOCK za pomocą fcntl
Przełącza się deskryptor w tryb nieblokujący, dodając flagę O_NONBLOCK za pomocą fcntl(). Zawsze należy najpierw odczytać bieżące flagi, a następnie wykonać operację OR z odpowiednim bitem, aby nie nadpisać pozostałych ustawień.
Ten sam pomocnik jest używany zarówno dla gniazd nasłuchujących, zaakceptowanych gniazd klientów, jak i potoków.
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);
}Obsługa częściowych odczytów
W trybie nieblokującym częściowe operacje są czymś normalnym. recv() może zwrócić mniej bajtów, niż zażądano, a send() może zaakceptować tylko część bufora.
Trzeba śledzić, ile danych zostało wysłanych lub odebranych, i wznawiać operację od tego miejsca. Nigdy nie należy zakładać, że jedno wywołanie przeniesie wszystkie bajty.
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;
}Aktywne oczekiwanie jest błędem
Naiwny sposób korzystania z nieblokujących gniazd polega na ciągłym przechodzeniu po wszystkich gniazdach i ponawianiu prób. Takie aktywne oczekiwanie zużywa 100% procesora, nawet gdy nic się nie dzieje.
Tak naprawdę chcemy zapytać jądro: "powiedz mi, które deskryptory są gotowe, i pozwól mi spać do tego czasu". Właśnie to zapewniają mechanizmy powiadamiania o gotowości.
Powiadamianie o gotowości
Multipleksowanie wejścia/wyjścia pozwala jednemu wątkowi czekać jednocześnie na wiele deskryptorów i wybudzać się dopiero wtedy, gdy co najmniej jeden z nich będzie gotowy. Jądro wykonuje obserwowanie za Państwa.
Klasyczne interfejsy to select() i poll(). Działają, ale przy każdym wywołaniu ponownie sprawdzają każdy deskryptor, co przy dużej skali staje się kosztowne.
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);Dlaczego select i poll się nie skalują
Zarówno select(), jak i poll() mają złożoność O(n): każde wywołanie przekazuje jądru cały zbiór deskryptorów, jądro sprawdza je wszystkie, a następnie Państwa kod ponownie przegląda wszystkie, aby znaleźć gotowe.
select() ma także limit w okolicach FD_SETSIZE (często 1024). W przypadku tysięcy połączeń ten narzut dominuje.
Nadchodzi epoll
epoll jest skalowalną odpowiedzią systemu Linux. Zainteresowanie deskryptorem rejestruje się raz, a jądro utrzymuje wewnętrzną strukturę danych śledzącą gotowość.
Każde oczekiwanie zwraca tylko deskryptory, które rzeczywiście są gotowe, więc koszt zależy od liczby aktywnych połączeń, a nie od ich całkowitej liczby. Dzięki temu wynosi w przybliżeniu O(1) na gotowe zdarzenie.
int epfd = epoll_create1(0);
/* register fds once, then wait for ready events */Tryb nieblokujący wraz z epoll
epoll i nieblokujące gniazda tworzą zgrany zespół. epoll informuje, że deskryptor jest gotowy, a nieblokujące wywołania pozwalają opróżnić go bez zasypiania.
Gniazdom przekazywanym do epoll należy zawsze ustawić O_NONBLOCK. W przeciwnym razie fałszywe wybudzenie lub częściowy odczyt może zablokować jedyny wątek pętli zdarzeń.
set_nonblocking(conn_fd);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);Model mentalny
Wyobraź sobie serwer jako pętlę: zablokuj się w epoll_wait(), odbierz krótką listę gotowych deskryptorów, wykonaj na każdym nieblokującą pracę i powtórz.
Wątek śpi, gdy nic się nie dzieje, i budzi się tylko do rzeczywistej pracy. Jeden wątek może teraz wydajnie obsługiwać dziesiątki tysięcy połączeń.
Szybkie sprawdzenie
Sprawdź swoje rozumienie nieblokujących gniazd.
Podsumowanie
Blokujące wejście/wyjście jest proste, ale zajmuje jeden wątek na każde połączenie, co nie skaluje się przy dużym obciążeniu. Nieblokujące wejście/wyjście zwraca natychmiast z EAGAIN, zamiast zasypiać.
Odpytywanie gniazd w ciasnej pętli marnuje czas procesora, dlatego używamy powiadamiania o gotowości. select/poll mają złożoność O(n), a epoll skaluje się do wielu tysięcy połączeń. Następnie skonfigurujemy epoll.
Często zadawane pytania
Czy lekcja „Blokujące a nieblokujące I/O” jest bezpłatna?
Tak — pełny tekst „Blokujące a nieblokujące I/O” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu C Academy, przejdź na CoddyKit PRO. Kurs C Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Blokujące a nieblokujące I/O”?
Dowiedzą się Państwo, dlaczego pętle zdarzeń mają znaczenie. Ćwiczysz C Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć C Academy?
Nie wymagamy żadnego doświadczenia. C Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 1 z 4.
Ile czasu zajmuje lekcja „Blokujące a nieblokujące I/O”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji C Academy?
Tak. Każda lekcja C Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Blokujące a nieblokujące I/O
- Konfiguracja epoll
- Pętla zdarzeń
- Prosty serwer echo