说明
一直听说epoll的饥饿场景,但是从未在实际环境中面对过,那么能不能模拟出来呢?实际的情况是怎样呢?
模拟步骤
- 基于epoll写一个简单的tcp echo server,将每次read返回的字节数打印出来
- 模拟一个客户端大量写入
- 测试其他客户端能否正常返回
Server代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define MAX_EVENTS 1024
#define LISTEN_BACKLOG 10int epoll_fd;
void do_read(int fd);int main() {int server_fd, nfds, i;struct epoll_event event, events[MAX_EVENTS];struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);int client_fd;// 创建 socketserver_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");return 1;}// 设置 socket 选项int opt = 1;if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {perror("setsockopt");close(server_fd);return 1;}// 绑定 socketmemset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(8080);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(server_fd);return 1;}// 监听 socketif (listen(server_fd, LISTEN_BACKLOG) == -1) {perror("listen");close(server_fd);return 1;}// 创建 epoll 实例epoll_fd = epoll_create1(0);if (epoll_fd == -1) {perror("epoll_create1");close(server_fd);return 1;}// 注册服务器 socketevent.events = EPOLLIN;event.data.fd = server_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {perror("epoll_ctl");close(server_fd);close(epoll_fd);return 1;}printf("Server listening on port 8080...\n");while (1) {// 等待事件就绪nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds == -1) {perror("epoll_wait");close(server_fd);close(epoll_fd);return 1;}// 处理就绪事件for (i = 0; i < nfds; i++) {if (events[i].data.fd == server_fd) {// 接受新连接client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");continue;}if (fcntl(client_fd , F_SETFL, O_NONBLOCK) == -1) {perror("fcntl");close(client_fd);continue;}// 注册客户端 socketevent.events = EPOLLIN;event.data.fd = client_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {perror("epoll_ctl");close(client_fd);continue;}printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));} else {do_read(events[i].data.fd);}}}close(server_fd);close(epoll_fd);return 0;
}void do_read(int fd) {// 处理客户端数据char buf[1024];while(1) {ssize_t bytes_read = read(fd, buf, sizeof(buf));if (bytes_read == -1) {perror("read");close(fd);if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1) {perror("epoll_ctl");}break;} else if (bytes_read == 0) {printf("Client disconnected\n");close(fd);if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1) {perror("epoll_ctl");}break;} else {printf("Received data: %d\n", bytes_read);if (write(fd, buf, bytes_read) != bytes_read) {perror("write");close(fd);if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1) {perror("epoll_ctl");}break;}if (bytes_read < 1024) {break;}}}
}
模拟客户端
客户端1:大量写入客户端:
cat /dev/random 2>/dev/null | nc 127.0.0.1 8080 >/dev/null
客户端2:其他写入客户端,少量写入检查返回值
nc 127.0.0.1 8080
模拟结果
- server端收到大量的数据,每次read返回1024个字节,句柄非常忙碌
- 客户端2往server发送的数据一直没有返回【处于饥饿状态】
- 一旦客户端1断开,客户端2就收到回复了
结果分析
从代码中可以知道,read一直都有数据读取,一直在处理数据,导致其他句柄无法处理数据。也就是说,其实是我们的代码造成了所谓的饥饿,那么也可以从我们的代码层面上去解决这个问题,思路官方man page中已经提到了,将fd维护一个list,均匀的读写数据即可。