Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 46.930 phút đọc

select, poll và epoll

Sau bài này bạn sẽ làm được

  • Viết vòng lặp sự kiện bằng select
  • Chuyển sang poll và giải thích ưu điểm
  • Dùng epoll trên Linux
  • So sánh ba API trên bốn tiêu chí

Ghép kênh vào ra là câu trả lời cho câu hỏi: làm sao một luồng biết được trong hàng nghìn socket thì socket nào đang có dữ liệu, mà không phải hỏi từng cái một.

#Ý tưởng

/* Van de: ban co N socket. recv tren mot socket rong se CHAN.
   Nen ban khong the cu lan luot goi recv tren tung cai.

   Ba loi giai sai:

   1. Mot luong moi socket.
      -> N luong. Bai 46.8 da noi ve gioi han cua no.

   2. Dat socket sang khong chan, roi quay vong hoi tung cai.
      -> Vong lap ban 100% CPU du khong co gi xay ra.
         Duoc goi la "hoi vong", va no la mot phan mem xau.

   3. Khong chan cong voi sleep giua cac vong.
      -> Do tre bang thoi gian sleep. 10 ms sleep nghia la
         moi phan hoi cham them trung binh 5 ms.

   Loi giai dung: hoi HE DIEU HANH.

      "Toi co N socket day. Hay cho toi ngu cho toi khi
       MOT trong so chung co viec, roi danh thuc toi
       va noi la cai nao."

   Do la select, poll, epoll, kqueue, va IOCP. */
Ghép kênh vào ra
Kỹ thuật để một luồng theo dõi nhiều mô tả tệp cùng lúc, chặn cho tới khi ít nhất một trong số đó sẵn sàng đọc hoặc ghi. Tên tiếng Anh là input output multiplexing.

#select

sel.c, vòng lặp đầy đủ
SOCKET kh[FD_SETSIZE];
int kh_co = 0;
int xong  = 0;

while (xong < SO_KH) {
    /* 1. Dung lai danh sach MOI VONG. select pha huy no. */
    fd_set doc;
    FD_ZERO(&doc);
    FD_SET(sv, &doc);                           /* socket lang nghe */
    for (int i = 0; i < kh_co; ++i) FD_SET(kh[i], &doc);

    struct timeval tv = { 5, 0 };
    int r = select(0, &doc, NULL, NULL, &tv);
    if (r == 0) { printf("select het gio\n"); break; }
    if (r < 0)  { printf("select loi %d\n", WSAGetLastError()); break; }

    /* 2. Socket lang nghe san sang doc = co ket noi moi */
    if (FD_ISSET(sv, &doc)) {
        SOCKET c = accept(sv, NULL, NULL);
        if (c != INVALID_SOCKET) {
            kh[kh_co++] = c;
            printf("[%4lu ms] server: accept, dang theo doi %d socket\n",
                   tich()-t0, kh_co);
        }
    }

    /* 3. Duyet cac socket client */
    for (int i = 0; i < kh_co; ) {
        if (!FD_ISSET(kh[i], &doc)) { ++i; continue; }

        char dem[64];
        int n = recv(kh[i], dem, 63, 0);
        if (n > 0) {
            dem[n] = 0;
            printf("[%4lu ms] server: doc \"%s\", tra loi ngay\n",
                   tich()-t0, dem);
            send(kh[i], dem, n, 0);
        }
        closesocket(kh[i]);
        ++xong;
        kh[i] = kh[--kh_co];        /* xoa: chuyen phan tu cuoi len */
        /* KHONG ++i o day, vi vi tri i gio la phan tu khac */
    }
}
terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 sel.c -o sel.exe -lws2_32
./sel.exe
[   0 ms] server: mot luong, dung select
[ 156 ms] client 1: ket noi, se ngu 2000 ms
[ 156 ms] server: accept, dang theo doi 1 socket
[ 218 ms] client 2: ket noi, se ngu 300 ms
[ 218 ms] server: accept, dang theo doi 2 socket
[ 265 ms] client 3: ket noi, se ngu 800 ms
[ 265 ms] server: accept, dang theo doi 3 socket
[ 531 ms] server: doc "toi la client 2", tra loi ngay
[ 531 ms] client 2: nhan "toi la client 2"
[1078 ms] server: doc "toi la client 3", tra loi ngay
[1078 ms] client 3: nhan "toi la client 3"
[2156 ms] server: doc "toi la client 1", tra loi ngay
[2156 ms] client 1: nhan "toi la client 1"
[2156 ms] server: da phuc vu 3 client bang MOT luong

