linux下的各种I/O多路复用详解---select/poll/epoll

多路复用技术是一种通过共享物理信道实现多路信号传输的通信技术,其核心原理是利用复用器将多路信号合成单一信号传输,接收端通过分用器分离还原。该技术主要包括频分复用(FDM)、时分复用(TDM)、波分复用(WDM)、码分多址(CDMA)和空分多址(SDMA)等类型,其中FDM通过划分频段实现ADSL数据传输,TDM采用时分片机制支持T1载波传输,WDM通过不同波长光信号在光纤中实现双向通信。(该部分来源于百度百科)

select的概念和原理

select 是一种事件通知机制,通常用于阻塞式 I/O 操作中,它允许程序在多个文件描述符上进行监视,一旦某个文件描述符变得可读、可写或出现异常,select 会通知程序进行相应的处理。

select 会在多个文件描述符上设置监视(例如,读、写、异常状态),然后它会阻塞程序,直到以下事件之一发生:

  • 读事件:某个文件描述符准备好读取数据(如套接字接收到数据)。
  • 写事件:某个文件描述符准备好写入数据(如套接字可以发送数据)。
  • 异常事件:文件描述符处于异常状态(如套接字连接关闭)。

select 会返回发生事件的文件描述符列表,程序可以通过这些文件描述符来进一步处理数据。

select原型

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:最大文件描述符值加1。通常可以传入所有需要监视的文件描述符的最大值加1。
  • readfds:用于监视哪些文件描述符可以读取(即数据已准备好读取)。
  • writefds:用于监视哪些文件描述符可以写入。
  • exceptfds:用于监视哪些文件描述符发生了异常。
  • timeout:指定等待事件的最长时间。如果为 NULL,则 select 会阻塞直到有事件发生;如果设置为 0,则为非阻塞模式;否则,设置为一个 timeval 结构体表示的超时时间。

select的使用

struct timeval timeout;
timeout.tv_sec = 5;  // 等待最多5秒
timeout.tv_usec = 0;

int ready = select(socket_fd + 1, &readfds, NULL, NULL, &timeout);
if (ready < 0) {
    perror("select error");
} else if (ready == 0) {
    printf("timeout\n");
} else {
    if (FD_ISSET(socket_fd, &readfds)) {
        // 如果 socket_fd 可读,进行读取操作
        char buffer[1024];
        int len = read(socket_fd, buffer, sizeof(buffer));
        if (len > 0) {
            printf("Received data: %s\n", buffer);
        }
    }
}

select的不足

文件描述符数量限制 select 的最大文件描述符数目是由 fd_set 中的位数限制的,通常在 1024 或 4096 左右。超出这个数量,select 将无法处理。因此,在高并发的应用场景下,select 可能不够高效。

性能瓶颈 当需要监视大量的文件描述符时,select 会不断地遍历所有文件描述符,检查哪些描述符有事件发生。这可能导致性能瓶颈,特别是在大规模的并发连接情况下。

无法支持优先级 select 不支持文件描述符的优先级调度,因此它处理多个文件描述符时是公平的,不会根据优先级处理某些请求。

poll的概念和原理

pollselect 的一个改进版本,常用于多路复用(I/O多路复用)的场景,它允许程序监控多个文件描述符的状态(如是否可以读写)。

poll原型

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:一个 pollfd 结构体数组,表示要监视的文件描述符及其关注的事件。
  • nfds:要监视的文件描述符的数量,即 fds 数组的大小。
  • timeout:指定 poll 阻塞等待的时间(单位是毫秒)。如果为负数,则阻塞直到有事件发生;如果为 0,则立即返回;如果为正数,则等待指定的时间后返回。

poll的使用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>

#define PORT 8080

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addr_len = sizeof(address);

    // 创建服务器套接字
    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 = INADDR_ANY;
    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 failed");
        exit(EXIT_FAILURE);
    }

    // 创建 pollfd 结构体
    struct pollfd fds[1];
    fds[0].fd = server_fd;
    fds[0].events = POLLIN;  // 关注可读事件

    printf("Server is listening on port %d...\n", PORT);

    while (1) {
        // 调用 poll,等待事件
        int ret = poll(fds, 1, -1); // -1 表示无限阻塞
        if (ret < 0) {
            perror("poll failed");
            exit(EXIT_FAILURE);
        }

        // 检查是否有可读事件
        if (fds[0].revents & POLLIN) {
            new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addr_len);
            if (new_socket < 0) {
                perror("accept failed");
                continue;
            }
            printf("New connection established\n");

            // 读取数据
            char buffer[1024] = {0};
            int valread = read(new_socket, buffer, sizeof(buffer));
            if (valread > 0) {
                printf("Received message: %s\n", buffer);
            }
            close(new_socket);
        }
    }

    return 0;
}

