说一下epoll的好处
Epoll是Linux下高效的I/O多路复用机制,它在设计上克服了select的多个缺陷,具有以下优点:
- 突破文件描述符数量限制。Epoll使用一个文件描述符管理多个socket连接,将用户关心的socket事件通过epoll_ctl维护在内核中,因此不存在描述符数量的限制,一般只与系统资源有关。
-
O(1)时间复杂度。Epoll使用事件驱动机制,当某个socket有事件发生时,内核会使用回调函数将其加入就绪队列。Epoll_wait只需要从就绪队列中取出事件,无须遍历整个描述符集,因此时间复杂度是O(1)。
-
内存拷贝次数少。Epoll使用mmap在内核和用户空间之间建立映射,通过这个映射区域传递事件,减少了内存拷贝的次数。此外,内核还可以通过共享内存直接访问用户态的数据,再一次减少了数据拷贝。
-
支持多种事件触发模式。Epoll支持边缘触发(edge-triggered)和水平触发(level-triggered)两种事件模式。边缘触发只在socket状态发生变化时才触发事件,避免了重复触发。而水平触发与select和poll的行为类似,只要socket处于就绪状态就一直触发。
下面是一个使用epoll边缘触发实现高效回显服务器的例子:
#include <iostream>
#include <cstring>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
using namespace std;
int main() {
int listenFd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = AF_INET;
srvAddr.sin_addr.s_addr = htonl(INADDR_ANY);
srvAddr.sin_port = htons(9999);
bind(listenFd, (sockaddr*)&srvAddr, sizeof(srvAddr));
listen(listenFd, 5);
int epfd = epoll_create(1);
epoll_event ev, events[1024];
ev.data.fd = listenFd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenFd, &ev);
while (true) {
int nReady = epoll_wait(epfd, events, 1024, -1);
for (int i = 0; i < nReady; ++i) {
if (events[i].data.fd == listenFd) {
sockaddr_in cliAddr;
socklen_t len = sizeof(cliAddr);
int connFd = accept(listenFd, (sockaddr*)&cliAddr, &len);
ev.data.fd = connFd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, connFd, &ev);
} else {
int fd = events[i].data.fd;
char buf[256];
while (true) {
memset(buf, 0, sizeof(buf));
int n = read(fd, buf, sizeof(buf));
if (n == -1) {
if (errno == EAGAIN) {
break;
}
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
} else if (n == 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
} else {
write(fd, buf, strlen(buf));
}
}
}
}
}
close(listenFd);
return 0;
}
该例子中使用了epoll的以下特性:
- 使用
epoll_create
创建一个epoll实例,返回一个表示epoll的文件描述符。 -
使用
epoll_ctl
将需要监听的socket添加到epoll实例中,并设置关心的事件类型。例子中,我们对监听socket关心”可读”事件,对已连接socket关心”可读”和”边缘触发”事件。 -
使用
epoll_wait
等待事件发生。一旦有事件发生,epoll_wait就会返回,并将发生的事件填充到传入的数组中。 -
遍历事件数组,根据事件类型进行不同处理。例子中,如果发生事件的是监听socket,则接受新连接;如果是已连接socket,则进行读写。
-
由于使用了边缘触发,一次事件到来时需要将socket的数据全部处理完毕。因此,例子中使用了while循环不断读取数据,直到read返回EAGAIN错误,表示数据已被读完。
总的来说,epoll是Linux下高性能网络编程的利器。它在高并发场景下表现优异,已被Nginx、Redis等知名项目广泛使用。对epoll的深入理解和应用,是C++服务端开发者的必备技能。