Linux高级I/O:多路转接模型

在这里插入图片描述

目录

  • 一.常见的IO模型介绍
  • 二.多路转接I/O
      • 1.select
        • 1.1.函数解析
        • 1.2. select特点和缺点
        • 1.3.基于 select 的多客户端网络服务器
      • 2.poll
        • 2.1.poll函数解析
        • 2.2.poll特点和缺点
        • 2.3.基于poll的tcp服务器
      • 3.epoll
        • 3.1.系列函数解析
        • 3.2.epoll原理解析
        • 2.3.基于 select 的多客户端网络服务器
        • 3.4.epoll的优点
  • 三.LT和ET

一.常见的IO模型介绍

在 Linux 中,主要有以下几种 I/O 模型:

  1. 阻塞 I/O:在阻塞 I/O 模型中,当进程请求 I/O 操作时,它会被挂起,直到该操作完成。这种方式进程在等待 I/O 的同时不能做其他的事。
  2. 非阻塞 I/O:非阻塞 I/O 允许进程发出 I/O 请求后立即返回,进程可以继续执行其他操作。为了检查 I/O 是否完成,进程需要通过轮询方式不断检查状态,所以也常称为非阻塞轮询IO,这种方式可以减少等待时间,但是在高频率轮询时很消耗cpu资源。
  3. I/O 多路复用:I/O 多路复用通过使用 select、poll 或 epoll 等系统调用,使一个进程/线程能够同时监视多个文件描述符。该模型在处理多个并发连接时效率较高,适合网络服务器等应用场景。
  4. 信号驱动 I/O:在信号驱动 I/O 模型中,进程在发出 I/O 请求后可以继续执行其他任务,操作的完成通过信号通知。这种方式可以减少轮询,但是处理起来较为复杂。
  5. 异步 I/O:异步 I/O 是最先进的模型,进程发出 I/O 请求后立即返回,并在操作完成时通过回调函数或其他机制获得通知。异步 I/O 模型通常提供最高的性能,适合高负载、高并发的应用程序。

二.多路转接I/O

1.select

1.1.函数解析
#include <sys/select.h>

int pselect(int nfds, fd_set *restrict readfds,
              fd_set *restrict writefds, fd_set *restrict errorfds,
              const struct timespec *restrict timeout,
              const sigset_t *restrict sigmask);
              
int select(int nfds, fd_set *restrict readfds,
              fd_set *restrict writefds, fd_set *restrict errorfds,
              struct timeval *restrict timeout);
              
