当前位置: 首页 > news >正文

select、poll、epoll的区别

select、poll、epoll均为linux中的多路复用技术。3种技术出现的顺序是select、poll、epoll,3个版本反应了多路复用技术的迭代过程。我们现在开发网络应用时, 一般都会使用多路复用,很少有用一个线程来监听一个fd的,其中epoll又是最常使用的。关于epoll的实现和常见问题可以参考epoll实现原理和常见问题总结。

当我们在使用epoll的时候,会想当然的认为这种技术是理所应当的,这就是多路复用技术应该的样子。殊不知,从select到poll再到epoll也经历了一定的过程,epoll并不是一开始就出现的。epoll为什么出现,这里边既有技术的进步,也有客观使用的需求,特别是随着互联网的发展,需要监听的fd越来越多,select和poll已经不能满足实际的使用需求,所以才产生了epoll。而现在,我们也可以想一想,epoll就是linux中多路复用技术最终的形态了吗,还有没有可以优化的空间呢?

本文首先记录三种多路复用技术的使用方式,然后总结3者的区别。3个例子,均使用tcp socket,客户端创建5个连接,服务端分别使用select,poll和epoll来监听事件。

1select

客户端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 12345
#define SERVER_IP "192.168.74.128"
#define NUM_CONNECTIONS 5
#define MESSAGE "Hello from client"int main() {int client_fds[NUM_CONNECTIONS] = {0};struct sockaddr_in serv_addr;char buffer[1024] = {0};int i = 0;for (i = 0; i < NUM_CONNECTIONS; i++) {if ((client_fds[i] = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket creation error");exit(EXIT_FAILURE);}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT);if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {perror("invalid address or address not supported");exit(EXIT_FAILURE);}if (connect(client_fds[i], (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {perror("connection failed");exit(EXIT_FAILURE);}}for (i = 0; i < NUM_CONNECTIONS; i++) {if (client_fds[i] > 0) {send(client_fds[i], MESSAGE, strlen(MESSAGE), 0);printf("data sent to client %d\n", i + 1);}}for (i = 0; i < NUM_CONNECTIONS; i++) {close(client_fds[i]);}return 0;
}

服务端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>#define SERVER_IP ("192.168.74.128")
#define PORT 12345
#define MAX_CLIENTS 5
#define BUFFER_SIZE 1024int set_reuse_addr(int fd) {int so_reuse = 1;if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &so_reuse, sizeof(so_reuse)) < 0) {printf("set reuse addr failed.\n");return -1;}return 0;
}int main() {int server_fd;int new_fd;int max_fd;int activity;int i;int client_sockets[MAX_CLIENTS];struct sockaddr_in address;fd_set readfds;char buffer[BUFFER_SIZE];for (i = 0; i < MAX_CLIENTS; i++) {client_sockets[i] = 0;}if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}set_reuse_addr(server_fd);address.sin_family = AF_INET;address.sin_addr.s_addr = inet_addr(SERVER_IP);address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}max_fd = server_fd;printf("waiting for connections...\n");while (1) {FD_ZERO(&readfds);FD_SET(server_fd, &readfds);for (i = 0; i < max_fd; i++) {int tmp_fd = client_sockets[i];if (tmp_fd > 0) {FD_SET(tmp_fd, &readfds);}if (tmp_fd > max_fd) {max_fd = tmp_fd;}}activity = select(max_fd + 1, &readfds, NULL, NULL, NULL);if ((activity < 0) && (errno != EINTR)) {perror("select error");}if (FD_ISSET(server_fd, &readfds)) {if ((new_fd = accept(server_fd, NULL, NULL)) < 0) {perror("accept");exit(EXIT_FAILURE);}printf("New connection, socket fd is %d\n", new_fd);if(new_fd > max_fd) {max_fd = new_fd;}for (i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] == 0) {client_sockets[i] = new_fd;break;}}}for (i = 0; i < max_fd; i++) {int tmp_fd = client_sockets[i];if (FD_ISSET(tmp_fd, &readfds)) {int valread = read(tmp_fd, buffer, BUFFER_SIZE);if (valread == 0) {close(tmp_fd);client_sockets[i] = 0;} else {buffer[valread] = '\0';printf("data from %d: %s\n", tmp_fd, buffer);}}}}return 0;
}