#Giới hạn của select

fdsize.c
#include <stdio.h>
#include <winsock2.h>
int main(void) {
    printf("FD_SETSIZE tren Winsock = %d\n", FD_SETSIZE);
    printf("sizeof(fd_set) = %d byte\n", (int)sizeof(fd_set));
    return 0;
}
terminal
./fs.exe
FD_SETSIZE tren Winsock = 64
sizeof(fd_set) = 520 byte
Giới hạnChi tiết
Số mô tả tệp tối đa1024 trên Linux, 64 trên Windows theo mặc định
Độ phức tạp mỗi vòngO(n) trong nhân: nhân duyệt toàn bộ bitmap dù chỉ một fd sẵn sàng
Chép qua lạiToàn bộ fd_set được chép từ không gian người dùng vào nhân mỗi vòng
Phá huỷ đầu vàoPhải dựng lại tập mỗi vòng
Độ chính xác thời gianMicro giây, đủ cho hầu hết mục đích

#poll

Cùng vòng lặp, viết bằng poll
#include <poll.h>

struct pollfd pfd[1024];
nfds_t so = 0;

pfd[so].fd     = sv;
pfd[so].events = POLLIN;
so++;

for (;;) {
    int r = poll(pfd, so, 5000);        /* thoi gian cho tinh bang MILI GIAY */
    if (r == 0) continue;               /* het gio */
    if (r < 0) {
        if (errno == EINTR) continue;
        break;
    }

    /* Socket lang nghe */
    if (pfd[0].revents & POLLIN) {
        int c = accept(sv, NULL, NULL);
        if (c >= 0 && so < 1024) {
            pfd[so].fd      = c;
            pfd[so].events  = POLLIN;
            pfd[so].revents = 0;
            so++;
        }
    }

    /* Client */
    for (nfds_t i = 1; i < so; ) {
        short re = pfd[i].revents;

        if (re & (POLLHUP | POLLERR | POLLNVAL)) {
            close(pfd[i].fd);
            pfd[i] = pfd[--so];
            continue;
        }
        if (re & POLLIN) {
            char dem[1024];
            ssize_t n = recv(pfd[i].fd, dem, sizeof dem, 0);
            if (n <= 0) {
                close(pfd[i].fd);
                pfd[i] = pfd[--so];
                continue;
            }
            send(pfd[i].fd, dem, (size_t)n, 0);
        }
        ++i;
    }
}
CờĐặt vào eventsNghĩa khi ở revents
POLLINCóCó dữ liệu để đọc, hoặc kết nối mới, hoặc EOF
POLLOUTCóGhi được mà không chặn
POLLERRKhông cầnLỗi trên mô tả tệp
POLLHUPKhông cầnBên kia đã đóng hoàn toàn
POLLNVALKhông cầnMô tả tệp không hợp lệ, thường là đã đóng
POLLRDHUPCó, LinuxBên kia đã đóng chiều gửi của nó

#epoll

epoll, chỉ có trên Linux
#include <sys/epoll.h>

int ep = epoll_create1(0);
if (ep < 0) { perror("epoll_create1"); return 1; }

/* Dang ky socket lang nghe MOT LAN duy nhat */
struct epoll_event sk;
sk.events  = EPOLLIN;
sk.data.fd = sv;
epoll_ctl(ep, EPOLL_CTL_ADD, sv, &sk);

struct epoll_event sk_ra[64];

