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 模型:
- 阻塞 I/O:在阻塞 I/O 模型中,当进程请求 I/O 操作时,它会被挂起,直到该操作完成。这种方式进程在等待 I/O 的同时不能做其他的事。
- 非阻塞 I/O:非阻塞 I/O 允许进程发出 I/O 请求后立即返回,进程可以继续执行其他操作。为了检查 I/O 是否完成,进程需要通过轮询方式不断检查状态,所以也常称为非阻塞轮询IO,这种方式可以减少等待时间,但是在高频率轮询时很消耗cpu资源。
- I/O 多路复用:I/O 多路复用通过使用 select、poll 或 epoll 等系统调用,使一个进程/线程能够同时监视多个文件描述符。该模型在处理多个并发连接时效率较高,适合网络服务器等应用场景。
- 信号驱动 I/O:在信号驱动 I/O 模型中,进程在发出 I/O 请求后可以继续执行其他任务,操作的完成通过信号通知。这种方式可以减少轮询,但是处理起来较为复杂。
- 异步 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特点和缺点
特点:
- 多路复用:select 允许一个进程同时监控多个文件描述符,等待其中一个或多个变为可读、可写或异常状态。
- 可以处理不同类型的文件描述符:能够监控常规文件、套接字、管道等多种类型的文件描述符。
- 跨平台支持:在许多 Unix-like 操作系统以及 Windows 中都有支持,具有良好的移植性。
缺点:
- 性能瓶颈:当监控的文件描述符数量很大时,select 的性能会显著下降。每次调用 select 都需要遍历所有文件描述符,增加了开销。
- 文件描述符数量限制:select 有文件描述符的数量限制(通常是 1024,取决于类型fd_set的大小),超过这个限制就不能监控更多的描述符。
- 每次调用都需要重置:每次调用 select 都需要重新设置文件描述符集合,这在处理多个连接时会带来额外的开销。
- 无优先级支持:select 不能为不同的文件描述符设置优先级,所有的描述符在监控时是平等的。
- 不适合高并发场景:在高并发情况下(如数千个连接),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; // 客户端IPuint16_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; // 当前位置已被占用,继续查找elsebreak; // 找到可用位置}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的取值:
事件类型 | events 值 | revents 值 | 描述 |
---|---|---|---|
可读事件 | POLLIN | POLLIN | 数据可读,通常指套接字上有数据可读 |
可写事件 | POLLOUT | POLLOUT | 数据可写,通常指套接字可以发送数据 |
错误事件 | POLLERR | POLLERR | 发生错误,需要处理 |
挂起事件 | POLLHUP | POLLHUP | 连接已关闭或挂起 |
非法请求 | POLLNVAL | POLLNVAL | 监控的文件描述符无效 |
超时 | - | - | 当设置的超时时间到达,但没有事件发生 |
2.2.poll特点和缺点
poll的优点:
- 无文件描述符限制:与 select 不同,poll 不受文件描述符数量的限制,因此可以处理更多的连接。
- 简单性:poll 的使用相对select简单,容易理解和实现。
poll的缺点:
- 线性扫描:在事件发生时,poll 会线性扫描所有文件描述符,效率较低。对于大量文件描述符,响应时间可能变长。
- 性能问题:每次调用 poll 时,都需要传递整个文件描述符数组,性能降低
- 不支持边缘触发: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); // 接收客户端连接,返回新socketif (sock < 0) return; // 如果返回值小于0,表示接收失败// 将新连接的socket放入_event_fds数组int pos = 1;for (; pos < fd_num_max; pos++) // 查找空闲的数组位置{if (_event_fds[pos].fd != defaultfd) // 如果当前文件描述符已经被占用,继续寻找continue;elsebreak; // 找到空闲位置退出循环}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; // 自定义封装的监听socketuint16_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:// 构造函数,初始化端口、监听套接字和 epollerEpollServer(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
会多次通知,即使在没有新数据到达的情况下,只要状态满足条件,它都会返回,支持阻塞读写和非阻塞读写。
- 在 LT 模式下,只要文件描述符处于可读或可写状态,
2. 边缘触发(ET)
-
工作方式:
- 在 ET 模式下,
epoll_wait
只在状态变化时通知事件。例如,文件描述符从不可读变为可读时,才会触发。 - 一旦事件被触发,开发者需要尽可能地读取所有可用数据,直到
EAGAIN
错误出现,只支持非阻塞的读写。每次通知都必须把本轮的数据全取走,必须非阻塞读。
- 在 ET 模式下,
-
LT 适用场景:
- 简单的应用场景,或者对性能要求不高的情况。
- 需要处理大量连接但不要求高并发性能的应用。
-
ET 适用场景:
- 高性能网络服务,处理大量并发连接的场景,如高负载的 Web 服务器。
- 需要尽量减少系统调用次数,提升性能的应用。
相关文章:

