目录
对比总结select, poll, epoll之间的优点和缺点(重要, 面试中常见).
引言
在互联网的黄金时代,网络服务的需求日益增长,如何高效地处理成千上万的并发连接成为服务器端编程的一大挑战。传统的多线程模型在应对高并发场景时显得力不从心,资源消耗巨大且难以维护。在这种背景下,I/O多路转接技术应运而生,其中select函数作为这一技术的代表,以其独特的优势在服务器端编程领域占据了一席之地。本文将带您深入了解select,探讨其在处理多路I/O时的原理与应用,以及如何在现代网络编程中发挥其重要作用。
本文核心:
1.初识各种IO模型
2.详细讲解多路转接模型
背景知识
IO除了数据在本地的拷贝,还包括网络层面的IO。网络层面的IO就是数据的接收和发送,而IO的效率是十分重要的。
在网络IO中,其实更多的时间我们并不是在进行数据的拷贝,而是在进行数据的等待---即等待读写资源就绪。
为了让IO的效率更加高效,我们将对等待的条件进行优化,从而使得IO效率提升。
其实scanf也一直在等,等待键盘输入资源就绪
读等:接收缓冲区是否有数据
写等:接收缓冲区是否有数据
为什么要写多线程:可以同时等待多个任务,那么IO等待是并行的,间接提高IO效率。
五中IO模型
我们用钓鱼举例。钓鱼的过程其实是:钓鱼 = 等鱼 + 上鱼(鱼咬钩)。我们以此为模型,进行IO知识的讲解。
在湖边,有这么几个人在钓鱼:张三 李四 王五 赵六 田七
张三:一直盯着鱼漂、钓上鱼来,才去干别的,否则一直盯着鱼漂 --- 阻塞式IO
李四:刷会抖音,再看一眼鱼漂,监测到鱼漂没有鱼,直接返回,不阻塞 ---- 非阻塞轮询IO
王五:往鱼漂上放一个铃铛,把鱼竿放在那里,直接不管了,直到铃铛响了,才回去收杆。 ---- 信号驱动式IO
赵六:放了100个鱼竿,赵六来回踱步,检测哪个鱼竿咬钩。 ---- 多路复用/多路转接
田七:喜欢鱼,找了秘书小王,小王去钓鱼,田七回到公司开会。小王钓上鱼之后,交给田七。
田七没有等鱼,所以田七不是严格的钓鱼者,而是钓鱼任务的分发者。----异步IO
田七在开会,没有钓鱼而这个小王一般就是OS!。田七给OS发配1M大小的缓冲区支配资源,以及一个可以通知田七的方式。当OS有数据之后,就把数据放在缓冲区中。田七只是用数据,并不参与IO。
在IO上,阻塞与非阻塞没有区别,因为等的时间是一样的,但是对于整个工程而言,由于非阻塞干了其他的事情,所以非阻塞的效率更高。
IO方式中,我们最常使用(效率最高)的是多路转接。为什么不采用田七的方式:代码复杂、IO逻辑混乱
同步VS异步
同步VS异步:最大的区别是就看你有没有参与IO:有没有等,有没有拷贝
异步IO的本质是:不参与IO,只是发起IO。
这个同步IO和线程同步没有关系,就像是老婆和老婆饼。
线程同步是借助条件变量wait实现了,只不过是一种谁先谁后的关系。
取代异步IO的IO方式
-
同步I/O:
- 阻塞I/O: 在发起I/O请求后,线程会被阻塞,直到I/O操作完成。这种方式简单但效率低下,因为线程在等待I/O完成期间无法执行其他任务。
- 非阻塞I/O: 线程在发起I/O请求后可以立即返回,但需要定期轮询检查I/O操作是否完成。这种方式避免了线程阻塞,但轮询会消耗CPU资源。
-
多路复用I/O (Multiplexed I/O):
- select: 允许程序同时监视多个文件描述符,等待一个或多个变得“就绪”(可读、可写或有异常)。
- poll: 类似于select,但解决了select的一些限制,如文件描述符数量上限。
- epoll (Linux特有): 提供了更高效的I/O事件通知机制,允许注册事件并仅在事件发生时才通知应用程序。
- kqueue (BSD特有): 类似于epoll,但用于BSD系统。
-
信号驱动I/O (Signal-Driven I/O):
- 允许I/O操作由内核信号通知完成,而不是通过轮询或阻塞。当文件描述符就绪时,内核会发送一个SIGIO信号。
-
基于线程/进程的I/O:
- 线程池: 使用一组预先创建的线程来处理I/O操作,可以减少线程创建和销毁的开销。
- 进程池: 类似于线程池,但是使用进程而不是线程。
图示IO模型
阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作