void FD_CLR(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
  • nfds:待监视的文件描述符最大值 + 1
  • readfds:指向一组需要监视“可读”事件的文件描述符集合。
  • writefds:指向一组需要监视“可写”事件的文件描述符集合。
  • exceptfds:指向一组需要监视异常事件的文件描述符集合。
  • timeout:timeout 是一个 struct timeval 结构体,包含等待的秒数和微秒数,如果在超时时间内有文件描述符变为可用状态,select 会返回,返回值是可用的文件描述符数量。如果发生错误,select 返回 -1,并设置 errno。
  • select 函数返回就绪的文件描述符数,如果返回 0 则表示超时。

fd_set的介绍
fd_set 是一个位图结构,用于存储多个文件描述符。select 使用宏来操作 fd_set,常见的宏有:

FD_ZERO(fd_set *set):将文件描述符集合置空。
FD_SET(int fd, fd_set *set):将文件描述符 fd加入集合。
FD_CLR(int fd, fd_set *set):将文件描述符 fd 从集合中移除。
FD_ISSET(int fd, fd_set *set):检查文件描述符 fd 是否在集合中,并且是否准备就绪。

下面是一个简单的使用演示代码:

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
int maxfd = sockfd + 1;

int result = select(maxfd, &readfds, nullptr, nullptr, nullptr);
if (result > 0) {
    if (FD_ISSET(sockfd, &readfds)) {
        // 处理读操作
    }
}

初始化fd_set集合将需要监控的fd添加进去,接着调用select函数,maxfd是需要监控的fd集合中最大值再加1,select可以同时监控读事件、写事件、异常事件。timeout设置为nullptr标识阻塞等待直到事件有就绪的或者出错。

1.2. select特点和缺点

特点

  1. 多路复用:select 允许一个进程同时监控多个文件描述符,等待其中一个或多个变为可读、可写或异常状态。
  2. 可以处理不同类型的文件描述符:能够监控常规文件、套接字、管道等多种类型的文件描述符。
  3. 跨平台支持:在许多 Unix-like 操作系统以及 Windows 中都有支持,具有良好的移植性。

缺点

  1. 性能瓶颈:当监控的文件描述符数量很大时,select 的性能会显著下降。每次调用 select 都需要遍历所有文件描述符,增加了开销。
  2. 文件描述符数量限制:select 有文件描述符的数量限制(通常是 1024,取决于类型fd_set的大小),超过这个限制就不能监控更多的描述符。
  3. 每次调用都需要重置:每次调用 select 都需要重新设置文件描述符集合,这在处理多个连接时会带来额外的开销。
  4. 无优先级支持:select 不能为不同的文件描述符设置优先级,所有的描述符在监控时是平等的。
  5. 不适合高并发场景:在高并发情况下(如数千个连接),select 的效率会下降,通常需要使用其他机制(如 poll、epoll 或 kqueue)来更好地处理大量并发连接。
1.3.基于 select 的多客户端网络服务器

下面我们来实现一个简单的基于 select 的 TCP 服务器来窥探他的使用(附上详细注释):

#pragma once

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include <string>

#include "Socket.hpp"

using namespace std;

// 默认端口号
static const uint16_t defaultport = 8888;
// 最大文件描述符数量
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1; // 默认文件描述符,表示未使用

class SelectServer
{
public:
    // 构造函数,初始化端口和文件描述符数组
    SelectServer(uint16_t port = defaultport) : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            fd_array[i] = defaultfd; // 初始化所有文件描述符为默认值
        }
    }

    // 初始化服务器,创建套接字、绑定和监听
    bool Init()
    {
        _listensock.Socket(); // 创建套接字
        _listensock.Bind(_port); // 绑定到指定端口
        _listensock.Listen(); // 开始监听

        return true;
    }

    // 接受新的连接
    void Accepter()
    {
        // 处理连接事件
        string clientip; // 客户端IP
        uint16_t clientport = 0; // 客户端端口
        int sock = _listensock.Accept(&clientip, &clientport); // 接受连接
        if (sock < 0) return; // 失败则返回


        // 将新连接的sock添加到fd_array数组
        int pos = 1; // 从1开始,0位置为监听套接字
        for (; pos < fd_num_max; pos++) // 查找可用的位置
        {
            if (fd_array[pos] != defaultfd)
                continue; // 当前位置已被占用,继续查找
            else
                break; // 找到可用位置
        }
        if (pos == fd_num_max) // 如果数组已满
        {
            close(sock); // 关闭新连接
        }
        else // 有空位
        {
            fd_array[pos] = sock; // 存储新连接的文件描述符
        }
    }

    // 接收数据
    void Recver(int fd, int pos)
    {
        char buffer[1024]; // 缓冲区
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // 读取数据
        if (n > 0) // 有数据到达
        {
            buffer[n] = 0; // 结束符
            cout << "get a messge: " << buffer << endl; // 输出接收到的信息
        }
        else if (n == 0) // 客户端关闭连接
        {
            close(fd); // 关闭连接
            fd_array[pos] = defaultfd; // 从数组中移除
        }
        else // 发生错误
        {
            close(fd); // 关闭连接
            fd_array[pos] = defaultfd; // 从数组中移除
        }
    }

    // 分发事件
    void Dispatcher(fd_set &rfds)
    {
        for (int i = 0; i < fd_num_max; i++) // 遍历文件描述符数组
        {
            int fd = fd_array[i];
            if (fd == defaultfd)
                continue; // 跳过未使用的描述符

            if (FD_ISSET(fd, &rfds)) // 检查描述符是否就绪
            {
                if (fd == _listensock.Fd()) // 如果是监听套接字
                {
                    Accepter(); // 处理新连接
                }
                else // 其他连接
                {
                    Recver(fd, i); // 接收数据
                }
            }
        }
    }

    // 启动服务器
    void Start()
    {
        int listensock = _listensock.Fd(); // 获取监听套接字的文件描述符
        fd_array[0] = listensock; // 将监听套接字放入数组的第一个位置
        while (true)
        {
            fd_set rfds; // 文件描述符集合
            FD_ZERO(&rfds); // 清空集合

            int maxfd = fd_array[0]; // 最大文件描述符
            for (int i = 0; i < fd_num_max; i++) // 遍历文件描述符数组
            {
                if (fd_array[i] == defaultfd)
                    continue; // 跳过未使用的描述符
                FD_SET(fd_array[i], &rfds); // 将就绪的描述符加入集合
                if (maxfd < fd_array[i]) // 更新最大文件描述符
                {
                    maxfd = fd_array[i];
                }
            }

            // 设置超时为 0,非阻塞轮询
            struct timeval timeout = {0, 0}; // 超时设置为 0

            // 调用 select 检测事件
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, timeout);
            switch (n)
            {
            case 0:
                cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl; // 超时处理
                break;
            case -1:
                cerr << "select error" << endl; // 错误处理
                break;
            default:
                cout << "get a new link!!!!!" << endl;
                Dispatcher(rfds); // 处理就绪的事件
                break;
            }
        }
    }

    // 析构函数,关闭监听套接字
    ~SelectServer()
    {
        _listensock.Close(); // 关闭套接字
    }

