模拟epoll的饥饿场景

epoll · 浏览次数 : 0

小编点评

本文通过使用epoll实现了一个简单的TCP echo服务器,并模拟了客户端大量写入的场景。实验结果表明,在大量客户端写入的情况下,会导致其他正常写入的客户端出现饥饿状态。这是因为服务器在处理每个连接请求时,都会消耗一定的时间和资源。为了解决这个问题,可以通过维护一个fd列表,均匀地分配数据和请求到各个连接上,从而避免饥饿现象的发生。 1. 本文首先介绍了epoll的相关概念和用法,以及如何创建监听套接字、绑定地址、监听端口、创建epoll实例等操作。 2. 在main函数中,服务器创建成功后开始监听客户端连接请求,并通过epoll_ctl添加到epoll实例中。 3. 服务器使用了一个while循环,不断接受新的连接请求,并对每个连接进行处理。当客户端连接有效时,添加到epoll实例中,并为其注册读事件。 4. do_read函数负责处理客户端的数据读取操作。在这个函数中,我们使用了循环来处理客户端发来的数据,并根据返回值判断是否有新的数据可读或客户端已断开连接。 5. 在模拟客户端的过程中,作者通过大量的写入操作来模拟多个客户端的请求,然后观察服务器端的响应情况。 6. 实验结果显示,当有大量客户端不断写入数据时,其他正常写入的客户端会出现饥饿状态,无法收到服务器的回应。这是因为服务器在处理数据时,会优先处理最早加入epoll实例的连接请求。 7. 为了解决这个问题,可以在do_read函数中维护一个fd列表,将客户端连接按照一定的策略分布到这个列表中。这样可以确保每个连接都能得到公平的处理机会,从而避免饥饿现象的发生。

正文

说明

一直听说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个字节,句柄非常忙碌
    image
  • 客户端2往server发送的数据一直没有返回【处于饥饿状态】
    image
  • 一旦客户端1断开,客户端2就收到回复了
    image

结果分析

从代码中可以知道,read一直都有数据读取,一直在处理数据,导致其他句柄无法处理数据。也就是说,其实是我们的代码造成了所谓的饥饿,那么也可以从我们的代码层面上去解决这个问题,思路官方man page中已经提到了,将fd维护一个list,均匀的读写数据即可。

与模拟epoll的饥饿场景相似的内容:

模拟epoll的饥饿场景

说明 一直听说epoll的饥饿场景,但是从未在实际环境中面对过,那么能不能模拟出来呢?实际的情况是怎样呢? 模拟步骤 基于epoll写一个简单的tcp echo server,将每次read返回的字节数打印出来 模拟一个客户端大量写入 测试其他客户端能否正常返回 Server代码 #include

高性能网络设计秘笈:深入剖析Linux网络IO与epoll

本文介绍了网络IO模型,引入了epoll作为Linux系统中高性能网络编程的核心工具。通过分析epoll的特点与优势,并给出使用epoll的注意事项和实践技巧,该文章为读者提供了宝贵的指导。

[转帖]总结:Tomcat的IO模型

一、介绍 对于 linux 操作系统,IO 多路复用使用的是 epoll 方式,对于 windows 操作系统中 IO 多路复用使用的是 iocp 方式,对于 mac 操作系统 IO 多路复用使用的是 kqueue 方式。 由于对于 tomcat 服务器来说基本主要部署在 linux 操作系统上,所

通过redis学网络(1)-用go基于epoll实现最简单网络通信框架

![image.png](https://img2023.cnblogs.com/blog/1382767/202306/1382767-20230607105418219-574417823.png) > 本系列主要是为了对redis的网络模型进行学习,我会用golang实现一个reactor网络

探索Java通信面试的奥秘:揭秘IO模型、选择器和网络协议,了解面试中的必备知识点!

通过深入探索Java通信面试的奥秘,我们将揭秘Java中的三种I/O模型(BIO、NIO和AIO)、选择器(select、poll和epoll)以及网络协议(如HTTP和HTTPS),帮助您了解在面试中必备的知识点。这些知识点对于网络编程和系统安全方面的求职者来说至关重要,掌握它们将为您的职业发展打下坚实的基础!

[转帖]深入了解epoll模型 -- 开卷有益

https://cloud.tencent.com/developer/article/1992927?areaSource=&traceId= 希望打开此篇对你有所帮助。 文章目录 什么是epoll?或者说,它和select有什么判别? 什么是select 为什么select最大只允许1024?

epoll使用与原理

使用要点 边缘模式(ET)与水平模式(LT)区别 下面内容来自linux man page The epoll event distribution interface is able to behave both as edge-triggered (ET) and as level-trigge

软考-官方模拟考试-务必参加!

模拟练习时间 模拟练习平台开放时间:2024年5月13日 9:00 至 5月23日 17:00,报名参加考试的考生可在该时段内自愿进行网上模拟平台练习。 模拟考试时间只开放10天时间。 官方公告原文:https://www.ruankao.org.cn/article/content/2405071

[转帖]模拟enq: TX - row lock contention争用

https://www.modb.pro/db/623036 enq: TX - row lock contention它表示一个事务正在等待另一个事务释放被锁定的行。这种等待事件通常发生在并发访问数据库时,多个事务试图同时修改同一行数据时会发生行级锁争用。 以下是可能导致 “enq: TX - r

模拟.NET应用场景,综合应用反编译、第三方库调试、拦截、一库多版本兼容方案

模拟.NET实际应用场景,综合应用三个主要知识点:一是使用dnSpy反编译第三库及调试,二是使用Lib.Harmony库实现第三库拦截、伪造,三是实现同一个库支持多版本同时引用。