Linux高级I/O:多路转接模型
目录 一.常见的IO模型介绍二.多路转接I/O1.select1.1.函数解析1.2. select特点和缺点1.3.基于 select 的多客户端网络服务器 2.poll2.1.poll函数解析2.2.poll特点和缺点2.3.基于poll的tcp服务器 3.epoll3.1.系列函数解析3.2.epoll原理解析2.3.基于 select 的多客户端网络服务器…...

MongoDB Limit 与 Skip 方法
MongoDB Limit 与 Skip 方法 MongoDB 是一个流行的 NoSQL 数据库,它提供了灵活的数据存储和强大的查询功能。在处理大量数据时,我们常常需要限制返回的结果数量或者跳过一部分结果,这时就可以使用 MongoDB 的 limit 和 skip 方法。 Limit 方…...

【2025】中医药健康管理小程序(安卓原生开发+用户+管理员)
博主介绍: ✌我是阿龙,一名专注于Java技术领域的程序员,全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师,我在计算机毕业设计开发方面积累了丰富的经验。同时,我也是掘金、华为云、阿里云、InfoQ等平台…...

VulnHub-Bilu_b0x靶机笔记
Bilu_b0x 靶机 概述 Vulnhub 的一个靶机,包含了 sql 注入,文件包含,代码审计,内核提权。整体也是比较简单的内容,和大家一起学习 Billu_b0x.zip 靶机地址: https://pan.baidu.com/s/1VWazR7tpm2xJZIGUS…...

Python | Leetcode Python题解之第421题数组中两个数的最大异或值
题目: 题解: class Trie:def __init__(self):# 左子树指向表示 0 的子节点self.left None# 右子树指向表示 1 的子节点self.right Noneclass Solution:def findMaximumXOR(self, nums: List[int]) -> int:# 字典树的根节点root Trie()# 最高位的二…...

如何将普通Tokenizer变成Fast Tokenizer
现在的huggingface库里面Tokenizer有两种,一种就是普通的,另一种是fast的。fast和普通的区别就是fast使用rust语言编写,在处理大量文本的时候会更快。我自己测试的时候单一一句的话fast要比普通的慢一些,当量叠上来,到…...

联合复现!考虑最优弃能率的风光火储联合系统分层优化经济调度!
前言 目前,尽管不断地追逐可再生能源全额消纳方式,大幅减小弃风弃光电量,但是若考虑风电、光伏发电的随机属性,全额消纳可能造成电网峰谷差、调峰难度及调峰调频等辅助服务费用的剧增,引起电网潜在运行风险。因此&…...

Vue开发前端图片上传给java后端
前端效果图 图片上传演示 1 前端代码 <template><div><!-- 页面标题 --><h1 class"page-title">图片上传演示</h1><div class"upload-container"><!-- 使用 van-uploader 组件进行文件上传,v-model 绑…...

react hooks--useCallback
概述 useCallback缓存的是一个函数,主要用于性能优化!!! 基本用法 如何进行性能的优化呢? useCallback会返回一个函数的 memoized(记忆的) 值;在依赖不变的情况下,多次定义的时候,返回的值是…...

828华为云征文|华为云Flexus X实例docker部署最新Appsmith社区版,搭建自己的低代码平台
828华为云征文|华为云Flexus X实例docker部署最新Appsmith社区版,搭建自己的低代码平台 华为云最近正在举办828 B2B企业节,Flexus X实例的促销力度非常大,特别适合那些对算力性能有高要求的小伙伴。如果你有自建MySQL、Redis、Ng…...

webservice cxf框架 jaxrs jaxws spring整合 接口测试方法 wsdl报文详解 springboot整合 拦截器 复杂参数类型
webservice cxf框架 jaxrs jaxws spring整合 【java进阶教程之webservice深入浅出【黑马程序员】】 webservice接口测试方法 【SoapUI让你轻松玩转WebService接口测试【特斯汀学院】】 webservice wsdl报文详解 【webservice - 尚硅谷周阳新视频】 webservice springbo…...

2024AI做PPT软件如何重塑演示文稿的创作
现在AI技术的发展已经可以帮我们写作、绘画,最近我发现了不少ai做ppt的工具!不体验不知道,原来合理使用AI工具可以有效的帮我们进行一些办公文件的编写,提高了不少工作效率。如果你也有这方面的需求就接着往下看吧。 1.笔灵AIPPT…...

谷神后端list转map
list转map /*** list2map* list转map:支持全量映射、单字段映射。* * param $list:list:列表。* param $key:string:键。* param $field:string:值字段域。** return map**/ #function list2map($list, $key, $field)#if ($vs.util.isList($list) and $vs.util.is…...

Java集合(Map篇)
一.Map a.使用Map i.键值(key-value)映射表的数据结构,能高效通过key快速查找value(元素)。 ii.Map是一个接口,最常用的实现类是HashMap。 iii.重复放入k-v不会有问题,但是一个…...

VUE3配置路由(超级详细)
第一步创建vue3的项目...

【笔记】机器学习算法在异常网络流量监测中的应用
先从一些相对简单的综述类看起,顺便学学怎么写摘要相关工作的,边译边学 机器学习算法在异常网络流量监测中的应用 原文:Detecting Network Anomalies in NetFlow Traffic with Machine Learning Algorithms Authors: Quc Vo, Philippe Ea, Os…...