非阻塞IO
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

我们可以对flag进行参数设置。但是一个个设置太麻烦,存在更加通用的做法。
I/O多路转接之select
初识select
前面的函数在IO的过程中,既负责等,也负责拷贝,而后面这个多路转接则提供了新的系统调用,将等与拷贝进行分离
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout)
参数解释:





SelectServer实现
主函数
#include "SelectServer.hpp"
#include <memory>
int main()
{
// std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
服务器功能的实现
#pragma once
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#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;
// std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter() //连接管理器
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport = 0;
int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
if (sock < 0) return;
lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);
// sock -> fd_array[]
int pos = 1;
for (; pos < fd_num_max; pos++) // 第二个循环:将连接维护到数组中(两种设计模式:1.fd = 下标,fd具有独一无二的性质 2.找到为空的位置,直接插入)
{
if (fd_array[pos] != defaultfd)
continue;
else
break;
}
if (pos == fd_num_max)
{
lg(Warning, "server is full, close %d now!", sock);
close(sock);
}
else
{
fd_array[pos] = sock;
//PrintFd();
// TODO
}
}
void Recver(int fd, int pos)
{
// demo
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)
{
lg(Info, "client quit, me too, close fd is : %d", fd);
close(fd);
fd_array[pos] = defaultfd; // 这里本质是从select中移除
}
else
{
lg(Warning, "recv error: fd is : %d", fd);
close(fd);
fd_array[pos] = defaultfd; // 这里本质是从select中移除
}
}
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)) //借助辅助数组传递fd信息。
{
if (fd == _listensock.Fd())
{
Accepter(); // 连接管理器
}
else // non listenfd
{
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++) // 第一次循环:更新maxfd 将辅助数组的fd_array[i]加入到rfds中
{
if (fd_array[i] == defaultfd)
continue;
FD_SET(fd_array[i], &rfds);
if (maxfd < fd_array[i])
{
maxfd = fd_array[i];
lg(Info, "max fd update, max fd is: %d", maxfd);
}
}
// accept?不能直接accept!检测并获取listensock上面的事件,新连接到来,等价于读事件就绪
// struct timeval timeout = {1, 0}; // 输入输出,可能要进行周期的重复设置
struct timeval timeout = {0, 0}; 输入输出,可能要进行周期的重复设置
// 如果事件就绪,上层不处理,select会一直通知你!
// select告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞
// rfds: 输入输出型参数。 1111 1111 -> 0000 0000
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout /*nullptr*/);
switch (n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cerr << "select error" << endl;
break;
default:
// 有事件就绪了,TODO
cout << "get a new link!!!!!" << endl;
Dispatcher(rfds); // 就绪的事件和fd你怎么知道只有一个呢???
break;
}
}
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_num_max]; // 辅助数组, 用户维护的!
// int wfd_array[fd_num_max];
};
代码剖析:
这段代码实现了一个基于 `select` 的服务器,用于处理多个客户端连接。以下是对代码的详细剖析:
### 头文件和命名空间
```cpp
#pragma once
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"
using namespace std;
```
- `#pragma once`:防止头文件被多次包含。
- 包含了必要的头文件,如 `iostream`、`sys/select.h`、`sys/time.h` 以及自定义的 `Socket.hpp`。
- 使用 `std` 命名空间。
### 常量定义
```cpp
static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;
```
- `defaultport`:默认端口号为 8888。
- `fd_num_max`:最大文件描述符数量,由 `fd_set` 的大小决定。
- `defaultfd`:默认的文件描述符值为 -1,表示无效或未使用。
### SelectServer 类
#### 构造函数
```cpp
SelectServer(uint16_t port = defaultport) : _port(port)
{
for (int i = 0; i < fd_num_max; i++)
{
fd_array[i] = defaultfd;
}
}
```
- 初始化监听端口 `_port`。
- 将 `fd_array` 数组中的所有元素初始化为 `defaultfd`。
#### Init 方法
```cpp
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
```
- 创建套接字并绑定到指定端口,然后开始监听。
#### Accepter 方法
```cpp
void Accepter()
{
std::string clientip;
uint16_t clientport = 0;
int sock = _listensock.Accept(&clientip, &clientport);
if (sock < 0) return;
lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);
int pos = 1;
for (; pos < fd_num_max; pos++)
{
if (fd_array[pos] != defaultfd)
continue;
else
break;
}
if (pos == fd_num_max)
{
lg(Warning, "server is full, close %d now!", sock);
close(sock);
}
else
{
fd_array[pos] = sock;
}
}
```
- 接受新的客户端连接。
- 如果连接成功,将其添加到 `fd_array` 中。如果服务器已满,则关闭新连接。
#### Recver 方法
```cpp
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)
{
lg(Info, "client quit, me too, close fd is : %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
else
{
lg(Warning, "recv error: fd is : %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
}
```
- 从指定的文件描述符读取数据。
- 根据读取结果处理不同的情况:正常读取、客户端断开连接、读取错误。
#### Dispatcher 方法
```cpp
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 // non listenfd
{
Recver(fd, i); // 处理客户端发送的数据
}
}
}
}
```
- 遍历 `fd_array`,检查哪些文件描述符有事件就绪。
- 如果监听套接字有事件就绪,调用 `Accepter` 处理新的连接。
- 如果其他文件描述符有事件就绪,调用 `Recver` 处理客户端数据。
#### Start 方法
```cpp
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];
lg(Info, "max fd update, max fd is: %d", maxfd);
}
}
struct timeval timeout = {0, 0}; // 不阻塞等待
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;
}
}
}
```
- 主循环:不断调用 `select` 等待文件描述符的事件。
- 更新 `rfds` 集合和 `maxfd`。
- 根据 `select` 返回值处理不同的事件情况。
- 调用 `Dispatcher` 处理就绪的文件描述符。
#### 析构函数
```cpp
~SelectServer()
{
_listensock.Close();
}
```
- 关闭监听套接字。
socket就绪条件
读就绪
socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
写就绪
select的缺陷
连接获取上来之后,不处理IO事件,那么会一直提醒你IO就绪。
select会检测文件描述符在内核中的数据信息,只有将数据全部取走才不会重复监听,而read读取一部分,没有全部读取完成,select仍然会因为这个fd而暂停等待。
这其其实牵扯到了内核的数据结构,比如struct file以及文件缓冲区。
需要注意的是:
1.在内核中,每个文件描述符都有自己的缓冲区。因此在后续的Server的完善中,在最终的reactor版本,将采用 1 fd 1缓冲区的策略去实现。
2.fd_set最大1024个(系统调用取决于系统的实现)
3.由于牵扯到不同函数中文件描述符的传递,所以使用辅助数组(上层维护)
select缺陷:
5.用户层需要额外去维护数组。
poll服务器
为了解决select的短板---poll的方式多路转接。
poll解决了select:1.输入输出型参数重复 2.导致每次都要对fd进行重置的问题。
在用户区,仍然需要手动维护数组,只不过数组变成了pollfd【size】。


