epoll相较于select和poll在实现非阻塞I/O方面的优势
- 可扩展性:
- select:支持的文件描述符数量受限于
FD_SETSIZE
,通常为1024,在处理大量文件描述符时可扩展性差。
- poll:理论上没有文件描述符数量限制,但由于其采用线性遍历文件描述符集合的方式,随着文件描述符数量增多,性能会急剧下降。
- epoll:基于事件驱动,通过红黑树管理文件描述符,在处理大量文件描述符时性能稳定,可扩展性强。
- 事件通知机制:
- select:需要遍历整个文件描述符集合来检查哪些描述符有事件发生,效率较低。
- poll:同样需要遍历整个文件描述符链表来获取事件,性能也不理想。
- epoll:使用回调机制,当有事件发生时,内核将事件添加到就绪列表中,
epoll_wait
函数直接从就绪列表中获取事件,大大提高了效率。
- 内存拷贝:
- select和poll:每次调用都需要将用户态的文件描述符集合拷贝到内核态,返回时又要将内核态的结果拷贝回用户态,开销较大。
- epoll:通过
epoll_ctl
将文件描述符注册到内核后,后续操作只需要在内核态进行事件管理,不需要每次都进行内存拷贝,提高了效率。
C语言程序使用epoll实现对多个套接字的非阻塞I/O监控
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define MAX_EVENTS 10
#define BUF_SIZE 1024
void setnonblocking(int sockfd) {
int flags;
if ((flags = fcntl(sockfd, F_GETFL, 0)) == -1) {
perror("fcntl F_GETFL");
exit(EXIT_FAILURE);
}
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL");
exit(EXIT_FAILURE);
}
}
int main() {
int listen_sock, conn_sock;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen = sizeof(cliaddr);
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);
if (bind(listen_sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind");
close(listen_sock);
exit(EXIT_FAILURE);
}
if (listen(listen_sock, 5) == -1) {
perror("listen");
close(listen_sock);
exit(EXIT_FAILURE);
}
int epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1");
close(listen_sock);
exit(EXIT_FAILURE);
}
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl add listen_sock");
close(listen_sock);
close(epollfd);
exit(EXIT_FAILURE);
}
setnonblocking(listen_sock);
while (1) {
int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listen_sock) {
conn_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &clilen);
if (conn_sock == -1) {
perror("accept");
continue;
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {
perror("epoll_ctl add conn_sock");
close(conn_sock);
}
} else {
int sockfd = events[i].data.fd;
char buf[BUF_SIZE];
ssize_t n = recv(sockfd, buf, sizeof(buf), 0);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
} else {
perror("recv");
epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
}
} else if (n == 0) {
epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
} else {
buf[n] = '\0';
printf("Received: %s\n", buf);
// 简单回显
if (send(sockfd, buf, n, 0) == -1) {
perror("send");
epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
}
}
}
}
}
close(listen_sock);
close(epollfd);
return 0;
}
epoll的两种工作模式(LT和ET)的区别以及应用场景
- 水平触发(LT - Level Triggered):
- 区别:只要文件描述符对应的缓冲区还有未处理的数据,或者还有空间可以写入数据,
epoll_wait
就会不断通知该文件描述符有事件发生。也就是说,对于读事件,只要缓冲区有数据,就会一直触发可读事件;对于写事件,只要缓冲区有空间,就会一直触发可写事件。
- 应用场景:适用于数据处理速度较慢的场景,因为即使一次没有处理完所有数据,下次
epoll_wait
依然会通知,保证数据不会丢失。在传统的网络服务器编程中,LT模式使用较为普遍,因为它更符合人们对事件处理的常规理解。
- 边缘触发(ET - Edge Triggered):
- 区别:只有当文件描述符对应的缓冲区状态发生变化时(例如有新数据到达、缓冲区从不可写变为可写等),
epoll_wait
才会通知该文件描述符有事件发生。对于读事件,只有当新数据到达缓冲区时才会触发可读事件,并且需要一次性将缓冲区数据读完,否则下次不会再触发;对于写事件,只有当缓冲区从不可写变为可写时才会触发可写事件。
- 应用场景:适用于数据处理速度较快的场景,因为它可以减少不必要的事件通知,提高效率。在一些对性能要求极高的网络应用中,如高性能的Web服务器、网络爬虫等,ET模式能够充分发挥其优势,减少系统开销。