江协科技STM32学习- P15 TIM输出比较
🚀write in front🚀 🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝…...

使用python-pptx批量删除备注:清除PPT文档中的所有备注信息
哈喽,大家好,我是木头左! 在制作和分享PPT时,经常需要添加一些注释或备注来帮助观众更好地理解内容。然而,有时候需要将这些备注从PPT中移除,以保持演示的简洁性。幸运的是,可以使用python-pptx库来实现这一目标。本文将详细介绍如何使用python-pptx批量删除PPT中的备注…...

RTX NVIDIA 3090卡配置对应pytorch,CUDA版本,NVIDIA驱动过程及问题整理
买了两块3090卡闲置很长时间了,之前tf 1.12.0版本用习惯了不想转工具。这段时间闲下来转了之后有些环境不适配,在雷神帮助下安装完毕,虽然出了点怪东西,整体还好。 原环境CUDA为11.4 其他配置如下 之前conda install的pytorch实为…...
【Verilog学习日常】—牛客网刷题—Verilog快速入门—VL21
根据状态转移表实现时序电路 描述 某同步时序电路转换表如下,请使用D触发器和必要的逻辑门实现此同步时序电路,用Verilog语言描述。 电路的接口如下图所示。 输入描述: input A , input clk , …...

【深度】为GPT-5而生的「草莓」模型!从快思考—慢思考到Self-play RL的强化学习框架
原创 超 超的闲思世界 2024年09月11日 19:17 北京 9月11日消息,据外媒The Information昨晚报道,OpenAI的新模型「草莓」(Strawberry),将在未来两周内作为ChatGPT服务的一部分发布。 「草莓」项目是OpenAI盛传已久的…...

【编程底层原理】Java常用读写锁的使用和原理
一、引言 在Java的并发世界中,合理地管理对共享资源的访问是至关重要的。读写锁(ReadWriteLock)正是一种能让多个线程同时读取共享资源,而写入资源时需要独占访问的同步工具。本文将带你了解读写锁的使用方法、原理以及它如何提高…...

自恢复保险丝SMD1206B005TF在电路中起什么作用
自恢复保险丝SMD1206B005TF在电路中起到过流保护的作用。 自恢复保险丝,也称为正温度系数(PTC)热敏电阻,是一种能够在电流超过预设值时自动断开电路,并在故障排除后自动恢复的元件。这种保险丝的核心材料是高分子聚合…...

2024年躺平,花大半年的时间,就弄了这一件事儿:《C++面试真题宝典》
今年,是我的第3个躺平年,躺得我四肢都快蜕化了... 为了让一切都变得舒服,我决定主动地去做些什。 在我生命的一个不起眼却意义非凡的角落,我与C结下了不解之缘。这份热爱,如同一位老友,陪伴我度过了无数个…...

PHP基础语法讲解
大家好,我是程序员小羊! 前言: PHP(Hypertext Preprocessor)是一种常用于网页开发的服务器端脚本语言,易于学习并且与 HTML 紧密结合。以下是 PHP 的基础语法详细讲解。 1. PHP 基础结构 1.1 PHP 脚本结…...

【论文速看】DL最新进展20240923-长尾综述、人脸防伪、图像分割
目录 【长尾学习】【人脸防伪】【图像分割】 【长尾学习】 [2024综述] A Systematic Review on Long-Tailed Learning 论文链接:https://arxiv.org/pdf/2408.00483 长尾数据是一种特殊类型的多类不平衡数据,其中包含大量少数/尾部类别,这些类…...

device靶机详解
靶机下载地址 https://www.vulnhub.com/entry/unknowndevice64-1,293/ 靶机配置 主机发现 arp-scan -l 端口扫描 nmap -sV -A -T4 192.168.229.159 nmap -sS -Pn -A -p- -n 192.168.229.159 这段代码使用nmap工具对目标主机进行了端口扫描和服务探测。 -sS:使用…...

十四、SOA(在企业中的应用场景)
在企业中,**SOA(面向服务架构)**被广泛应用于多个场景,帮助企业提高灵活性、效率和业务响应能力。SOA通过分解企业系统中的功能模块,以服务的形式进行封装和集成,支持跨平台、跨系统的协同工作。以下是SOA在…...

单片机与PIC的区别:多方面对比
单片机与PIC的区别:多方面对比 在现代电子产品的设计中,单片机和PIC都是不可或缺的控制器。尽管它们在功能上有许多相似之处,但在设计、应用、优势和劣势等方面却有显著区别。今天,我们就来详细对比一下单片机和PIC。 1. 定义与…...

python新手的五个练习题
代码 # 1. 定义一个变量my_Number,将其设置为你的学号,然后输出到终端。 my_Number "20240001" # 假设你的学号是20240001 print("学号:", my_Number) # 2. 计算并输出到终端:两个数(例如3和5)的和、差、乘积和商。 num1 3 num2 5 print(&…...