1.1select第一个参数为什么要传max_fd+1

select的系统调用如下,第一个参数n是需要监听的最大fd+1。为什么加1呢,因为要遍历到所有的fd,而fd是从0开始的,遍历的时候是一个for循环for(int i=0; i < n; i++),只有n为max_fd+1才能遍历到max_fd。假如要监听的fd是0、1、2、3、4、5、6、7、8、9、10,那么第一个参数就应该传11。注意这里的n不是表示select需要监听的fd的个数,而只是和最大fd有关,假如只需要监听一个fd,fd是10,那么n也应该传11。

SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,

        fd_set __user *, exp, struct __kernel_old_timeval __user *, tvp)

{

    return kern_select(n, inp, outp, exp, tvp);

}

在内核中select最终会调用到do_select,可以看到在该函数中,使用for循环来遍历监听的fd。

static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{...for (;;) {...for (i = 0; i < n; ++rinp, ++routp, ++rexp) {...}...}...
}

1.21024限制

使用select最多只能监听1024个文件描述符,并且fd的取值范围需要在[0,1023]之内。如果你只需要监听一个fd,这个fd是1024,那么这个fd也是不能监听的。

从select的形参可以看到,select将要监听的fd放到3个fd_set中,分别是可读、可写、错误。fd_set是一个bitmap,实现如下,所以可以得出,一个fd_set中最多可以保存1024个fd。

#undef __FD_SETSIZE

#define __FD_SETSIZE    1024

typedef struct {

    unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];

} __kernel_fd_set;

1.3性能不友好

从select的使用过程可以看到,在使用select的时候,有以下几点对性能不友好:

①假如我们要监听100个fd,fd从0到99,那么在select的内核实现中,需要对每个fd进行判断,判断这个fd上有没有事件;当select返回之后,用户也需要遍历100个fd来判断是不是有事件。假如这个时候只有1个fd上有事件,那么有99次判断都是空转的,浪费cpu资源。

②从select的使用上可以看到,每次调用select之前,用户想要监听哪些fd的可读事件,哪些fd的可写事件,哪些fd的错误事件,都需要分别设置3个fd set,每次都要设置。内核也会设置这些fd,如果我们要监听一个fd的可读事件,假设fd是10,那么用户会将fd设置到inp中,内核中会判断这个文件是不是有可读事件,有事件则select返回之后,在inp中还包含10这个fd;否则,就不包含了。

2poll

poll服务端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>#define SERVER_IP ("192.168.74.128")
#define PORT 12345
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024int set_reuse_addr(int fd) {int so_reuse = 1;if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &so_reuse, sizeof(so_reuse)) < 0) {printf("set reuse addr failed.\n");return -1;}return 0;
}int main() {int server_fd;int new_socket;int i;struct sockaddr_in address;struct pollfd fds[MAX_CLIENTS + 1]; // 额外一个用于监听新连接的文件描述符int nfds = 1; // 初始只有服务器套接字char buffer[BUFFER_SIZE];int poll_result;// 创建服务器套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置服务器地址address.sin_family = AF_INET;address.sin_addr.s_addr = inet_addr(SERVER_IP);address.sin_port = htons(PORT);set_reuse_addr(server_fd);// 绑定套接字if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 监听连接if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}// 初始化 pollfd 结构体memset(fds, 0, sizeof(fds));fds[0].fd = server_fd;fds[0].events = POLLIN;printf("Waiting for connections...\n");while (1) {// 等待事件发生poll_result = poll(fds, nfds, -1); // 阻塞直到有事件发生if (poll_result < 0) {perror("poll error");exit(EXIT_FAILURE);}// 检查是否有新的连接请求if (fds[0].revents & POLLIN) {if ((new_socket = accept(server_fd, NULL, NULL)) < 0) {perror("accept");exit(EXIT_FAILURE);}printf("New connection, socket fd is %d\n", new_socket);// 将新连接添加到 pollfd 结构体中if (nfds < MAX_CLIENTS + 1) {fds[nfds].fd = new_socket;fds[nfds].events = POLLIN;nfds++;} else {printf("Maximum number of clients reached.\n");close(new_socket);}}// 处理所有客户端的事件for (i = 1; i < nfds; i++) {if (fds[i].revents & POLLIN) {int valread = read(fds[i].fd, buffer, BUFFER_SIZE);if (valread == 0) {// 客户端断开连接close(fds[i].fd);// 将断开的套接字从 pollfd 数组中移除fds[i] = fds[nfds - 1];nfds--;} else {// 打印客户端发来的消息buffer[valread] = '\0';printf("Client %d sent: %s\n", fds[i].fd, buffer);}}}}return 0;
}

