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 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区别

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,一个双向链表来管理已有的事件等。

  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值