private:
    Sock _listensock; // 监听套接字对象
    uint16_t _port; // 服务器端口
    int fd_array[fd_num_max]; // 文件描述符数组,用于维护连接
};


2.poll

2.1.poll函数解析
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds: 传入一个结构体数组,用于保存需要监控的文件描述符以及感兴趣的事件。
  • nfds: 数组 fds 中的文件描述符个数。
  • timeout: 指定超时时间,单位为毫秒。
  • 返回值小于0表示出错,等于表示超时,大于0表示事件就绪。

pollfd 结构体

struct pollfd {
    int fd;         // 文件描述符
    short events;   // 需要监控的事件
    short revents;  // 实际发生的事件,由 poll 填充
};
  • fd: 需要监控的文件描述符。
  • events: 指定感兴趣的事件,比如 POLLIN(可读)、POLLOUT(可写)等。
  • revents: 存储实际发生的事件,由 poll 调用后填充。

events和revents的取值:

事件类型eventsrevents描述
可读事件POLLINPOLLIN数据可读,通常指套接字上有数据可读
可写事件POLLOUTPOLLOUT数据可写,通常指套接字可以发送数据
错误事件POLLERRPOLLERR发生错误,需要处理
挂起事件POLLHUPPOLLHUP连接已关闭或挂起
非法请求POLLNVALPOLLNVAL监控的文件描述符无效
超时--当设置的超时时间到达,但没有事件发生
2.2.poll特点和缺点

poll的优点

  1. 无文件描述符限制:与 select 不同,poll 不受文件描述符数量的限制,因此可以处理更多的连接。
  2. 简单性:poll 的使用相对select简单,容易理解和实现。

poll的缺点

  1. 线性扫描:在事件发生时,poll 会线性扫描所有文件描述符,效率较低。对于大量文件描述符,响应时间可能变长。
  2. 性能问题:每次调用 poll 时,都需要传递整个文件描述符数组,性能降低
  3. 不支持边缘触发:poll **不支持边缘触发模式
2.3.基于poll的tcp服务器

下面我们来实现一个简单的基于poll的 TCP 服务器来窥探他的使用(附上详细注释):

#pragma once

#include <iostream>
#include <poll.h>       // 引入poll头文件,用于多路复用
#include <sys/time.h>   // 时间操作相关
#include "Socket.hpp"   // 自定义的Socket类,封装了socket的基本操作

using namespace std;

static const uint16_t defaultport = 8888; // 默认监听端口号为8888
static const int fd_num_max = 128;         // 最大支持的文件描述符数量
int defaultfd = -1;                       // 默认无效的文件描述符值
const int _no events = 0;                        // 无事件标志

