【后端面试总结】select、poll、epoll三种IO多路复用方法对比
一、引言
IO多路复用是一种在网络编程中广泛使用的技术,它允许单个线程或进程同时处理多个IO操作,显著提高了系统的并发处理能力。本文将详细介绍三种常见的IO多路复用方法:select、poll和epoll,并探讨它们的实现原理、劣势及源码示例。
二、select的实现原理及劣势
实现原理:
select方法通过调用一个系统调用来实现IO多路复用。它要求用户进程传入三个文件描述符集合:读集合、写集合和异常集合。内核会监控这些集合中的文件描述符,当任何一个文件描述符变为就绪状态时,select会返回。用户进程随后通过遍历这些集合来确定哪些文件描述符已经就绪,并进行相应处理。
源码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int lfd, cfd;
struct sockaddr_in seraddr, cliaddr;
fd_set rset, aset;
int nfds, ret;
lfd = socket(AF_INET, SOCK_STREAM, 0);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8000);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
listen(lfd, 128);
FD_ZERO(&aset);
FD_SET(lfd, &aset);
nfds = lfd + 1;
while (1) {
rset = aset;
ret = select(nfds, &rset, NULL, NULL, NULL);
if (ret == -1) {
perror("select error");
exit(1);
}
if (FD_ISSET(lfd, &rset)) {
cfd = accept(lfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
FD_SET(cfd, &aset);
if (nfds - 1 < cfd) {
nfds = cfd + 1;
}
}
for (int i = lfd + 1; i < nfds; i++) {
if (FD_ISSET(i, &rset)) {
char buf[1024];
int rr = read(i, buf, sizeof(buf));
if (rr < 0) {
perror("read error");
exit(1);
} else if (rr == 0) {
FD_CLR(i, &aset);
close(i);
} else {
write(i, buf, rr);
}
}
}
}
return 0;
}
优势:
-
跨平台性好:select几乎在所有操作系统上都得到了支持,具有良好的跨平台性。
-
使用简单:select的接口相对简单,易于理解和使用。
劣势:
-
文件描述符数量限制:select使用固定大小的位图来存储文件描述符,导致它能够监控的文件描述符数量有限,通常为1024个
-
效率低下:每次调用select时,内核和用户空间之间需要复制文件描述符集合,这在大规模并发场景下会导致显著的性能开销。
-
轮询机制:select使用轮询机制来检查文件描述符的就绪状态,这在高并发场景下会导致效率低下。
三、poll的实现原理及劣势
实现原理:
poll方法是对select的改进,它使用链表来存储文件描述符,从而消除了文件描述符数量的限制。poll方法通过调用一个系统调用来监控多个文件描述符,并返回就绪的文件描述符集合。
源码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXFD 1024
int main() {
int lfd, cfd;
struct sockaddr_in seraddr, cliaddr;
struct pollfd fds[MAXFD];
int nfds, ret;
lfd = socket(AF_INET, SOCK_STREAM, 0);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8000);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
listen(lfd, 128);
nfds = 1;
fds[0].fd = lfd;
fds[0].events = POLLIN;
while (1) {
ret = poll(fds, nfds, -1);
if (ret < 0) {
perror("poll error");
exit(1);
}
if (fds[0].revents & POLLIN) {
cfd = accept(lfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
for (int i = 1; i < MAXFD; i++) {
if (fds[i].fd < 0) {
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
if (i > nfds - 1) {
nfds = i + 1;
}
}
for (int i = 1; i < nfds; i++) {
if (fds[i].fd == -1) {
continue;
}
if (fds[i].revents & POLLIN) {
char buf[1024];
int rr = read(fds[i].fd, buf, sizeof(buf));
if (rr < 0) {
perror("read error");
exit(1);
} else if (rr == 0) {
close(fds[i].fd);
fds[i].fd = -1;
} else {
write(fds[i].fd, buf, rr);
}
}
}
}
return 0;
}
优势:
-
无文件描述符数量限制:poll使用链表来存储文件描述符,因此没有数量上的限制,可以监控更多的文件描述符。
-
功能分离:poll将等待队列和阻塞进程分开,使得管理更加灵活。
劣势:
-
效率问题:尽管poll解决了文件描述符数量的限制,但它仍然需要每次调用时复制整个文件描述符集合,这在大规模并发场景下会导致性能问题。
-
轮询机制:与select类似,poll也使用轮询机制来检查文件描述符的就绪状态,这在高并发场景下效率较低。
四、epoll的实现原理及劣势
实现原理:
epoll是Linux特有的IO多路复用机制,它通过红黑树和就绪链表来高效管理文件描述符。epoll提供了三个系统调用:epoll_create用于创建epoll实例,epoll_ctl用于添加、删除或修改监控的文件描述符,epoll_wait用于等待事件并返回就绪的文件描述符集合。
epoll的核心优势在于其事件驱动机制。当文件描述符变为就绪状态时,内核会通过回调函数将其添加到就绪链表中。因此,epoll_wait只需要返回就绪的文件描述符集合,而无需遍历整个文件描述符集合。
源码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXEVENTS 10
int main() {
int lfd, cfd, epfd;
struct sockaddr_in seraddr, cliaddr;
struct epoll_event ev, events[MAXEVENTS];
int nfds, ret;
lfd = socket(AF_INET, SOCK_STREAM, 0);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8000);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
listen(lfd, 128);
epfd = epoll_create(10);
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
while (1) {
nfds = epoll_wait(epfd, events, MAXEVENTS, -1);
if (nfds == -1) {
perror("epoll_wait error");
exit(1);
}
for (int n = 0; n < nfds; ++n) {
if (events[n].data.fd == lfd) {
cfd = accept(lfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
ev.events = EPOLLIN;
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
} else {
char buf[1024];
int rr = read(events[n].data.fd, buf, sizeof(buf));
if (rr < 0) {
perror("read error");
exit(1);
} else if (rr == 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, events[n].data.fd, NULL);
close(events[n].data.fd);
} else {
write(events[n].data.fd, buf, rr);
}
}
}
}
return 0;
}
优势:
-
高效性:epoll通过红黑树和就绪链表高效地管理文件描述符,避免了大量的数据拷贝和遍历开销,显著提高了性能。
-
无文件描述符数量限制:epoll支持的文件描述符数量受限于系统最大文件描述符数,远超过select和poll的限制。
-
事件驱动机制:epoll采用事件驱动机制,只在有事件发生时通知用户进程,减少了不必要的系统调用和上下文切换。
劣势:
-
平台依赖性:epoll是Linux特有的机制,不具备跨平台性,这限制了其在非Linux系统上的使用。
-
复杂度较高:与select和poll相比,epoll的接口更加底层,需要更多的编程技巧和经验。
-
频繁系统调用:在大量描述符同时变更状态的情况下,epoll_ctl的系统调用开销可能较高。
应用:
由于epoll的高效性和灵活性,它在多个高性能中间件中得到了广泛应用,如Nginx、Redis、Memcached等。这些中间件需要处理大量的并发连接,epoll能够显著提高它们的性能和稳定性。
五、总结
select、poll和epoll是三种常见的IO多路复用方法,它们各有优缺点。select和poll适用于中小规模的并发场景,但在处理大规模并发连接时效率较低。epoll则专为大规模并发场景设计,通过高效的数据结构和事件驱动机制提供了显著的性能优势。然而,epoll的平台依赖性也限制了其在非Linux系统上的使用。在实际应用中,应根据具体需求选择合适的IO多路复用方法,并不是epoll就一定比select和poll好,虽然epoll却是应该是目前应用最广的IO多路复用方法。
原文地址:https://blog.csdn.net/u013135921/article/details/144385980
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!