一文讲清I/O多路复用(select、poll和epoll)
一、写在前面
本文尽可能使用最简单和最清楚的逻辑对比select、poll和epoll技术之间的区别,说明I/O多路复用从select发展到epoll到底优化了什么。
注意:1、本文不讲解select、poll和epoll三个系统调用的具体参数;2、未有特殊说明则本文以Linux平台作为基础;3、所有讲解仅代表作者本人理解,如有错误烦请指正。
二、概述
网络服务端处理并发连接的最简方案当属多线程设计,而多线程最大的问题就在于CPU上下文切换,且线程的资源占用较多,在高并发情况下多线程设计方案显得非常不合理,所以我们不如从单线程逻辑上进行I/O优化。
这里说明一下,我们刚才所谈及的线程是信号处理线程,这里读者可以简单理解成管理连接的线程(监听文件描述符是否有事件触发),而不是工作线程(及处理数据的线程)。下面是单线程设计伪码:
while(true)
{
for(fd : fd[n])
{
if(fd有事件触发)
{
处理fd上的事件
}
}
}
单线程的问题在于if判断上,用户态程序为了判断fd是否有事件触发(例如有数据可读或可写)需要切换到内核态,这样以来,每次if判断都要进行用户态到内核态的切换,效率低下,而I/O多路复用技术主要就是对这个缺陷进行优化,它的整体思路便是:如果在用户态监听fd每次判断都要切换到内核态,那么就让内核直接帮我们监听fd,然后把触发的事件一次性返回给我们。
三、select技术
1、概述
在用户态初始化一个bitmap,将需要监听的文件描述符映射到这个bitmap中,把bitmap传入内核,内核监听对应的fd,如果fd有事件触发则对bitmap进行标记,标记完后整个返回到用户态,用户程序通过对bitmap的判断确定需要处理的fd进行处理。
2、流程
1)现有5个文件描述符fd(1~5)分别为:1、3、4、6、7;
2)用户程序创建一个位图rset,将第1位、第3位依次类推至第7位置为1;
3)将rset传入select(下面是select的处理):
① 将rset拷贝到内核,内核中的位图记为rset1(用户态到内核态,第一次切换)
② 内核阻塞(可以设置超时)监听fd(1~5),有事件发生时,内核通过修改rset1标记事件对应的fd
③ 将rset1拷贝回用户态的rset返回(内核态到用户态,第二次切换)
4)select退出,用户程序获得rset,并通过遍历rset确认需要处理的fd及对应的事件;
3、编码
void main()
{
// ...
// 已创建服务器socket为sockfd
listen(sockfd, 5);
int fds[5] = {0};
int max = 0;
for(int i = 0; i < 5; ++i)
{
int fds[i] = accept(sockfd, NULL, NULL);
if(fds[i] > max)
{
max = fds[i];
}
}
fd_set rset;
char buf[MAXLEN] = {0};
while(true)
{
FD_ZERO(&rset);
for(int i = 0; i < 5; ++i)
{
FD_SET(fds[i], &rset);
}
select(max + 1, &rset, NULL, NULL, NULL); // 阻塞
for(int i = 0; i < 5; ++i)
{
if(FD_ISSET(fds[i], &rset))
{
memset(buf, 0, MAXLEN);
read(fds[i], buf, MAXLEN); // 事件处理,以读事件为例
std::cout << buf << "\n";
}
}
}
}
4、问题
1)bitmap大小受限(默认为1024)
2)rset不可重用,每次while开始需要循环初始化rset,时间复杂度O(n)
3)用户态和内核态之间切换
4)用户程序需要循环遍历rset,判断哪个fd上发生了事件,时间复杂度O(n)
四、poll技术
1、概述
struct pollfd
{
int fd;
short events;
short revents;
};
在用户态初始化一个pollfd数组pfds,通过需要监听的fd和事件初始化pfds(修改pollfd结构中的fd和events),把pfds传入内核,内核监听对应的fd,如果有事件触发则修改对应的pollfd的revents,修改完成后返回pollfd数组到用户态,用户态遍历pollfd数组确认需要处理的fd进行处理。
2、流程
1)现有5个文件描述符fd(1~5);
2)用户程序创建一个长度为5的pollfd数组pfds,使用fd(1~5)和需要监听的事件初始化pfds的fd和events成员;
3)将pfds传入poll(下面是poll的处理):
① 将pfds拷贝到内核,内核中的pollfd数组记为pfds1(用户态到内核态,第一次切换)
② 内核阻塞监听pfds1中的fd,有事件发生时,内核修改pfds1的revents
③ 将pfds1拷贝回用户态的pfds返回(内核态到用户态,第二次切换)
4)poll退出,用户程序获得pfds,并通过遍历pfds确认哪些fd有事件发生;
3、编码
void main()
{
// ...
struct pollfd pfds[5];
for(int i = 0; i < 5; ++i)
{
pfds[i].fd = accept(sockfd, NULL, NULL);
pfds[i].events = POLLIN; // 读事件
}
char buf[MAXLEN] = {0};
while(true)
{
poll(pfds, 5, 1000); // 阻塞,超时时间1000ms
for(int i = 0; i < 5; ++i)
{
if(pfds[i].revents & POLLIN)
{
pfds[i].revents = 0;
memset(buf, 0, MAXLEN);
read(pfds[i].fd, buf, MAXLEN); // 事件处理,以读事件为例
std::cout << buf << "\n";
}
}
}
}
4、问题
1)bitmap大小受限(默认为1024)(使用了pollfd数组)
2)rset不可重用,每次while开始需要循环初始化rset,时间复杂度O(n)(pollfd结构因为有revents成员所以可重用)
3)用户态和内核态之间切换
4)用户程序需要循环遍历rset,判断哪个fd上发生了事件,时间复杂度O(n)
五、epoll技术
1、概述
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
};
用户态通过调用epoll_create函数创建一个epoll实例(底层实现为红黑树,通过文件描述符管理),将被监听fd和事件通过epoll_ctl和epoll_event注册到epoll实例,创建一个epoll_events数组用于接收epoll处理结果,用户态和内核态共享这个epoll_events数组,内核监听对应的fd,当有事件触发时内核对epoll_events数组进行重排,将已经就绪的fd放到靠前位置,然后返回触发事件的fd总数,用户程序拿到返回后直接处理已经发生事件的fd,而不需要遍历所有fd。
水平触发(LT): 在水平触发模式下,当文件描述符上有事件发生时,epoll_wait会立即返回通知应用程序。如果应用程序没有处理完这个事件,下次调用epoll_wait时仍然会返回这个事件。
边缘触发(ET): 在边缘触发模式下,epoll_wait只在发生状态变化时才返回通知,而不是在文件描述符上有事件发生时就返回。这意味着应用程序必须一次性处理完这个事件,否则会错过后续的事件通知。
2、流程
1)现有5个文件描述符fd(1~5);
2)用户程序通过epoll_create调用获取epfd;
3)循环调用epoll_ctl将fd及对应的待监听事件注册到epfd上(以epoll_event作为载体)
4)创建一个epoll_event数组events用于存储触发结果(events由用户态和内核态共享);
5)将epfd和events通过epoll_wait传入epoll(下面是epoll的处理):
① 内核阻塞监听注册到epfd上的fd,有事件发生时对events进行重排(分为水平触发和边缘触发)
② 内核返回已经触发事件的fd总数
6)epoll退出,用户程序拿到触发事件的fd总数,直接处理已经发生事件events
3、编码
void main()
{
int epfd = epoll_create(1);
for(int i = 0; i < 5; ++i)
{
static struct epoll_event ev;
ev.data.fd = accept(sockfd, NULL, NULL);
ev.events = EPOLLIN; // 读事件
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); //注册事件
}
struct epoll_event events[5];
char buf[MAXLEN] = {0};
while(true)
{
int fds = epoll_wait(epfd, events, 5, 1000); // 阻塞,超时时间1000ms
for(int i = 0; i < fds; ++i)
{
memset(buf, 0, MAXLEN);
read(events[i].data.fd, buf, MAXLEN); // 事件处理,以读事件为例
std::cout << buf << "\n";
}
}
}
4、问题
1)bitmap大小受限(默认为1024)
2)rset不可重用,每次while开始需要循环初始化rset,时间复杂度O(n)
3)用户态和内核态之间切换(用户态和内核态共用epoll_events数组)
4)用户程序需要循环遍历rset,判断哪个fd上发生了事件,时间复杂度O(n)(直接返回已经发生事件的fd总数)
5、一个完整的epoll使用示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAX_EVENTS 10
int main()
{
int listen_sock, conn_sock, nfds, epollfd;
struct epoll_event ev, events[MAX_EVENTS];
struct sockaddr_in serv_addr;
// 创建 socket
if ((listen_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(8888);
// 绑定 socket
if (bind(listen_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
{
perror("bind");
exit(EXIT_FAILURE);
}
// 监听 socket
if (listen(listen_sock, 5) == -1)
{
perror("listen");
exit(EXIT_FAILURE);
}
// 创建 epoll 文件描述符
if ((epollfd = epoll_create1(0)) == -1)
{
perror("epoll_create1");
exit(EXIT_FAILURE);
}
// 设置要监控的事件
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
// 将 listen_sock 加入到 epoll 监控列表中
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1)
{
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;)
{
// 等待事件的发生
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1)
{
perror("epoll_wait");
exit(EXIT_FAILURE);
}
// 处理所有发生的事件
for (int n = 0; n < nfds; ++n)
{
if (events[n].data.fd == listen_sock)
{
// 如果是 listen_sock,表示有新的连接
if ((conn_sock = accept(listen_sock, NULL, NULL)) == -1)
{
perror("accept");
continue;
}
printf("Accepted new connection from client\n");
// 将新的 socket 加入到 epoll 监控列表中
ev.events = EPOLLIN;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1)
{
perror("epoll_ctl: conn_sock");
close(conn_sock);
}
}
else
{
// 如果是已连接的 socket,处理读事件
char buffer[1024];
int ret = recv(events[n].data.fd, buffer, sizeof(buffer), 0);
if (ret <= 0)
{
// 关闭连接
close(events[n].data.fd);
continue;
}
// 处理接收到的数据
printf("Received data from client: %s\n", buffer);
}
}
}
// 关闭 epoll 文件描述符和 socket
close(epollfd);
close(listen_sock);
return 0;
}