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ạn | Chi tiết |
|---|---|
| Số mô tả tệp tối đa | 1024 trên Linux, 64 trên Windows theo mặc định |
| Độ phức tạp mỗi vòng | O(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ại | Toà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ào | Phải dựng lại tập mỗi vòng |
| Độ chính xác thời gian | Micro 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 events | Nghĩa khi ở revents |
|---|---|---|
| POLLIN | Có | Có dữ liệu để đọc, hoặc kết nối mới, hoặc EOF |
| POLLOUT | Có | Ghi được mà không chặn |
| POLLERR | Không cần | Lỗi trên mô tả tệp |
| POLLHUP | Không cần | Bên kia đã đóng hoàn toàn |
| POLLNVAL | Không cần | Mô tả tệp không hợp lệ, thường là đã đóng |
| POLLRDHUP | Có, Linux | Bê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í | select | poll | epoll |
|---|---|---|---|
| Nền tảng | Mọi nơi | POSIX, và WSAPoll | Chỉ Linux |
| Số fd tối đa | 1024 hoặc 64 | Không giới hạn cứng | Không giới hạn cứng |
| Độ phức tạp mỗi vòng | O(n) | O(n) | O(số sẵn sàng) |
| Dựng lại tập mỗi vòng | Có | Không | Không |
| Chép người dùng sang nhân | Toàn bộ, mỗi vòng | Toàn bộ, mỗi vòng | Một lần khi đăng ký |
| Kích hoạt theo cạnh | Không | Không | Có |
| Độ chính xác thời gian chờ | Micro giây | Mili giây | Mili 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ử
- Chạy
sel.cvà giải thích vì sao thứ tự phục vụ khác thứ tự kết nối. - Bỏ phần dựng lại
fd_settrong vòng lặp, chạy lại, và mô tả hiện tượng. - In ra
FD_SETSIZEtrên máy bạn. - Viết lại
sel.cbằngWSAPollhoặcpoll. - Trên Linux, viết bản
epollvà đo số lần gọi hệ thống bằngstrace -cso với bảnselect, với một trăm kết nối. - Cài phần đệm ghi và
EPOLLOUTcho trường hợpsendgử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.
selectcó ở 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.pollkhông phá huỷ đầu vào và không có giới hạn cứng.epolllà 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ớiEAGAIN.- 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ý
EPOLLOUTkhi gửi xong.