class PollServer
{
public:
    // 构造函数,初始化监听端口号,并设置poll事件数组
    PollServer(uint16_t port = defaultport) 
            : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++) // 初始化所有的文件描述符和事件
        {
            _event_fds[i].fd = defaultfd;    // 将所有文件描述符设置为默认值
            _event_fds[i].events = _no events; // 清空所有事件类型
            _event_fds[i].revents = _no events; // 清空所有返回事件
        }
    }

    // 初始化服务器,创建监听套接字并绑定到指定端口
    bool Init()
    {
        _listensock.Socket();   // 创建监听socket
        _listensock.Bind(_port); // 绑定到指定端口
        _listensock.Listen();   // 开始监听
        return true;            // 初始化成功返回true
    }

    // 处理新连接的客户端
    void Accepter()
    {
        // 有新的连接事件触发,接收连接
        string clientip;         // 用于存储客户端IP地址
        uint16_t clientport = 0;      // 用于存储客户端端口号
        int sock = _listensock.Accept(&clientip, &clientport); // 接收客户端连接,返回新socket
        if (sock < 0) return;         // 如果返回值小于0,表示接收失败

        // 将新连接的socket放入_event_fds数组
        int pos = 1;
        for (; pos < fd_num_max; pos++) // 查找空闲的数组位置
        {
            if (_event_fds[pos].fd != defaultfd) // 如果当前文件描述符已经被占用,继续寻找
                continue;
            else
                break; // 找到空闲位置退出循环
        }
        if (pos == fd_num_max) // 如果位置满了,无法接收更多客户端
        {
            close(sock); // 关闭新连接
        }
        else
        {
            // 将新socket添加到poll事件数组中,监听读事件
            _event_fds[pos].fd = sock;
            _event_fds[pos].events = POLLIN; // 监听读事件
            _event_fds[pos].revents = _no events; // 初始化返回事件
            PrintFd(); // 打印当前在线的fd列表
        }
    }

    // 处理客户端发送过来的消息
    void Recver(int fd, int pos)
    {
        char buffer[1024];                // 接收数据的缓冲区
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // 从客户端读取数据
        if (n > 0) // 正常读取到数据
        {
            buffer[n] = 0;                // 将数据末尾补上字符串结束符
            cout << "get a messge: " << buffer << endl; // 打印接收到的消息
        }
        else if (n == 0) // 客户端主动断开连接
        {
            close(fd);                    // 关闭文件描述符
            _event_fds[pos].fd = defaultfd; // 从事件数组中移除该客户端
        }
        else // 读取出错
        {
            close(fd);                    // 关闭文件描述符
            _event_fds[pos].fd = defaultfd; // 从事件数组中移除该客户端
        }
    }

    // 事件分发器,处理所有准备好的事件
    void Dispatcher()
    {
        for (int i = 0; i < fd_num_max; i++) // 遍历所有的文件描述符
        {
            int fd = _event_fds[i].fd;
            if (fd == defaultfd)  // 如果是默认值,说明该位置空闲,跳过
                continue;

            if (_event_fds[i].revents & POLLIN) // 如果当前文件描述符有读事件
            {
                if (fd == _listensock.Fd()) // 如果是监听socket上的事件,处理新的连接
                {
                    Accepter(); // 处理新连接
                }
                else // 如果是普通客户端socket上的事件
                {
                    Recver(fd, i); // 接收客户端消息
                }
            }
        }
    }

    // 服务器主循环,启动服务器并进入事件处理
    void Start()
    {
        _event_fds[0].fd = _listensock.Fd(); // 将监听socket放入第一个位置
        _event_fds[0].events = POLLIN;       // 监听读事件
        int timeout = 1000; // 设置poll的超时时间为3秒
        for (;;)
        {
            // 调用poll函数,监控事件
            int n = poll(_event_fds, fd_num_max, timeout);
            switch (n)
            {
            case 0: // 超时无事件
                cout << "time out... " << endl;
                break;
            case -1: // 发生错误
                cerr << "poll error" << endl;
                break;
            default: // 有事件发生
                cout << "get a new link!!!!!" << endl;
                Dispatcher(); // 分发处理就绪的事件
                break;
            }
        }
    }

    // 打印当前在线的文件描述符列表
    void PrintFd()
    {
        cout << "fd list: ";
        for (int i = 0; i < fd_num_max; i++) // 遍历所有的文件描述符
        {
            if (_event_fds[i].fd == defaultfd) // 跳过无效的文件描述符
                continue;
            cout << _event_fds[i].fd << " "; // 打印文件描述符
        }
        cout << endl;
    }

    // 析构函数,关闭监听socket
    ~PollServer()
    {
        _listensock.Close();
    }

private:
    Sock _listensock;                      // 自定义封装的监听socket
    uint16_t _port;                        // 服务器监听的端口号
    struct pollfd _event_fds[fd_num_max];  // poll事件数组,保存所有的文件描述符及其事件
};

3.epoll

epoll 是 Linux 下用于处理 I/O 事件的高效多路复用机制,适用于需要处理大量并发连接的网络服务器,相较于poll提供了更好的性能,可以说是他的升级版。

3.1.系列函数解析
int epoll_create(int size);

创建一个 epoll 实例,返回一个文件描述符(指向的struct_file对象细节在原理讲解),用于后续的 epoll_ctl 和 epoll_wait 调用。

参数:

  • size:指定要监听的最大文件描述符数量。
  • 返回值:成功返回 epoll 文件描述符,失败返回 -1 并设置 errno。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

控制 epoll 实例的行为,包括添加、修改或删除文件描述符的事件。