poll的用法
由于将事件设置进位图中,所以采用了外部 按位与& 的用法。
同样,事件就绪之后,我们需要采用手动设置到维护的数组中的方式,让后续的等待去监视连接是否就绪。
但是poll仍然避免不了大量的遍历数组的操作,因此性能最优的epoll产生了
pollserver
#pragma once
#include <iostream>
#include <poll.h>
#include <sys/time.h>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8888;
static const int fd_num_max = 64;
int defaultfd = -1;
int non_event = 0;
class PollServer
{
public:
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 = non_event;
_event_fds[i].revents = non_event;
// std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter()
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport = 0;
int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
if (sock < 0) return;
lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);
// sock -> fd_array[]
int pos = 1;
for (; pos < fd_num_max; pos++) // 第二个循环
{
if (_event_fds[pos].fd != defaultfd)
continue;
else
break;
}
if (pos == fd_num_max)
{
lg(Warning, "server is full, close %d now!", sock);
close(sock);
// 扩容
}
else
{
// fd_array[pos] = sock;
_event_fds[pos].fd = sock;
_event_fds[pos].events = POLLIN;
_event_fds[pos].revents = non_event;
PrintFd();
// TODO
}
}
void Recver(int fd, int pos)
{
// demo
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
if (n > 0)
{
buffer[n] = 0;
cout << "get a messge: " << buffer << endl;
}
else if (n == 0)
{
lg(Info, "client quit, me too, close fd is : %d", fd);
close(fd);
_event_fds[pos].fd = defaultfd; // 这里本质是从select中移除
}
else
{
lg(Warning, "recv error: fd is : %d", fd);
close(fd);
_event_fds[pos].fd = defaultfd; // 这里本质是从select中移除
}
}
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())
{
Accepter(); // 连接管理器
}
else // non listenfd
{
Recver(fd, i);
}
}
}
}
void Start()
{
_event_fds[0].fd = _listensock.Fd();
_event_fds[0].events = POLLIN;
int timeout = 3000; // 3s
for (;;)
{
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:
// 有事件就绪了,TODO
cout << "get a new link!!!!!" << endl;
Dispatcher(); // 就绪的事件和fd你怎么知道只有一个呢???
break;
}
}
}
void PrintFd()
{
cout << "online 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;
}
~PollServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
struct pollfd _event_fds[fd_num_max]; // 数组, 用户维护的!
// struct pollfd *_event_fds;
// int fd_array[fd_num_max];
// int wfd_array[fd_num_max];
};
epoll
按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
句柄是用于操作系统资源的唯一标识符,如文件、设备和管道等。
句柄是一个数值,由操作系统分配给每个打开的资源,应用程序通过句柄来访问这些资源。在Linux系统中,文件描述符(File Descriptor)就是句柄的一种形式,它是一个非负整数,用于表示已打开的文件、网络连接或其它I/O资源。
在Linux中,epoll是一种高效的I/O多路复用机制,它允许程序监视多个文件描述符,以便在某个文件描述符就绪时执行I/O操作。与传统的select和poll机制相比,epoll能够处理更多的文件描述符,且不会因为文件描述符数量的增加而导致性能下降。这是因为epoll使用了一个就绪队列,只有当文件描述符变为就绪状态时,它才会被加入到这个队列中,从而避免了遍历整个文件描述符集合的性能开销。