2.1poll的进步

①fd没有1024限制,可以将服务端代码中的MAX_CLIENTS改大,比如改成1500,同时也将客户端NUM_CONNECTIONS改成1500,这样就可以使用poll监听最大为1500的fd。这样的测试,首先要修改ulimit,默认情况下,系统限制每个进程可以打开的fd为1024个,如果我们想将这个参数调大,比如调到2048,那么可以通过ulimit -n 2048来修改。

②poll接口使用简化,首先在使用poll的时候不需要把fd放到不同的fd_set(读、写、错误)中,使用一个结构体struct pollfd来描述要监听的事件,其中events表示用户要监听的事件类型,POLLIN读事件,POLLOUT表示写事件,其它事件表示错误;其次用户要监听的事件和实际存在的事件进行了隔离,revents表示返回的事件,这样就不需要每次调用poll之前先要对每个fd进行设置了,只需要一开始设置一次即可。

struct pollfd {int   fd;         /* file descriptor */short events;     /* requested events */short revents;    /* returned events */
};

3epoll

poll相对于select,在易用性上有一些进步,但是使用poll的时候,仍然需要对所有fd进行遍历,也就是说即使这次poll,一个fd上没有事件,那么仍然需要进行一次判断,导致了cpu资源的浪费。

server服务端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define SERVER_IP ("192.168.74.128")
#define PORT 12345
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024int main() {int server_fd;int client_fd;int epoll_fd;int nfds;int i;struct sockaddr_in address;struct epoll_event ev, events[MAX_EVENTS];char buffer[BUFFER_SIZE];// 创建服务器套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {perror("socket");exit(EXIT_FAILURE);}// 设置服务器地址address.sin_family = AF_INET;address.sin_addr.s_addr = inet_addr(SERVER_IP);;address.sin_port = htons(PORT);// 绑定套接字if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == -1) {perror("bind");exit(EXIT_FAILURE);}// 监听连接if (listen(server_fd, 5) == -1) {perror("listen");exit(EXIT_FAILURE);}// 创建 epoll 实例if ((epoll_fd = epoll_create1(0)) == -1) {perror("epoll_create1");exit(EXIT_FAILURE);}// 添加监听套接字到 epollev.events = EPOLLIN;ev.data.fd = server_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {perror("epoll_ctl");exit(EXIT_FAILURE);}printf("Waiting for connections...\n");while (1) {nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds == -1) {perror("epoll_wait");exit(EXIT_FAILURE);}for (i = 0; i < nfds; i++) {if (events[i].data.fd == server_fd) {// 新的连接while ((client_fd = accept(server_fd, NULL, NULL)) != -1) {printf("New connection, socket fd is %d\n", client_fd);ev.events = EPOLLIN | EPOLLET;ev.data.fd = client_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {perror("epoll_ctl");exit(EXIT_FAILURE);}}if (client_fd == -1) {perror("accept");}} else {// 处理客户端数据int fd = events[i].data.fd;int bytes_read = read(fd, buffer, BUFFER_SIZE);if (bytes_read == 0) {// 客户端断开连接close(fd);epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);} else if (bytes_read > 0) {buffer[bytes_read] = '\0';printf("Received message from client %d: %s\n", fd, buffer);} else {perror("read");}}}}close(server_fd);close(epoll_fd);return 0;
}

3.1epoll的进步

epoll解决了select和poll都存在的问题,epoll_wait返回之后,返回的结果中fd都是有事件的,不会造成cpu空转。也就是说如果select、poll、epoll都监听100个fd,而此时只有一个fd有事件,那么select和poll需要对100个fd进行判断,epoll只需要对这一个有事件的fd进行处理即可。