参数:

  • epfd:由 epoll_create 返回的 epoll 文件描述符。
  • op:操作类型,可以是以下之一:
  • EPOLL_CTL_ADD:添加文件描述符。
  • EPOLL_CTL_MOD:修改文件描述符。
  • EPOLL_CTL_DEL:删除文件描述符。
  • fd:要操作的文件描述符。
  • event:指向 epoll_event 结构的指针,描述要监听的事件。
  • 返回值:成功返回 0,失败返回 -1 并设置 errno。

epoll_event 结构:

struct epoll_event {
    uint32_t events;    // 事件类型
    epoll_data_t data;  // 用户数据
};
union epoll_data {
    void *ptr;         // 用户自定义数据指针
    int fd;            // 文件描述符
    uint32_t u32;      // 无符号整型数据
    uint64_t u64;      // 无符号长整型数据
};

events:

  • 类型:uint32_t
  • 描述:要监听的事件类型,可以多个组合。
  • 常用的事件类型包括:

EPOLLIN:表示文件描述符可读。
EPOLLOUT:表示文件描述符可写。
EPOLLERR:表示发生错误。
EPOLLHUP:表示挂起事件。
EPOLLET:边缘触发模式(Edge Triggered),意味着在状态变化时才通知。

使用示例

struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 监听可读事件,设置为边缘触发模式
event.data.fd = socket_fd;         // 关联的文件描述符
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

功能:等待事件发生,返回已就绪的个数。

参数:

  • epfd:由 epoll_create 返回的 epoll 文件描述符。
  • events:保存就绪事件。
  • maxevents:events 数组的大小。
  • timeout:等待的时间,单位是毫秒。可以设置为 0(不阻塞),-1(永久等待),或正值(指定超时时间)。
  • 返回值:成功时返回就绪事件的数量,失败返回 -1 并设置 errno。
3.2.epoll原理解析

当调用 epoll_create 时,内核会执行以下操作:

  • L创建一个 eventpoll 结构体实例,用于管理注册的文件描述符及其事件。
  • 返回一个文件描述符(fd),该 fd 代表了 eventpoll 结构体

eventpoll 结构体内容

struct eventpoll {
    struct list_head        wq_entry;     // 等待队列条目,用于管理等待事件的线程
    struct hlist_head       rdlist;       // 用于管理注册的可读事件的文件描述符
    struct hlist_head       pr_list;      // 用于优先级处理的链表
    struct rb_root          rbr_tree;     // 红黑树,管理注册的文件描述符
    struct rb_root          rbr_wait;     // 等待事件的红黑树
    wait_queue_head_t       wait;         // 等待队列,用于线程调度
    int                     epfd;         // epoll 文件描述符
    int                     wakeup;       // 唤醒标志
    // 其他字段...
};

主要使用过程解析连接基于tcp实现的epoll多客户端网络服务器后,将需要监控的fd加到红黑树(rbr_tree),等待网卡数据就绪发送中断信号,将数据向上交付后,在红黑树查找到对应的fd,将此结点(包含fd和对应事件)插入就绪队列中(wq_entry),便于用户调用epoll_wait获取就绪节点。

epoll模型原理
在这里插入图片描述

2.3.基于 select 的多客户端网络服务器
#pragma once

#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoller.hpp"
using namespace std;

// 定义事件常量
const int num = 64; // epoll 事件数组的最大数量

// EpollServer 类,用于处理基于 epoll 的服务器逻辑
class EpollServer 
{

public:
    // 构造函数,初始化端口、监听套接字和 epoller
    EpollServer(uint16_t port)
        : _port(port),
          _listsocket_ptr(new Sock()), // 创建一个新的套接字对象
          _epoller_ptr(new Epoller())   // 创建一个新的 epoller 对象
    {
    }

    // 初始化服务器
    void Init()
    {
        _listsocket_ptr->Socket(); // 创建套接字
        _listsocket_ptr->Bind(_port); // 绑定端口
        _listsocket_ptr->Listen(); // 开始监听    
    }