for (;;) {
    int n = epoll_wait(ep, sk_ra, 64, -1);      /* -1 = cho mai */
    if (n < 0) {
        if (errno == EINTR) continue;
        break;
    }

    /* CHI duyet nhung cai SAN SANG, khong duyet toan bo */
    for (int i = 0; i < n; ++i) {
        int fd = sk_ra[i].data.fd;

        if (fd == sv) {
            int c = accept(sv, NULL, NULL);
            if (c < 0) continue;
            struct epoll_event e;
            e.events  = EPOLLIN;
            e.data.fd = c;
            epoll_ctl(ep, EPOLL_CTL_ADD, c, &e);
            continue;
        }

        if (sk_ra[i].events & (EPOLLHUP | EPOLLERR)) {
            epoll_ctl(ep, EPOLL_CTL_DEL, fd, NULL);
            close(fd);
            continue;
        }

        char dem[1024];
        ssize_t r = recv(fd, dem, sizeof dem, 0);
        if (r <= 0) {
            epoll_ctl(ep, EPOLL_CTL_DEL, fd, NULL);
            close(fd);
            continue;
        }
        send(fd, dem, (size_t)r, 0);
    }
}
close(ep);

#So sánh

Tiêu chíselectpollepoll
Nền tảngMọi nơiPOSIX, và WSAPollChỉ Linux
Số fd tối đa1024 hoặc 64Không giới hạn cứngKhông giới hạn cứng
Độ phức tạp mỗi vòngO(n)O(n)O(số sẵn sàng)
Dựng lại tập mỗi vòngCóKhôngKhông
Chép người dùng sang nhânToàn bộ, mỗi vòngToàn bộ, mỗi vòngMột lần khi đăng ký
Kích hoạt theo cạnhKhôngKhôngCó
Độ chính xác thời gian chờMicro giâyMili giâyMili giây
Vòng lặp sự kiện sai
for (;;) { epoll_wait(ep, sk, 64, -1); for (...) { /* Doc mot tep 100 MB */ doc_tep(duong_dan, &du_lieu); /* Truy van co so du lieu */ ket_qua = truy_van(sql); /* Phan giai DNS */ getaddrinfo(ten, ...); send(fd, ket_qua, co, 0); /* CO THE CHAN */ } } /* Moi thao tac cham o day lam TAT CA cac ket noi khac dung hinh. Mot truy van 2 giay = 2 giay tre cho 10000 client. */
Vòng lặp sự kiện đúng
for (;;) { epoll_wait(ep, sk, 64, -1); for (...) { /* Chi lam viec KHONG CHAN */ recv(fd, dem, co, 0); /* socket khong chan */ /* Viec cham -> day sang be luong, nhan ket qua qua mot mo ta tep thong bao (eventfd tren Linux, hoac mot cap socket tu tao) */ be_them(&be, tao_viec(fd, dem)); /* Ghi: neu send khong het, LUU phan con lai va dang ky EPOLLOUT, dung vong lap cho */ ssize_t g = send(fd, k->dem_ra + k->ra_da_gui, k->ra_co - k->ra_da_gui, 0); if (g > 0) k->ra_da_gui += (size_t)g; if (k->ra_da_gui < k->ra_co) { e.events = EPOLLIN | EPOLLOUT; epoll_ctl(ep, EPOLL_CTL_MOD, fd, &e); } } }

Tự làm thử

  1. Chạy sel.c và giải thích vì sao thứ tự phục vụ khác thứ tự kết nối.
  2. Bỏ phần dựng lại fd_set trong vòng lặp, chạy lại, và mô tả hiện tượng.
  3. In ra FD_SETSIZE trên máy bạn.
  4. Viết lại sel.c bằng WSAPoll hoặc poll.
  5. Trên Linux, viết bản epoll và đo số lần gọi hệ thống bằng strace -c so với bản select, với một trăm kết nối.
  6. Cài phần đệm ghi và EPOLLOUT cho trường hợp send gửi không hết.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Ghép kênh vào ra: hỏi hệ điều hành xem socket nào sẵn sàng, thay vì hỏi vòng hoặc tạo một luồng cho mỗi socket.
  • select có ở mọi nơi nhưng giới hạn 1024 fd trên Linux và 64 trên Windows, và phải dựng lại tập mỗi vòng.
  • poll không phá huỷ đầu vào và không có giới hạn cứng.
  • epoll là O(số sẵn sàng) vì danh sách nằm trong nhân; kích hoạt theo cạnh nhanh hơn nhưng bắt buộc đọc tới EAGAIN.
  • Không bao giờ làm việc chậm bên trong vòng lặp sự kiện, và nhớ huỷ đăng ký EPOLLOUT khi gửi xong.