1.epoll的接口

epoll_wait
返回值:返回已经就绪的events的个数
2.原理
epoll属于在文件管理模块,和poll、select完全不是一个东西!
epoll的核心机制就是三个:
1.红黑树:用来管理fd事件
2.就绪队列:用来存储已经就绪的IO事件。检测就绪队列只需要O(1)的复杂度,
3.回调函数:每个fd对应一个注册的回调函数,负责事件的向上交付,将事件添加到就绪队列。
epoll模型与三个函数的解读
epoll_create函数在Linux系统中用于创建一个epoll实例,这个实例可以用来监控多个文件描述符的I/O事件。当调用epoll_create时,内核会为这个epoll实例分配资源,包括你提到的红黑树和就绪队列,但epoll_create本身并不直接关联一个回调函数。
epoll_create会注册epoll模型,将文件描述符部分与这个模型相关联。
epoll_wait将就绪队列的就绪事件调出来。
epoll_ctl负责对红黑树进行增删查改。
如何看待这个红黑树:这个红黑树就是我们之前用户维护数组
如何看待就绪队列:这就就绪队列存放的就是连续的就绪事件(即取即用)。
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来 的事件.
对比总结select, poll, epoll之间的优点和缺点(重要, 面试中常见).
在Linux系统中,select
、poll
和epoll
是三种用于IO多路复用的系统调用,它们允许程序同时监视多个文件描述符,以便它们中的任何一个准备好进行IO操作时能够得到通知。以下是这三种机制的优点和缺点对比总结:
select
优点:
- 跨平台:
select
是POSIX标准的一部分,因此在多数Unix-like系统中都得到了支持。 - 简单易用:
select
的API相对简单,易于理解和实现。
缺点:
- 文件描述符数量限制:通常最大只能监听1024个文件描述符(依赖于具体实现)。
- 效率低:每次调用
select
都需要重新传入文件描述符集合和超时时间,并且需要在返回时重新遍历整个集合来确定哪些文件描述符就绪。 - 内核态与用户态数据交换开销大:
select
需要复制整个文件描述符集合到内核空间,当监视的文件描述符很多时,这个开销很大。 - 无法直接获取就绪的文件描述符:需要遍历整个文件描述符集合来检查哪些文件描述符就绪。
poll
优点:
- 没有文件描述符数量限制:
poll
没有select
的文件描述符数量限制问题。 - 提供了更丰富的事件类型:
poll
可以监视更多类型的事件。
缺点:
- 效率问题:与
select
类似,poll
也有效率问题,每次调用都需要重复传递整个文件描述符集合,并且返回时需要遍历整个集合。 - 内核态与用户态数据交换开销大:和
select
一样,poll
也存在这个问题。
epoll
优点:
- 没有文件描述符数量限制:
epoll
可以处理大量的文件描述符。 - 高效的文件描述符事件通知:
epoll
使用回调机制,不需要像select
和poll
那样每次调用时都传递文件描述符集合,因此效率更高。 - 边缘触发(ET)模式:
epoll
支持边缘触发,这可以减少事件被触发的次数,提高应用程序的性能。 - 减少数据拷贝:
epoll
通过共享内存来减少用户态和内核态之间的数据拷贝次数。
缺点:
- 仅限于Linux系统:
epoll
是Linux特有的,不是跨平台的。 - 使用稍微复杂:
epoll
的API比select
和poll
稍微复杂一些,需要理解ET和LT(水平触发)模式的区别。
总结来说,epoll
在现代Linux系统中通常是首选的IO多路复用机制,因为它在处理大量文件描述符和提供高效率的事件通知方面有明显的优势。然而,对于需要跨平台支持的应用程序,或者文件描述符数量不是很多的情况下,select
和poll
仍然是可行的选择。在面试中,理解这些优缺点,并根据具体场景选择合适的IO多路复用机制,是展示你深入理解系统编程能力的一个重要方面。
EpollServer
#pragma once
#include "nocopy.hpp"
#include "Log.hpp"
#include <cerrno>
#include <cstring>
#include <sys/epoll.h>
class Epoller : nocopy
{
public:
Epoller()
{
_epfd = epoll_create(11);
if (_epfd == -1)
{
lg(Error, "epoll_create error: %s", strerror(errno));
}
else
{
lg(Info, "epoll_create success: %d", _epfd);
}
}
int EpollerWait(struct epoll_event revents[], int num)
{
int n = epoll_wait(_epfd, revents, num, /*_timeout 0*/ -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)
{
lg(Error, "epoll_ctl delete error!");
}
}
else
{
// EPOLL_CTL_MOD || EPOLL_CTL_ADD
struct epoll_event ev;
ev.events = event; //关注事件的位图
ev.data.fd = sock; // 目前,方便我们后期得知,是哪一个fd就绪了!
n = epoll_ctl(_epfd, oper, sock, &ev);
if (n != 0)
{
lg(Error, "epoll_ctl error!");
}
}
return n;
}
~Epoller() //关闭底层注册的模型
{
if (_epfd >= 0)
close(_epfd);
}
private:
int _epfd;
int _timeout{3000};
};
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "Log.hpp"
#include "nocopy.hpp"
uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "Log.hpp"
#include "nocopy.hpp"
uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);
class EpollServer : public nocopy
{
static const int num = 64;
public:
EpollServer(uint16_t port)
: _port(port),
_listsocket_ptr(new Sock()),
_epoller_ptr(new Epoller())
{
}
void Init()
{
_listsocket_ptr->Socket();
_listsocket_ptr->Bind(_port);
_listsocket_ptr->Listen();
lg(Info, "create listen socket success: %d\n", _listsocket_ptr->Fd());
}
void Accepter() //连接管理器
{
// 获取了一个新连接
std::string clientip;
uint16_t clientport;
int sock = _listsocket_ptr->Accept(&clientip, &clientport);
if (sock > 0)
{
// 我们能直接读取吗?不能
_epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
lg(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);
}
}
// for test
void Recver(int fd)
{
// demo
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
if (n > 0)
{
buffer[n] = 0;
std::cout << "get a messge: " << buffer << std::endl;
// wrirte
std::string echo_str = "server echo $ ";
echo_str += buffer;
write(fd, echo_str.c_str(), echo_str.size());
}
else if (n == 0)
{
lg(Info, "client quit, me too, close fd is : %d", fd);
//细节3:连接关闭,需要从红黑树移除连接
_epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
else
{
lg(Warning, "recv error: fd is : %d", fd);
_epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0);
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 & EVENT_IN)
{
if (fd == _listsocket_ptr->Fd()) //listensock就绪,就意味着连接事件到来。
{
Accepter();
}
else
{
// 其他fd上面的普通读取事件就绪
Recver(fd);
}
}
else if (events & EVENT_OUT)
{
}
else
{
}
}
}
void Start()
{
// 将listensock添加到epoll中 -> listensock和他关心的事件,添加到内核epoll模型中rb_tree.
_epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, _listsocket_ptr->Fd(), EVENT_IN);
struct epoll_event revs[num];
for (;;)
{
int n = _epoller_ptr->EpollerWait(revs, num);
if (n > 0)
{
// 有事件就绪
lg(Debug, "event happened, fd is : %d", revs[0].data.fd);
Dispatcher(revs, n);
}
else if (n == 0)
{
lg(Info, "time out ...");
}
else
{
lg(Error, "epll wait error");
}
}
}
~EpollServer()
{
_listsocket_ptr->Close();
}
private:
std::shared_ptr<Sock> _listsocket_ptr;
std::shared_ptr<Epoller> _epoller_ptr; //已经进行封装,不需要再关心_epfd
uint16_t _port;
};
这段代码实现了一个基于 `epoll` 的服务器,用于处理网络连接和数据接收。以下是对代码的详细剖析:
### 类定义与成员变量
```cpp
class EpollServer : public nocopy
{
static const int num = 64;
public:
EpollServer(uint16_t port)
: _port(port),
_listsocket_ptr(new Sock()),
_epoller_ptr(new Epoller())
{
}
```
- **类继承**:`EpollServer` 继承自 `nocopy`,表示该类不能被拷贝。
- **静态常量**:`num` 是一个静态常量,表示 `epoll` 事件数组的大小。
- **构造函数**:初始化端口号、监听套接字指针和 `epoll` 对象指针。
### 初始化方法
```cpp
void Init()
{
_listsocket_ptr->Socket();
_listsocket_ptr->Bind(_port);
_listsocket_ptr->Listen();
lg(Info, "create listen socket success: %d\n", _listsocket_ptr->Fd());
}
```
- **创建套接字**:调用 `Socket` 方法创建一个套接字。
- **绑定端口**:将套接字绑定到指定端口。
- **监听**:开始监听连接请求。
- **日志记录**:记录成功创建监听套接字的信息。
### 接受连接的方法
```cpp
void Accepter()
{
std::string clientip;
uint16_t clientport;
int sock = _listsocket_ptr->Accept(&clientip, &clientport);
if (sock > 0)
{
_epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
lg(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);
}
}
```
- **接受新连接**:调用 `Accept` 方法接受一个新的客户端连接。
- **更新 `epoll`**:将新的连接套接字添加到 `epoll` 中,并关注其读事件。
- **日志记录**:记录新连接的信息。
### 接收数据的方法
```cpp
void Recver(int fd)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
if (n > 0)
{
buffer[n] = 0;
std::cout << "get a messge: " << buffer << std::endl;
std::string echo_str = "server echo $ ";
echo_str += buffer;
write(fd, echo_str.c_str(), echo_str.size());
}
else if (n == 0)
{
lg(Info, "client quit, me too, close fd is : %d", fd);
_epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
else
{
lg(Warning, "recv error: fd is : %d", fd);
_epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
}
```
- **读取数据**:从文件描述符 `fd` 读取数据到缓冲区 `buffer`。
- **处理数据**:如果读取到的数据长度大于0,则打印消息并回显给客户端。
- **处理连接关闭**:如果读取到的数据长度为0,表示客户端关闭了连接,移除 `epoll` 中的该文件描述符并关闭它。
- **处理错误**:如果读取出错,同样移除 `epoll` 中的该文件描述符并关闭它。
### 事件分发器
```cpp
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 & EVENT_IN)
{
if (fd == _listsocket_ptr->Fd())
{
Accepter();
}
else
{
Recver(fd);
}
}
else if (events & EVENT_OUT)
{
}
else
{
}
}
}
```
- **遍历事件**:遍历所有就绪的事件。
- **处理读事件**:如果是监听套接字有读事件,调用 `Accepter` 方法接受新连接;否则调用 `Recver` 方法处理普通读事件。
- **处理写事件**:目前没有实现写事件的处理逻辑。
- **其他事件**:没有处理其他类型的事件。
### 启动方法
```cpp
void Start()
{
_epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, _listsocket_ptr->Fd(), EVENT_IN);
struct epoll_event revs[num];
for (;;)
{
int n = _epoller_ptr->EpollerWait(revs, num);
if (n > 0)
{
lg(Debug, "event happened, fd is : %d", revs[0].data.fd);
Dispatcher(revs, n);
}
else if (n == 0)
{
lg(Info, "time out ...");
}
else
{
lg(Error, "epll wait error");
}
}
}
```
- **添加监听套接字到 `epoll`**:将监听套接字添加到 `epoll` 中,并关注其读事件。
- **无限循环等待事件**:进入无限循环,等待事件发生。
- **处理事件**:如果有事件发生,调用 `Dispatcher` 方法处理这些事件。
- **处理超时和错误**:如果没有事件发生(超时),记录日志;如果出现错误,记录错误日志。
### 析构函数
```cpp
~EpollServer()
{
_listsocket_ptr->Close();
}
```
- **关闭监听套接字**:在析构函数中关闭监听套接字。
Epoll的LT模式与ET模式