4区别

selectpollepoll
fd个数限制有限制无限制无限制
对所有fd进行遍历全部遍历全部遍历只需要遍历有事件的fd
目标事件和返回事件是不是共享共享,每次都要设置不共享不共享

在fd个数限制方面,select有限制,poll和epoll均没有限制。

对所有fd进行遍历方面,select和poll都需要对所有fd进行遍历,有事件则处理,没事件,则这次空转;epoll返回的结果一定是有事件的。

目标事件和返回的事件是不是共享,select是共享的,每次调用select之间,都需要设置3个fdset(读、写、错误),select返回之后,再判断是不是有事件;poll中是隔离开的,用户监听的事件是events,返回的事件是revents,不需要每次调用poll之前都设置events;epoll是隔离开的,用户监听的事件通过epoll_ctl设置,返回的事件是epoll_wait中的第二个参数。

现在,当我们开发网络应用时,几乎全部选择epoll,epoll的优点是显而易见的。但是epoll也是有缺点的,只不过epoll的缺点相对于优点来说,可以接受。epoll会占用一个fd,也就是会消耗一个fd资源;epoll在内核态的实现增加了多个数据结构,比如一个红黑树来管理所有需要监听的fd,一个双向链表来管理已有的事件等。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 组合模式composite
  • MySql约束练习
  • 5.3.数据结构-c/c++二叉树代码
  • C语言-第八章:指针进阶
  • 保研 比赛 利器: 用AI比赛助手降维打击数学建模
  • 内推|京东|后端开发|运维|算法...|北京 更多岗位扫内推码了解,直接投递,跟踪进度
  • 数据传输安全——混合加解密
  • 压缩PDF,介绍这五种压缩方案
  • 什么是Web服务器集群?
  • springboot服务器文件读取工具类
  • 一文梳理RAG(检索增强生成)的现状与挑战
  • Go语言结构体和元组全面解析
  • 【IPV6从入门到起飞】4-RTMP推流,ffmpeg拉流,纯HTML网页HLS实时直播
  • PyTorch 卷积层详解
  • 什么是银行挤兑
  • JavaScript 如何正确处理 Unicode 编码问题!
  • 2017 前端面试准备 - 收藏集 - 掘金
  • 2019年如何成为全栈工程师?
  • css的样式优先级
  • electron原来这么简单----打包你的react、VUE桌面应用程序
  • JAVA多线程机制解析-volatilesynchronized
  • Java精华积累:初学者都应该搞懂的问题
  • rc-form之最单纯情况
  • 阿里云应用高可用服务公测发布
  • 百度小程序遇到的问题
  • 得到一个数组中任意X个元素的所有组合 即C(n,m)
  • 经典排序算法及其 Java 实现
  • 配置 PM2 实现代码自动发布
  • 双管齐下,VMware的容器新战略
  • 通过获取异步加载JS文件进度实现一个canvas环形loading图
  • 追踪解析 FutureTask 源码
  • puppet连载22:define用法
  • 机器人开始自主学习,是人类福祉,还是定时炸弹? ...
  • 进程与线程(三)——进程/线程间通信
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • (145)光线追踪距离场柔和阴影
  • (4)(4.6) Triducer
  • (Java入门)抽象类,接口,内部类
  • (LNMP) How To Install Linux, nginx, MySQL, PHP
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (solr系列:一)使用tomcat部署solr服务
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (解决办法)ASP.NET导出Excel,打开时提示“您尝试打开文件'XXX.xls'的格式与文件扩展名指定文件不一致
  • (续)使用Django搭建一个完整的项目(Centos7+Nginx)
  • (一)C语言之入门:使用Visual Studio Community 2022运行hello world
  • (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
  • (转)Groupon前传:从10个月的失败作品修改,1个月找到成功
  • (转)http协议
  • .gitignore文件设置了忽略但不生效
  • .NET delegate 委托 、 Event 事件,接口回调
  • .NET Framework与.NET Framework SDK有什么不同?
  • .NET 分布式技术比较