poll的缺点和优点

优点

  • 没有文件描述符数量的限制:不像 select 限制了文件描述符数量,poll 可以处理更大规模的文件描述符。
  • 灵活的事件选择:通过 pollfd 可以灵活地指定感兴趣的事件,并且能够处理多种类型的事件。

缺点

  • 性能瓶颈:在大量文件描述符的情况下,poll 仍然会遍历所有文件描述符,因此它在大规模并发连接时的性能较低。
  • 没有优先级poll 处理事件的方式是均等的,没有优先级调度功能。

epoll的概念和原理

epoll 是 Linux 下提供的高效的 I/O 多路复用机制,特别适用于处理大量并发连接。与 selectpoll 相比,epoll 提供了更高效的方式来管理大量文件描述符。

epoll相关函数

epoll_create:创建一个 epoll 实例,返回一个文件描述符。
int epoll_create(int size);
size 参数已被废弃,但它仍然被要求传递。通常传入一个大于零的数。

epoll_ctl:用于添加、删除或修改事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create 返回的 epoll 文件描述符。
op:操作类型(EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD)。
fd:待操作的文件描述符。
event:待监控的事件结构。

epoll_wait:等待事件发生,并返回就绪的文件描述符。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:epoll_create 返回的 epoll 文件描述符。
events:用于存放就绪事件的数组。
maxevents:数组大小。
timeout:等待时间(单位毫秒),可以设置为 -1(永久阻塞),0(非阻塞),或正数(指定超时时间)。

epoll的使用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define PORT 8080
#define MAX_EVENTS 10

int main() {
    int server_fd, client_fd, epfd, n;
    struct sockaddr_in address;
    struct epoll_event ev, events[MAX_EVENTS];
    int addr_len = sizeof(address);

    // 创建服务器套接字
    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 = INADDR_ANY;
    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 failed");
        exit(EXIT_FAILURE);
    }

    // 创建 epoll 实例
    epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create failed");
        exit(EXIT_FAILURE);
    }

    // 将 server_fd 添加到 epoll 监控列表中
    ev.events = EPOLLIN;
    ev.data.fd = server_fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
        perror("epoll_ctl failed");
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %d...\n", PORT);

    while (1) {
        // 等待事件发生
        n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (n == -1) {
            perror("epoll_wait failed");
            exit(EXIT_FAILURE);
        }

        // 处理所有就绪事件
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == server_fd) {
                // 新连接到达
                client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addr_len);
                if (client_fd == -1) {
                    perror("accept failed");
                    continue;
                }

                // 将 client_fd 添加到 epoll 监控列表中
                ev.events = EPOLLIN;
                ev.data.fd = client_fd;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
                    perror("epoll_ctl failed");
                    exit(EXIT_FAILURE);
                }
                printf("New connection established\n");
            } else if (events[i].events & EPOLLIN) {
                // 有数据可读
                char buffer[1024] = {0};
                int len = read(events[i].data.fd, buffer, sizeof(buffer));
                if (len > 0) {
                    printf("Received: %s\n", buffer);
                } else {
                    // 客户端关闭连接
                    printf("Closing connection\n");
                    close(events[i].data.fd);
                }
            }
        }
    }

    close(server_fd);
    close(epfd);
    return 0;
}

epoll的优点

  • 高效性epoll 使用事件驱动模式,当有文件描述符就绪时才通知应用程序,因此避免了 poll 和 select 中每次都要遍历所有文件描述符的性能问题。
  • 支持大规模并发:在大量并发连接的情况下,epoll 处理效率更高,尤其是当活跃的文件描述符很少时,它只会通知那些就绪的文件描述符,避免了不必要的检查。
  • 支持边缘触发和水平触发epoll 提供了两种触发模式,可以根据需要选择。

总结

特性selectpollepoll
最大文件描述符限制(通常 1024)没有最大文件描述符限制没有文件描述符数量的限制
效率随着文件描述符增加,性能下降随着文件描述符增加,性能下降高效,性能不随文件描述符数量变化
API设计每次调用时需要重设文件描述符使用结构体数组使用事件驱动,灵活且高效
操作方式阻塞或非阻塞阻塞或非阻塞阻塞、非阻塞、事件驱动
适用场景文件描述符数量较少,简单应用文件描述符数量适中,但不高高并发场景,如大规模网络服务
优点简单易用较灵活,解决了 select 限制极高的性能,无限制的并发处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小龙学IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值