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 1024
int 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 1024
int 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 1024
int 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);
}
// 添加监听套接字到 epoll
ev.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区别
select | poll | epoll | |
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,一个双向链表来管理已有的事件等。