    // 接收新连接
    void Accepter()
    {
        string clientip; // 客户端 IP 地址
        uint16_t clientport;  // 客户端端口
        int sock = _listsocket_ptr->Accept(&clientip, &clientport); // 接收新连接
        if (sock > 0)
        {
            // 将新连接的套接字添加到 epoll 中进行事件监听
            _epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, sock, EPOLLIN);
        }
    }

    // 测试用的接收函数
    void Recver(int fd)
    {
        char buffer[1024]; // 接收缓冲区
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // 读取数据
        if (n > 0)
        {
            buffer[n] = 0; // 确保字符串结束
            cout << "get a messge: " << buffer << endl; // 输出接收到的消息
            
            // 发送回显消息
            string echo_str = "server echo $ ";
            echo_str += buffer;
            write(fd, echo_str.c_str(), echo_str.size());
        }
        else if (n == 0)
        {
            _epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0); // 从 epoll 中移除
            close(fd); // 关闭套接字
        }
        else
        {
            // 发生接收错误
            _epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0); // 从 epoll 中移除
            close(fd); // 关闭套接字
        }
    }

    // 分发事件处理
    void Dispatcher(struct epoll_event revs[], int num)
    {
        for (int i = 0; i < num; i++)
        {
            uint32_t events = revs[i].events; // 获取事件类型
            int fd = revs[i].data.fd; // 获取文件描述符
            if (events & EPOLLIN) // 可读事件
            {
                if (fd == _listsocket_ptr->Fd()) // 如果是监听套接字
                {
                    Accepter(); // 接受新连接
                }
                else
                {
                    // 处理普通套接字的可读事件
                    Recver(fd);
                }
            }
            else
            {}
        }
    }

    // 启动服务器
    void Start()
    {
        // 将监听套接字添加到 epoll 中进行事件监听
        _epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, _listsocket_ptr->Fd(), EPOLLIN);
        struct epoll_event temp[num]; // 创建事件数组
        while(true)
        {
            int n = _epoller_ptr->EpollerWait(revs, num); // 等待事件发生
            if (n > 0)
            {
                // 有事件就绪
                Dispatcher(temp, n); // 分发事件
            }
            else if (n == 0)
            {
                lg(Info, "time out ..."); // 超时日志
            }
            else
            {
                lg(Error, "epll wait error"); // 错误日志
            }
        }
    }

    // 析构函数,关闭监听套接字
    ~EpollServer()
    {
        _listsocket_ptr->Close(); // 关闭监听套接字
    }

private:
    shared_ptr<Sock> _listsocket_ptr; // 监听套接字指针
    shared_ptr<Epoller> _epoller_ptr; // epoller 指针
    uint16_t _port; // 服务器端口
};

Epoll.hpp
#pragma once

#include <cerrno>
#include <cstring>
#include <sys/epoll.h>

class Epoller
{
    static const int size = 128;

public:
    Epoller()
    {
        _epfd = epoll_create(size);
        if (_epfd == -1)
        {}
        else
        {
            cout<<"epoll_create success "<<_epfd<<endl;
        }
    }
    int EpollerWait(struct epoll_event revents[], int num)
    {
        int n = epoll_wait(_epfd, revents, num, -1);
        return n;
    }
    int EpllerUpdate(int oper, int sock, uint32_t event)
    {
        int n = 0;
        if (oper == EPOLL_CTL_DEL)
        {
            n = epoll_ctl(_epfd, oper, sock, nullptr);
            if (n != 0)
            {
				cout<<"epoll_ctl error"<<endl;
            }
        }
        else
        {
            struct epoll_event ev;
            ev.events = event;
            ev.data.fd = sock;

            n = epoll_ctl(_epfd, oper, sock, &ev);
            if (n != 0)
            {
                cout<<"epoll_ctl error"<<endl;
            }
        }
        return n;
    }
    ~Epoller()
    {
        if (_epfd >= 0)
            close(_epfd);
    }

private:
    int _epfd;
    int _timeout{1000};
};
3.4.epoll的优点
  • 高性能:相比于 select 和 poll,epoll 在处理大量文件描述符时性能更优,尤其是在有很多文件描述符处于非活动状态时。epoll
    使用内核中的红黑树和链表来管理文件描述符,能够快速地插入、删除和查找事件。
  • 支持边缘触发和水平触发:epoll 支持两种工作模式:水平触发(Level Triggered, LT):默认模式,只有当文件描述符处于可读或可写状态时,epoll_wait 才会返回。边缘触发(Edge Triggered,
    ET):只有在状态改变时才会通知,这意味着你需要在事件发生时一次性读取所有数据,这种模式更加高效,但需要开发者小心处理。
  • 不限制文件描述符数量:epoll 可以处理的文件描述符数量大于 select 的 1024 限制,具体数量仅受系统资源限制。
  • 减少上下文切换:epoll 通过在内核中管理事件,减少了用户空间和内核空间之间的切换,从而提高了性能。

三.LT和ET

epoll 的 LT 和 ET 模式解析

epoll 提供了两种工作模式:水平触发(Level Triggered, LT)和边缘触发(Edge Triggered, ET)。这两种模式适用于不同的场景,epoll默认是LT。

