select、poll、epoll的区别

2024-09-08 03:36
文章标签 区别 select epoll poll

本文主要是介绍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,一个双向链表来管理已有的事件等。

这篇关于select、poll、epoll的区别的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1147001

相关文章

native和static native区别

本文基于Hello JNI  如有疑惑,请看之前几篇文章。 native 与 static native java中 public native String helloJni();public native static String helloJniStatic();1212 JNI中 JNIEXPORT jstring JNICALL Java_com_test_g

Android fill_parent、match_parent、wrap_content三者的作用及区别

这三个属性都是用来适应视图的水平或者垂直大小,以视图的内容或尺寸为基础的布局,比精确的指定视图的范围更加方便。 1、fill_parent 设置一个视图的布局为fill_parent将强制性的使视图扩展至它父元素的大小 2、match_parent 和fill_parent一样,从字面上的意思match_parent更贴切一些,于是从2.2开始,两个属性都可以使用,但2.3版本以后的建议使

easyui 验证下拉菜单select

validatebox.js中添加以下方法: selectRequired: {validator: function (value) {if (value == "" || value.indexOf('请选择') >= 0 || value.indexOf('全部') >= 0) {return false;}else {return true;}},message: '该下拉框为必选项'}

Collection List Set Map的区别和联系

Collection List Set Map的区别和联系 这些都代表了Java中的集合,这里主要从其元素是否有序,是否可重复来进行区别记忆,以便恰当地使用,当然还存在同步方面的差异,见上一篇相关文章。 有序否 允许元素重复否 Collection 否 是 List 是 是 Set AbstractSet 否

javascript中break与continue的区别

在javascript中,break是结束整个循环,break下面的语句不再执行了 for(let i=1;i<=5;i++){if(i===3){break}document.write(i) } 上面的代码中,当i=1时,执行打印输出语句,当i=2时,执行打印输出语句,当i=3时,遇到break了,整个循环就结束了。 执行结果是12 continue语句是停止当前循环,返回从头开始。

maven发布项目到私服-snapshot快照库和release发布库的区别和作用及maven常用命令

maven发布项目到私服-snapshot快照库和release发布库的区别和作用及maven常用命令 在日常的工作中由于各种原因,会出现这样一种情况,某些项目并没有打包至mvnrepository。如果采用原始直接打包放到lib目录的方式进行处理,便对项目的管理带来一些不必要的麻烦。例如版本升级后需要重新打包并,替换原有jar包等等一些额外的工作量和麻烦。为了避免这些不必要的麻烦,通常我们

ActiveMQ—Queue与Topic区别

Queue与Topic区别 转自:http://blog.csdn.net/qq_21033663/article/details/52458305 队列(Queue)和主题(Topic)是JMS支持的两种消息传递模型:         1、点对点(point-to-point,简称PTP)Queue消息传递模型:         通过该消息传递模型,一个应用程序(即消息生产者)可以

多路转接之select(fd_set介绍,参数详细介绍),实现非阻塞式网络通信

目录 多路转接之select 引入 介绍 fd_set 函数原型 nfds readfds / writefds / exceptfds readfds  总结  fd_set操作接口  timeout timevalue 结构体 传入值 返回值 代码 注意点 -- 调用函数 select的参数填充  获取新连接 注意点 -- 通信时的调用函数 添加新fd到

深入探讨:ECMAScript与JavaScript的区别

在前端开发的世界中,JavaScript无疑是最受欢迎的编程语言之一。然而,很多开发者在使用JavaScript时,可能并不清楚ECMAScript与JavaScript之间的关系和区别。本文将深入探讨这两者的不同之处,并通过案例帮助大家更好地理解。 一、什么是ECMAScript? ECMAScript(简称ES)是一种脚本语言的标准,由ECMA国际组织制定。它定义了语言的语法、类型、语句、

Lua 脚本在 Redis 中执行时的原子性以及与redis的事务的区别

在 Redis 中,Lua 脚本具有原子性是因为 Redis 保证在执行脚本时,脚本中的所有操作都会被当作一个不可分割的整体。具体来说,Redis 使用单线程的执行模型来处理命令,因此当 Lua 脚本在 Redis 中执行时,不会有其他命令打断脚本的执行过程。脚本中的所有操作都将连续执行,直到脚本执行完成后,Redis 才会继续处理其他客户端的请求。 Lua 脚本在 Redis 中原子性的原因