說明
一直聽說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 10
int 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;
// 建立 socket
server_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;
}
// 繫結 socket
memset(&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;
}
// 監聽 socket
if (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;
}
// 註冊伺服器 socket
event.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;
}
// 註冊客戶端 socket
event.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,均勻的讀寫資料即可。