1. 水平触发(LT)

  • 工作方式
    • 在 LT 模式下,只要文件描述符处于可读或可写状态,epoll_wait 就会返回该文件描述符。
    • 如果文件描述符有未读的数据,epoll_wait多次通知,即使在没有新数据到达的情况下,只要状态满足条件,它都会返回支持阻塞读写和非阻塞读写

2. 边缘触发(ET)

  • 工作方式

    • 在 ET 模式下,epoll_wait 只在状态变化时通知事件。例如,文件描述符从不可读变为可读时,才会触发。
    • 一旦事件被触发,开发者需要尽可能地读取所有可用数据,直到 EAGAIN 错误出现,只支持非阻塞的读写。每次通知都必须把本轮的数据全取走,必须非阻塞读。
  • LT 适用场景

    • 简单的应用场景,或者对性能要求不高的情况。
    • 需要处理大量连接但不要求高并发性能的应用。
  • ET 适用场景

    • 高性能网络服务,处理大量并发连接的场景,如高负载的 Web 服务器。
    • 需要尽量减少系统调用次数,提升性能的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/882333.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java 入门指南:JVM(Java虚拟机)—— Java 类加载器详解

类加载器 类加载器&#xff08;Class Loader&#xff09;是 Java 虚拟机&#xff08;JVM&#xff09;的一部分&#xff0c;它的作用是将类的字节码文件&#xff08;.class 文件&#xff09;从磁盘或其他来源加载到 JVM 中。类加载器负责查找和加载类的字节码文件&#xff0c;并…

BEV学习--Nuscenes数据集解读

一、Nuscenes数据集简介 Nuscenes数据的采集来自不同城市的1000个场景中&#xff0c;采集车上配备了完善的传感器&#xff0c;包括6个相机&#xff08;CAM&#xff09;、1个激光雷达&#xff08;LIDAR&#xff09;、5个毫米波雷达&#xff08;RADAR&#xff09;、IMU和GPS&…

论文(六):Fire-Net: A Deep Learning Framework for Active Forest Fire Detection

文章目录 1.Introduction2.Study Area2.1Landsat-8 Dataset2.2Inventory data 3.Methodology3.1Image Pre-processing3.2Proposed Deep Learning Architecture (Fire-Net)3.2.1Convolution Layers3.2.2 Evaluation Indices/methods or accuracy assessment. 4.Results4.1 Austr…

新一代图像生成E2E FT:深度图微调突破

文章地址&#xff1a;Fine-Tuning Image-Conditional Diffusion Models is Easier than You Think 项目主页&#xff1a;https://gonzalomartingarcia.github.io/diffusion-e2e-ft/ 代码地址&#xff1a;https://github.com/VisualComputingInstitute/diffusion-e2e-ft 机构&am…

SpringBoot 整合 apache fileupload 轻松实现文件上传与下载(通用版)

我们以Thymeleaf页面模板引擎为例&#xff0c;简单介绍利用 apache fileupload 工具实现文件上传的功能。 2.1、添加相关依赖包 首先创建一个基础的 Spring Boot 项目&#xff0c;并引入相关的依赖包。 2.2、添加相关配置参数 2.3、文件上传示例 对应文件上传的Controller类&…

传知代码-基于多尺度动态卷积的图像分类

代码以及视频讲解 本文所涉及所有资源均在传知代码平台可获取 概述 在计算机视觉领域&#xff0c;图像分类是非常重要的任务之一。近年来&#xff0c;深度学习的兴起极大提升了图像分类的精度和效率。本文将介绍一种基于动态卷积网络&#xff08;Dynamic Convolutional Netw…

计算机网络17——IM聊天系统——客户端核心处理类框架搭建

目的 拆开客户端和服务端&#xff0c;使用Qt实现客户端&#xff0c;VS实现服务端 Qt创建项目 Qt文件类型 .pro文件&#xff1a;配置文件&#xff0c;决定了哪些文件参与编译&#xff0c;怎样参与编译 .h .cpp .ui&#xff1a;画图文件 Qt编码方式 Qt使用utf-8作为编码方…

【delphi】正则判断windows完整合法文件名,包括路径

在 Delphi 中&#xff0c;可以使用正则表达式来检查 Windows 文件名称或路径是否合法。合法的文件名和路径要求符合以下几点&#xff1a; 禁止的字符&#xff1a;文件名和路径不能包含以下字符&#xff1a;<, >, :, ", /, \, |, ?, *。文件名不能以空格或点结束。…

idea多模块启动