Epoll总结



其他注意点
1.
写事件(Write Events)
写事件则通常按需设置,原因如下:
输出缓冲区状态:当发送数据的速度超过了网络链路或者对端处理的速度时,输出缓冲区可能会满。如果此时继续写入,可能会导致缓冲区溢出。因此,1.当输出缓冲区满时,需要停止关注写事件,直到缓冲区有空余空间。 2.缓冲区常常存在剩余空间,可以直接写入。
按需写入:通常情况下,应用层不需要不断地写入数据,而是有数据要发送时才写入。因此,只有在发送缓冲区有空余且确实有数据要发送时,才需要关注写事件。
资源利用:持续关注写事件可能会造成不必要的 CPU 资源浪费(epoll持续返回),因为大多数时间可能并不需要写入数据。
正确的策略:
直接写入,如果写入完成,就结束。如果写入完成,但是数据没有写完,outbuffer里还有内容,我们就需要设置对写事件进行关心了,如果写完了,去掉对写事件的关心!
2.给每个文件描述符都定义属于自己的方法与缓冲区(内核中也是这么实现的)
3.#pragma once可以保证防止头文件被重复包含,从而当头文件出现函数的定义时,防止出现函数的重定义问题
4.
这样在读或者写的时候一定就会出错,可以在读写中去排查。