声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
对 Linux 系统中 3 种 I/O 多路复用方式(select
、poll
、epoll
)进行介绍,包括函数说明、处理流程、示例代码和优缺点分析,并对 3 种方式进行对比分析。
目录
服务器端采用 一线程一连接的通信方式,当客户端连接变多时,线程也会变多。由于线程对存储资源的占用,以及 CPU 在线程之间的调度开销,会造成系统性能下降。
采用 I/O 多路复用技术,可以实现一个线程维护多个客户端连接,从而减少开销,提高性能。
1 I/O 多路复用
I/O 多路复用,通过某种机制,实现一个线程可以同时监视多个 I/O 描述符,内核一旦发现某个描述符就绪(如读就绪、写就绪),就通知程序进行相应的(读、写)操作。
I/O 多路复用有多种实现方式,系统调用包括 select
、poll
、epoll
等,其中 epoll
是 Linux 所特有的,select
是 POSIX(Portable Operating System Interface)所规定的,一般操作系统均可实现。
在 Linux 2.4 内核前,主要是 select
和 poll
(目前在小规模服务器上仍然有用武之地,并且在维护老系统代码时,也会经常用到这两个函数);
从 Linux 2.6 内核正式引入 epoll
以来,epoll
已经成为目前实现高性能网络服务器的必备技术。
2 基于 select 的服务器
2.1 select 函数
/**
* @file <sys/select.h>
* @brief 允许程序监视多个文件描述符,等待其中一个或多个文件描述符对某类 I/O 操作变得“就绪”,
* 即可以在没有阻塞的情况下执行对应的 I/O 操作(如读,或少量数据的写)。
*
* @param[in] maxfd 需要被设置为 “集合中所有文件描述符的最大值 + 1”,
* 在等待是否有套接字准备就绪时,只需要监测 maxfd 个套接字。
* 在 Linux 系统中,默认最大值为 1024。
* @param[inout] readfds 指向 fd_set 结构的指针,里面包含多个文件描述符。
* 输入为,需要监视其“是否可读”的文件描述符的集合,
* 如果为 NULL,表示不关心任何文件的“可读事件”;
* 输出为,已经“可读”(读操作不会被阻塞)的文件描述符的集合。
* 即 readfds 中套接字的状态包括:
* 1)有数据可读,可以调用 recv 接收数据;
* 2)连接已经关闭,即调用 recv 返回 0,需调用 close;
* 3)正在请求建立连接,可以调用 accept 建立链接。
* @param[inout] writefds 类似 readfds,不同的是,其监视文件描述符“是否可写”;
* 另外,如果写入数据量过大,写操作仍然会被阻塞。
* 输出 writefds 中套接字的状态包括:
* 1)可以写数据,即可以调用 send 发送数据;
* 2)可以请求连接,即可以调用 connect 建立连接。
* @param[inout] exceptfds 类似 readfds,不同的是,其监视文件描述符“是否异常”。
* @param[inout] timeout 表示 select 的等待时间,
* 设置为 NULL 时,表明 select 函数是阻塞的;
* 设置为 timeout->tv_sec = 0,timeout->tv_usec = 0 时,表明函数是非阻塞的;
* 设置为非 0 时间,表明函数有超时时间,超时后,就会返回。
* @return 成功 >0,值为包含在三个描述符集合中的“就绪”文件描述符的数量;
* 超时 =0,没有任何文件描述符准备就绪;
* 出错 -1,可以通过 errno 获取错误信息。
* 文件描述符集合保持不变,而 timeout 变为未定义。
*/
int select(int maxfd, fd_set *restrict readfds, fd_set *restrict writefds,
fd_set *restrict exceptfds, struct timeval *restrict timeout);
void FD_ZERO(fd_set *set); // 将 set 集合初始化为空集合
void FD_SET(int fd, fd_set *set); // 将套接字 fd 加入到 set 集合
void FD_CLR(int fd, fd_set *set); // 从 set 集合中删除 fd 套接字
int FD_ISSET(int fd, fd_set *set); // 检查 fd 是否在 set 集合中
通过 select(2) 或 select(3p) 可以查看详细说明。
2.2 处理流程
采用 select
方式,服务器端 I/O 多路复用处理流程示意:
2.3 示例代码
采用 select
方式,实现服务器端可读写事件监测。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h> // 包含 select 函数相关
#include <sys/poll.h> // 包含 poll 函数相关
#include <sys/epoll.h> // 包含 epoll 函数相关
#define SOCK_IP INADDR_ANY
#define SOCK_PORT 2048
#define LISTEN_BACKLOG 10
#define BUF_SIZE 256
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main()
{
int sockfd;
socklen_t addrlen;
struct sockaddr_in addr, server_addr;
// 创建一个套接字,用于监听客户端的连接
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
handle_error("socket");
// 允许地址的立即重用
// int setsockopt(int sockfd, int level, int option_name,
// const void *option_value, socklen_t option_len);
int on = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
handle_error("setsockopt");
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(SOCK_IP);
addr.sin_port = htons(SOCK_PORT);
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
handle_error("bind");
// 获取本地套接字地址
addrlen = sizeof(server_addr);
if (getsockname(sockfd, (struct sockaddr *)&server_addr, &addrlen) == -1)
handle_error("getsockname");
printf("Server Addr [%s:%d]\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));
if (listen(sockfd, LISTEN_BACKLOG) == -1)
handle_error("listen");
#if 1 // I/O 多路复用处理
// 使用 select 实现 I/O 多路复用
struct // 参照 fd_set,将 fd(如 3)保存在索引为 fd(如 fd_buf[3])的位置
{
char rbuf[BUF_SIZE]; // 读缓存
char wbuf[BUF_SIZE]; // 写缓存
} fd_buf[FD_SETSIZE]; // 读写缓存数组
// “in_* 仅输入” 集合,用于 FD 事件保存和调整
// “inout_* 既输入又输出” 集合,用于传入参数,获取返回值
fd_set in_rfds, inout_rfds; // 读事件监测集合
fd_set in_wfds, inout_wfds; // 写事件监测集合
FD_ZERO(&in_rfds);
FD_ZERO(&in_wfds);
FD_SET(sockfd, &in_rfds); // 设置 “监听套接字” 监测 “读” 事件
int maxfd = sockfd; // 初始化最大 FD
while (1)
{
inout_rfds = in_rfds; // 调用 select 前,更新参数
inout_wfds = in_wfds;
// 传入数组参数,获取数组返回值【性能影响】
int nready = select(maxfd + 1, &inout_rfds, &inout_wfds, NULL, NULL);
if (nready == -1)
handle_error("select");
if (FD_ISSET(sockfd, &inout_rfds)) // “监听套接字” 可读
{
int clientfd;
struct sockaddr_in client_addr;
// 从连接请求队列中取出排在最前面的客户端请求
addrlen = sizeof(client_addr);
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (clientfd == -1)
handle_error("accept");
printf("accept fd(%d) [%s:%d]\n", clientfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
FD_SET(clientfd, &in_wfds); // 设置 “客户套接字” 监测 “写” 事件,发送服务说明
fd_buf[clientfd].rbuf[0] = '\0';
sprintf(fd_buf[clientfd].wbuf, "Welcome [%s:%d] to the server!\n"
"Send \"bye\" to end the communication,\n"
"Send \"q\" to quit the service,\n"
"Send anything else to continue the communication.\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
if (clientfd > maxfd) maxfd = clientfd; // 更新最大 FD
}
// 在循环中遍历每一个客户端连接套接字事件【性能影响】
for (int clientfd = sockfd + 1; clientfd <= maxfd; clientfd++)
{
if (FD_ISSET(clientfd, &inout_rfds)) // “客户端连接套接字” 可读
{
// 接收数据
char *buf = fd_buf[clientfd].rbuf;
int rlen = recv(clientfd, buf, BUF_SIZE, 0);
if (rlen == -1)
handle_error("recv");
if (rlen == 0) // 客户端 close 断开连接
{
printf("close fd(%d)\n", clientfd);
FD_CLR(clientfd, &in_rfds);
if (close(clientfd) == -1)
handle_error("close clientfd");
continue;
}
buf[rlen] = '\0';
printf("fd(%d) recv: %s(%d)\n", clientfd, buf, rlen);
// 通信控制
if (strcmp(buf, "bye") == 0)
{
printf("close fd(%d)\n", clientfd);
FD_CLR(clientfd, &in_rfds);
if (close(clientfd) == -1)
handle_error("close clientfd");
continue;
}
else if (strcmp(buf, "q") == 0)
{
// 关闭所有客户端 FD
for (int cfd = sockfd + 1; cfd <= maxfd; cfd++)
{
// 查找仍在监测读写事件(即未关闭)的客户端连接
if (FD_ISSET(cfd, &in_rfds) || FD_ISSET(cfd, &in_wfds))
{
printf("close fd(%d)\n", cfd);
if (close(cfd) == -1)
handle_error("close clientfd");
}
}
if (close(sockfd) == -1)
handle_error("close sockfd");
return 0;
}
FD_CLR(clientfd, &in_rfds); // 不再接收数据
FD_SET(clientfd, &in_wfds); // 需要发送数据
strcpy(fd_buf[clientfd].wbuf, fd_buf[clientfd].rbuf); // 发送“接收到的内容”
}
if (FD_ISSET(clientfd, &inout_wfds)) // “客户端连接套接字” 可写
{
// 发送数据
char *buf = fd_buf[clientfd].wbuf;
int wlen = send(clientfd, buf, strlen(buf), 0);
if (wlen != strlen(buf))
handle_error("send");
printf("fd(%d) send: %s(%d)\n", clientfd, buf, wlen);
FD_CLR(clientfd, &in_wfds); // 不再发送数据
FD_SET(clientfd, &in_rfds); // 需要接收数据
}
}
}
#endif // I/O 多路复用处理
return 0;
}
2.4 select 优缺点
优点:
- 跨平台:几乎在所有的平台上都可用。
缺点:
- 拷贝开销:需要在用户和内核地址空间之间,对文件描述符 fd 集合进行整体拷贝;
- 内核检测效率低:内核每次检测都需要遍历输入的 fd 集合;
- 应用查找效率低:应用程序也需要对输出的 fd 集合进行遍历,查找就绪状态的 fd;
- 参数过多:有 3 个集合参数,需要分别对每个集合进行遍历;
- 数量限制:监视 fd 的数量默认最多为 1024(Linux)。
3 基于 poll 的服务器
3.1 poll 函数
/**
* @file <sys/poll.h>
* @brief 与 select 类似,poll 同样允许程序监视多个文件描述符,
* 等待其中一个或多个文件描述符对某类 I/O 操作变得“就绪”。
*
* @param[inout] fds 指向 pollfd 结构体数组的指针
* struct pollfd {
* int fd; // 文件描述符,如果为负,将忽略 events,revents 返回 0
* short int events; // [输入] 等待的事件,如果为 0,revents 只会返回 POLLHUP, POLLERR, POLLNVAL
* short int revents; // [输出] 实际发生的事件
* };
* 可以监视的事件有 POLLIN(可读)、POLLPRI(异常)、POLLOUT(可写) 等。
* @param[in] nfds 指定第一个参数 fds 数组中元素的个数
* 不同于 select,文件描述符数量没有 1024 上限
* @param[in] timeout 表示 poll 的等待时间(毫秒)
* 设置为 ‒1 时,表明 poll 函数是阻塞的;
* 设置为 0 时,表明函数是非阻塞的,立即返回;
* 设置为 >0 时,表明函数有超时时间,超时后,就会返回。
* @return 成功 >0,值为 fds 中 revents 非 0 的文件描述符的数量
* 超时 =0,没有任何文件描述符监视事件发生;
* 出错 -1,可以通过 errno 获取错误信息。
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
通过 poll(2) 或 poll(3p) 可以查看详细说明。
3.2 处理流程
采用 poll
函数,服务器端 I/O 多路复用处理流程示意:
3.3 示例代码
采用 poll
方式,实现服务器端可读写事件监测。
// 使用 poll 实现 I/O 多路复用
#define MAX_FD_SIZE 1024
struct // 简化实现,将 fd(如 3)保存在索引为 fd(如 fd_buf[3])的位置
{
char rbuf[BUF_SIZE]; // 读缓存
char wbuf[BUF_SIZE]; // 写缓存
} fd_buf[MAX_FD_SIZE]; // 读写缓存数组
struct pollfd fds[MAX_FD_SIZE] = {0};
for (int i = 0; i < MAX_FD_SIZE; i++)
fds[i].fd = -1;
fds[sockfd].fd = sockfd; // 简化实现,将 fd 保存在索引为 fd 的位置
fds[sockfd].events = POLLIN; // 设置 “监听套接字” 监测 “读” 事件
int maxfd = sockfd; // 初始化最大 FD
while (1)
{
// 传入 fds 数组参数,获取 fds 数组返回值【性能影响】
int nready = poll(fds, maxfd + 1, -1);
if (nready == -1)
handle_error("poll");
if (fds[sockfd].revents & POLLIN) // “监听套接字” 可读
{
int clientfd;
struct sockaddr_in client_addr;
// 从连接请求队列中取出排在最前面的客户端请求
addrlen = sizeof(client_addr);
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (clientfd == -1)
handle_error("accept");
printf("accept fd(%d) [%s:%d]\n", clientfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLOUT; // 设置 “客户套接字” 监测 “写” 事件,发送服务说明
fd_buf[clientfd].rbuf[0] = '\0';
sprintf(fd_buf[clientfd].wbuf, "Welcome [%s:%d] to the server!\n"
"Send \"bye\" to end the communication,\n"
"Send \"q\" to quit the service,\n"
"Send anything else to continue the communication.\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
if (clientfd > maxfd) maxfd = clientfd; // 更新最大 FD
}
// 在循环中遍历每一个客户端连接套接字事件【性能影响】
for (int clientfd = sockfd + 1; clientfd <= maxfd; clientfd++)
{
if (fds[clientfd].revents & POLLIN) // “客户端连接套接字” 可读
{
// 接收数据
char *buf = fd_buf[clientfd].rbuf;
int rlen = recv(clientfd, buf, BUF_SIZE, 0);
if (rlen == -1)
handle_error("recv");
if (rlen == 0) // 客户端 close 断开连接
{
printf("close fd(%d)\n", clientfd);
fds[clientfd].fd = -1;
if (close(clientfd) == -1)
handle_error("close clientfd");
continue;
}
buf[rlen] = '\0';
printf("fd(%d) recv: %s(%d)\n", clientfd, buf, rlen);
// 通信控制
if (strcmp(buf, "bye") == 0)
{
printf("close fd(%d)\n", clientfd);
fds[clientfd].fd = -1;
if (close(clientfd) == -1)
handle_error("close clientfd");
continue;
}
else if (strcmp(buf, "q") == 0)
{
// 关闭所有客户端 FD
for (int cfd = sockfd + 1; cfd <= maxfd; cfd++)
{
// 查找未关闭的客户端连接
if (fds[cfd].fd != -1)
{
printf("close fd(%d)\n", cfd);
if (close(cfd) == -1)
handle_error("close clientfd");
}
}
if (close(sockfd) == -1)
handle_error("close sockfd");
return 0;
}
fds[clientfd].events = POLLOUT; // 需要发送数据
strcpy(fd_buf[clientfd].wbuf, fd_buf[clientfd].rbuf); // 发送“接收到的内容”
}
if (fds[clientfd].revents & POLLOUT) // “客户端连接套接字” 可写
{
// 发送数据
char *buf = fd_buf[clientfd].wbuf;
int wlen = send(clientfd, buf, strlen(buf), 0);
if (wlen != strlen(buf))
handle_error("send");
printf("fd(%d) send: %s(%d)\n", clientfd, buf, wlen);
fds[clientfd].events = POLLIN; // 需要接收数据
}
}
}
使用上述代码片段替换 select 示例代码 中 #if 1 // I/O 多路复用处理
宏定义内部分,可以形成完整示例代码。
3.4 poll 优缺点
poll
和 select
函数的本质是一样的。
优点:
- 文件描述符 fd 数量仅受其类型影响,没有额外限制。
缺点:
- 缺少针对大量 fd 的优化机制,fd 数量过大后性能会下降;
- 拷贝开销:需要在用户和内核地址空间之间,对 fd 集合进行整体拷贝;
- 内核检测效率低:内核每次检测都需要遍历输入的 fd 集合;
- 应用查找效率低:应用程序也需要对输出的 fd 集合进行遍历,查找就绪状态的 fd。
4 基于 epoll 的服务器
4.1 epoll 函数
/**
* @file <sys/epoll.h>
* @brief 创建一个 epoll 实例,返回一个 epoll 文件描述符。
* 这个文件描述符用于后续所有对 epoll 接口的调用;
* 当不再需要时,应该使用 close 释放资源。
*
* @param[in] size 大于 0 的整数,如 1
* 需要监测的文件描述符的数目(从 Linux 2.6.8 起被忽略)
* @return 如果成功,返回一个 int 类型非负的文件描述符;
* 如果失败,返回 ‒1,可以通过 errno 获取错误信息。
*/
int epoll_create(int size);
/**
* @file <sys/epoll.h>
* @brief 在 “epfd 指向的 epoll 实例” 的监控列表中,添加、修改或删除条目,
* 即,请求为目标文件描述符 fd 执行操作 op。
*
* @param[in] epfd 通过 epoll_create 返回的文件描述符
* @param[in] op 表示控制操作,可以设置为:
* EPOLL_CTL_ADD:添加新的 fd 到 epfd 的监控列表中,并监测 event 事件
* EPOLL_CTL_MOD:使用 event 修改已经添加的 fd 的监测事件
* EPOLL_CTL_DEL:从 epfd 的监控列表中删除 fd,event 可以为 NULL
* @param[in] fd 控制操作 op 的实施对象,即需要操作的文件描述符
* @param[in] event 指定文件描述符 fd 需要监测的事件
* struct epoll_event {
* uint32_t events; // Epoll events
* epoll_data_t data; // User data variable
* };
* union epoll_data {
* void *ptr;
* int fd;
* uint32_t u32;
* uint64_t u64;
* };
* typedef union epoll_data epoll_data_t;
*
* data 成员,是用户自定义数据,内核通过 epoll_wait() 会直接返回;
* events 成员,是通过“按位或”组合在一起的零个或多个事件类型
* 事件类型有:
* EPOLLIN:表示对应的文件描述符可以读
* EPOLLOUT:表示对应的文件描述符可以写
* EPOLLPRI:表示对应的文件描述符有紧急的数据可读
* ...
* @return 成功返回 0;失败返回 ‒1,可以通过 errno 获取错误信息。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *_Nullable event);
/**
* @file <sys/epoll.h>
* @brief 获取在 “epfd 指向的 epoll 实例” 的监控列表中已经发生的事件
*
* @param[in] epfd 通过 epoll_create 返回的文件描述符
* @param[out] events 将所有就绪的事件拷贝到 events 指向的数组中;
* 数组元素 epoll_event 结构的
* data 成员,是最近一次通过 EPOLL_CTL_ADD, EPOLL_CTL_MOD 设置的用户数据;
* events 成员,是已经发生的事件。
* @param[in] maxevents 指定 events 数组中最多可容纳事件的数量
* @param[in] timeout 指定 epoll_wait 的等待时间(毫秒)
* 设置为 ‒1 时,表明 epoll_wait 函数是阻塞的;
* 设置为 0 时,表明函数是非阻塞的,立即返回;
* 设置为 >0 时,表明函数有超时时间,超时后,就会返回。
* @return 成功 >0,表示 events 中就绪文件描述符的个数
* 超时 =0,没有任何文件描述符事件发生;
* 出错 -1,可以通过 errno 获取错误信息。
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
通过 epoll(7) 可以查看详细说明。
4.2 处理流程
采用 epoll
方式,服务器端 I/O 多路复用处理流程示意:
4.3 示例代码
采用 epoll
方式,实现服务器端可读写事件监测。
// 使用 epoll 实现 I/O 多路复用
#define MAX_FD_SIZE 1024
struct // 简化实现,统一分配空间,将 fd(如 3)保存在索引为 fd(如 fd_buf[3])的位置
{
int fd; // 客户端连接套接字描述符
char rbuf[BUF_SIZE]; // 读缓存
char wbuf[BUF_SIZE]; // 写缓存
} fd_buf[MAX_FD_SIZE] = {0}; // 读写缓存数组
struct epoll_event events[MAX_FD_SIZE] = {0};
int epfd = epoll_create(1);
if (epfd == -1)
handle_error("epoll_create");
struct epoll_event ev;
ev.data.fd = sockfd;
ev.events = EPOLLIN; // 设置 “监听套接字” 监测 “读” 事件
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1)
handle_error("EPOLL_CTL_ADD");
while (1)
{
int nready = epoll_wait(epfd, events, MAX_FD_SIZE, -1);
if (nready == -1)
handle_error("epoll_wait");
for (int i = 0; i < nready; i++)
{
if (events[i].data.fd == sockfd) // “监听套接字” 可读
{
int clientfd;
struct sockaddr_in client_addr;
// 从连接请求队列中取出排在最前面的客户端请求
addrlen = sizeof(client_addr);
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (clientfd == -1)
handle_error("accept");
printf("accept fd(%d) [%s:%d]\n", clientfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
ev.data.fd = clientfd;
ev.events = EPOLLOUT; // 设置 “客户套接字” 监测 “写” 事件,发送服务说明
if (epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev) == -1)
handle_error("EPOLL_CTL_ADD");
fd_buf[clientfd].fd = clientfd;
fd_buf[clientfd].rbuf[0] = '\0';
sprintf(fd_buf[clientfd].wbuf, "Welcome [%s:%d] to the server!\n"
"Send \"bye\" to end the communication,\n"
"Send \"q\" to quit the service,\n"
"Send anything else to continue the communication.\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
continue;
}
// 处理每一个客户端连接套接字事件
int clientfd = events[i].data.fd;
if (events[i].events & EPOLLIN) // “客户端连接套接字” 可读
{
// 接收数据
char *buf = fd_buf[clientfd].rbuf;
int rlen = recv(clientfd, buf, BUF_SIZE, 0);
if (rlen == -1)
handle_error("recv");
if (rlen == 0) // 客户端 close 断开连接
{
printf("close fd(%d)\n", clientfd);
fd_buf[clientfd].fd = -1;
if (epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL) == -1)
handle_error("EPOLL_CTL_DEL");
if (close(clientfd) == -1)
handle_error("close clientfd");
continue;
}
buf[rlen] = '\0';
printf("fd(%d) recv: %s(%d)\n", clientfd, buf, rlen);
// 通信控制
if (strcmp(buf, "bye") == 0)
{
printf("close fd(%d)\n", clientfd);
fd_buf[clientfd].fd = -1;
if (epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL) == -1)
handle_error("EPOLL_CTL_DEL");
if (close(clientfd) == -1)
handle_error("close clientfd");
continue;
}
else if (strcmp(buf, "q") == 0)
{
// 关闭所有客户端 FD
for (int cfd = epfd + 1; cfd < MAX_FD_SIZE; cfd++)
{
// 查找未关闭的客户端连接
if (fd_buf[cfd].fd > 0)
{
printf("close fd(%d)\n", cfd);
if (close(cfd) == -1)
handle_error("close clientfd");
}
}
if (close(epfd) == -1)
handle_error("close epfd");
if (close(sockfd) == -1)
handle_error("close sockfd");
return 0;
}
ev.data.fd = clientfd;
ev.events = EPOLLOUT; // 需要发送数据
if (epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev) == -1)
handle_error("EPOLL_CTL_MOD");
strcpy(fd_buf[clientfd].wbuf, fd_buf[clientfd].rbuf); // 发送“接收到的内容”
}
if (events[i].events & EPOLLOUT) // “客户端连接套接字” 可写
{
// 发送数据
char *buf = fd_buf[clientfd].wbuf;
int wlen = send(clientfd, buf, strlen(buf), 0);
if (wlen != strlen(buf))
handle_error("send");
printf("fd(%d) send: %s(%d)\n", clientfd, buf, wlen);
ev.data.fd = clientfd;
ev.events = EPOLLIN; // 需要接收数据
if (epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev) == -1)
handle_error("EPOLL_CTL_MOD");
}
}
}
使用上述代码片段替换 select 示例代码 中 #if 1 // I/O 多路复用处理
宏定义内部分,可以形成完整示例代码。
4.4 epoll 优缺点
epoll
是 select
/poll
的增强版本,在“并发连接数量多,活跃连接较少”的情况下,能够显著提高系统的性能。
优点:
- 没有大量无效输入/输出拷贝开销:
- 拆分为 epoll_ctl() 和 epoll_wait() 两个独立接口(调用频率不同);
- epoll_ctl() 设置输入参数,每次只设置目标文件描述符 fd 事件(调用频率相对低);
- epoll_wait() 返回输出结果,每次只返回当前就绪 fd 集合(调用频率相对高);
- 内核检测效率高:采用回调方式检测就绪事件;
- 应用查找效率高:系统调用仅返回就绪事件,不需要再判断 fd 是否就绪;
- 增加“边缘触发 ET”工作方式,可以进一步提升性能。
缺点:
- 在活跃连接比较多时,回调函数被触发得过于频繁,效率会受到影响。
5 三种方式比较
系统调用 | select | poll | epoll |
---|---|---|---|
输入/输出事件集合数量 | 不同事件类型对应不同集合参数 | 一个集合参数包含不同事件类型 | 同 poll |
输入/输出共享内存情况 | 输出直接覆盖输入数据(再次调用需重新赋值) | 输入输出使用不同字段 | 输出为独立内存空间 |
输入 fd 集合有效性 | 可能包含无效 fd,并且会反复传入目标 fd 事件(整体拷贝) | 同 select | 通过 epoll_ctl 一次性处理目标 fd 事件 |
输出 fd 集合有效性 | 可能包含无效 fd(整体拷贝) | 同 select | 通过 epoll_wait 仅返回就绪 fd |
最大支持 fd 数量 | Linux 默认 1024 | 与 fd 类型相关 | 同 poll |
内核检测方式 | 采用轮询方式检测就绪事件 | 同 select | 采用回调方式检测就绪事件 |
内核检测时间复杂度 | O(n) | 同 select | O(1) |
应用查找就绪 fd 方式 | 采用轮询方式查找就绪 fd 事件 | 同 select | 直接定位就绪 fd 事件 |
应用查找就绪 fd 时间复杂度 | O(n) | 同 select | O(1) |
工作模式 | LT | 同 select | LT、ET |
适用场景 | 活跃连接多 & 并发连接少 | 同 select | 活跃连接少 |
参考
- 朱文伟,李建英著.Linux C/C++ 服务器开发实践.清华大学出版社.2022.
宁静以致远,感谢 King 老师。