文章目录 idea多模块启动2018版本的idea2019版本的idea idea多模块启动 2018版本的idea 1.首先看一下view> Tool Windows下有没有Run Dashboard 如果有&#xff0c;点击一下底部的窗口就会出现 如果不存在&#xff0c;执行下一步 2.查看自己项目的工作空间位置 点击 File&…

Java中的事件(动作监听-ActionListener)

&#xff08;一&#xff09;、ActionListener接口 ActionListener接口用于处理用户界面上的动作事件&#xff0c;例如&#xff1a;按钮点击、菜单选择等。实现ActionListener接口需要重写actionPerformed(ActionEvent e)方法&#xff0c;该方法会在动作发生时被调用。 &#…

Android WebView H5 Hybrid 混和开发

对于故乡&#xff0c;我忽然有了新的理解&#xff1a;人的故乡&#xff0c;并不止于一块特定的土地&#xff0c;而是一种辽阔无比的心情&#xff0c;不受空间和时间的限制&#xff1b;这心情一经唤起&#xff0c;就是你已经回到了故乡。——《记忆与印象》 前言 移动互联网发展…

Python | Leetcode Python题解之第415题字符串相加

题目&#xff1a; 题解&#xff1a; class Solution:def addStrings(self, num1: str, num2: str) -> str:res ""i, j, carry len(num1) - 1, len(num2) - 1, 0while i > 0 or j > 0:n1 int(num1[i]) if i > 0 else 0n2 int(num2[j]) if j > 0 e…

Dify创建自定义工具,调用ASP.NET Core WebAPI时的注意事项(出现错误:Reached maximum retries (3) for URL ...)

1、要配置Swagger using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models;var builder WebApplication.CreateBuilder(args);builder.Services.AddCors(options > {options.AddPolicy("AllowSpecificOrigin",builder > builder.WithOrigins("…

SpringSecurity6.x整合手机短信登录授权

前言&#xff1a;如果没有看过我的这篇文章的Springboot3.x.x使用SpringSecurity6(一文包搞定)_springboot3整合springsecurity6-CSDN博客需要看下&#xff0c;大部分多是基于这篇文章的基础上实现的。 明确点我们的业务流程&#xff1a; 需要有一个发送短信的接口&#xff0…

【C++】10道经典面试题带你玩转二叉树

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:C ⚙️操作环境:Leetcode/牛客网 目录 一.根据二叉树创建字符串 二.二叉树的层序遍历 三.二叉树的层序遍历 II 四.二叉树的最近公共祖先 五.二叉搜索树与双向链表 六.从前序与中序遍历序列构造二叉树 七.从中序与后序遍历…

基于yolov8的无人机检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv8的无人机检测系统是一项前沿技术&#xff0c;结合了YOLOv8深度学习模型的强大目标检测能力与无人机的灵活性。YOLOv8作为YOLO系列的最新版本&#xff0c;在检测精度和速度上均有显著提升&#xff0c;特别适用于复杂和高动态的场景。 该系统通过捕获实…

【QML 基础】QML ——描述性脚本语言,用于用户界面的编写

文章目录 1. QML 定义2. QML 1. QML 定义 &#x1f427; QML全称为Qt Meta-Object Language&#xff0c;QML是一种描述性的脚本语言&#xff0c;文件格式以.qml结尾。支持javascript形式的编程控制。QML是Qt推出的Qt Quick技术当中的一部分&#xff0c;Qt Quick是 Qt5中用户界…

C++笔记---set和map

1. 序列式容器与关联式容器 前面我们已经接触过STL中的部分容器如&#xff1a;string、vector、list、deque、array、forward_list等&#xff0c;这些容器统称为序列式容器&#xff0c;因为逻辑结构为线性序列的数据结构&#xff0c;两个位置存储的值之间一般没有紧密的关联关…

U盘格式化了怎么办?这4个工具能帮你恢复数据。

如果你思维U盘被格式化了&#xff0c;也不用太过担心&#xff0c;其实里面的数据并没有被删除&#xff0c;只是被标记为了可覆盖的状态。只要我们及时采取正确的数据恢复措施&#xff0c;就有很大的机会可以将数据找回。比如使用专业得的数据恢复软件&#xff0c;我也可以跟大家…

缓存的思考与总结

缓存的思考与总结 什么是缓存缓存命中率数据一致性旁路模式 Cache aside双写模式直写模式 write through异步写 Write Behind 旁路和双写 案例 新技术或中间的引入&#xff0c;一定是解决了亟待解决的问题或是显著提升了系统性能&#xff0c;并且这种改变所带来的增幅&#xff…