IO多路复用

1.实现并发:

多线程、多进程、IO多路复用

IO多路复用(I/O Multiplexing)是一种能够让单个进程或线程同时处理多个I/O操作的技术,常用于网络编程中,尤其是在需要处理大量并发连接时。它的核心思想是通过非阻塞的方式,让程序能够在单一的线程或进程中同时监听多个I/O操作,而不需要为每一个连接创建一个独立的线程或进程。

2.常见技术

委托内核检测文件描述符(监听:1个,通信:n个)状态

1. select

select 是最早的一种IO多路复用方式,可以同时监听多个文件描述符的可读、可写和异常事件。它的优点是跨平台兼容性好,但存在以下缺点:

  • 文件描述符数量有限制(通常是1024)。

  • 性能在文件描述符数量较多时会降低,因为需要线性扫描所有文件描述符。

2. poll

pollselect的改进版本,能够同时监听多个文件描述符的可读、可写和异常事件。与select相比,poll没有文件描述符数量的限制,但其性能问题依然存在,因为poll也需要线性扫描文件描述符数组。

3. epoll

epoll 是Linux特有的IO多路复用机制,性能优于selectpoll,特别适用于高并发环境。epoll支持两种工作模式:(红黑树查询)

  • 水平触发(LT,Level Triggered):只要有数据可读或可写,就会通知应用程序。

  • 边缘触发(ET,Edge Triggered):只有当文件描述符的状态发生变化时才会通知应用程序。

epoll通过回调机制通知应用程序事件的发生,避免了线性扫描的开销,因此在处理大量并发连接时效率更高。

3.函数详解

1. select

流程图

select函数的原型如下:

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
  • nfds:指定要监视的文件描述符的最大值加1(即max(fd) + 1)。select会检查从0nfds-1的所有文件描述符。

  • readfds:指向fd_set类型的指针,表示需要检查可读性的文件描述符集合。如果为nullptr,表示不检查可读性。

  • writefds:指向fd_set类型的指针,表示需要检查可写性的文件描述符集合。如果为nullptr,表示不检查可写性。

  • exceptfds:指向fd_set类型的指针,表示需要检查异常条件的文件描述符集合。如果为nullptr,表示不检查异常条件。

  • timeout:指向timeval结构的指针,表示select调用的超时时间。如果为nullptrselect将阻塞直到有事件发生;如果timeout指向的timeval结构的值为{0, 0}select将立即返回。

fd_set是一个位集合,用于存储文件描述符。可以通过FD_ZEROFD_SETFD_CLRFD_ISSET宏来操作fd_set

  • FD_ZERO(fd_set* set):清空fd_set集合。

  • FD_SET(int fd, fd_set* set):将文件描述符fd添加到fd_set集合中。

  • FD_CLR(int fd, fd_set* set):从fd_set集合中移除文件描述符fd

  • FD_ISSET(int fd, fd_set* set):检查文件描述符fd是否在fd_set集合中。

timeval结构定义如下:

struct timeval {
    long tv_sec;  // 秒
    long tv_usec; // 微秒
};
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define MAX_CLIENTS 10
#define PORT 8080
#define BUFFER_SIZE 1024

// 设置服务器套接字
int setup_server_socket() {
    int server_fd;
    struct sockaddr_in server_addr;

    // 创建 TCP 套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听套接字
    if (listen(server_fd, MAX_CLIENTS) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    return server_fd;
}

// 处理客户端消息
void handle_client_message(int client_fd) {
    char buffer[BUFFER_SIZE];
    int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);

    if (bytes_read <= 0) {
        if (bytes_read == 0) {
            printf("Client disconnected\n");
        } else {
            perror("read failed");
        }
        close(client_fd);
    } else {
        buffer[bytes_read] = '\0';  // 确保缓冲区以 '\0' 结尾
        printf("Received from client: %s\n", buffer);
        // 回传收到的消息
        write(client_fd, buffer, bytes_read);
    }
}

int main() {
    int server_fd, new_client_fd, max_fd;
    struct sockaddr_in client_addr;
    socklen_t addr_len = sizeof(client_addr);
    fd_set read_fds, active_fds;

    server_fd = setup_server_socket();

    // 初始化文件描述符集合
    FD_ZERO(&active_fds);
    FD_SET(server_fd, &active_fds);
    max_fd = server_fd;

    while (1) {
        read_fds = active_fds;  // 每次调用 select 都需要重新设置

        // 使用 select 监控多个文件描述符
        int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
        if (activity < 0) {
            perror("select error");
            break;
        }

        // 检查是否有新的客户端连接
        if (FD_ISSET(server_fd, &read_fds)) {
            new_client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
            if (new_client_fd < 0) {
                perror("accept failed");
                continue;
            }
            printf("New client connected\n");

            FD_SET(new_client_fd, &active_fds);  // 将新连接的客户端添加到活动集合
            if (new_client_fd > max_fd) {
                max_fd = new_client_fd;  // 更新最大文件描述符
            }
        }

        // 处理所有已经连接的客户端
        for (int i = 0; i <= max_fd; i++) {
            if (FD_ISSET(i, &read_fds) && i != server_fd) {
                handle_client_message(i);  // 处理客户端的消息
            }
        }
    }

    close(server_fd);
    return 0;
}

2. poll

int poll(struct pollfd* fds, nfds_t nfds, int timeout);
  • fds:指向pollfd结构数组的指针,每个pollfd结构表示一个文件描述符及其感兴趣的事件。

  • nfdsfds数组中元素的数量。

  • timeout:指定poll调用的超时时间,单位为毫秒。如果为-1poll将阻塞直到有事件发生;如果为0poll将立即返回。

pollfd结构定义如下:

struct pollfd {
    int fd;       // 文件描述符
    short events; // 感兴趣的事件
    short revents;// 实际发生的事件
};
  • fd:需要监视的文件描述符。

  • events:指定对文件描述符感兴趣的事件,可以是以下值的组合:

    • POLLIN:数据可读。

    • POLLOUT:数据可写。

    • POLLERR:发生错误。

    • POLLHUP:挂起。

    • POLLNVAL:无效的文件描述符。

  • reventspoll返回时,revents字段会包含实际发生的事件,可以是events中指定的事件的组合。

3. epoll

epoll是一个基于事件的IO多路复用机制,主要由以下三个函数组成:

int epoll_create(int size);
  • size:指定epoll实例的大小(在Linux 2.6.8及更高版本中,这个参数已被忽略)。

  • 返回值:成功时返回一个非负的文件描述符,表示创建的epoll实例;失败时返回-1

epoll_ctl

用于向epoll实例中添加、修改或删除文件描述符:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
  • epfd:由epoll_create创建的epoll实例的文件描述符。

  • op:指定操作类型:

    • EPOLL_CTL_ADD:向epoll实例中添加文件描述符。

    • EPOLL_CTL_MOD:修改已注册的文件描述符的事件。

    • EPOLL_CTL_DEL:从epoll实例中删除文件描述符。

  • fd:需要操作的文件描述符。

  • event:指向epoll_event结构的指针,用于指定感兴趣的事件。对于EPOLL_CTL_DEL操作,event可以为nullptr

epoll_event结构定义如下:

struct epoll_event {
    uint32_t events; // 感兴趣的事件
    epoll_data_t data; // 用户数据
};
  • events:指定感兴趣的事件,可以是以下值的组合:

    • EPOLLIN:数据可读。

    • EPOLLOUT:数据可写。

    • EPOLLRDHUP:对端关闭连接。

    • EPOLLERR:发生错误。

    • EPOLLHUP:挂起。

  • data:用户数据,可以是文件描述符、指针或其他数据类型。

epoll_wait

等待epoll实例中的事件发生

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • epfd:由epoll_create创建的epoll实例的文件描述符。

  • events:指向epoll_event结构数组的指针,用于存储发生的事件。

  • maxeventsevents数组的最大容量。

  • timeout:指定epoll_wait调用的超时时间,单位为毫秒。如果为-1epoll_wait将阻塞直到有事件发生;如果为0epoll_wait将立即返回。

4.epoll

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

#define MAX_EVENTS 10

void handle_new_connection(int server_fd, int epoll_fd);
void handle_client_request(int client_fd);

int main() {
    int server_fd, epoll_fd, conn_fd;
    struct sockaddr_in server_addr;
    struct epoll_event event, events[MAX_EVENTS];
    int nfds, port = 8080;

    // 创建套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 初始化地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_fd, 5) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

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

    // 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) {
        perror("epoll_create1 failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 将服务器套接字添加到 epoll 实例
    event.data.fd = server_fd;
    event.events = EPOLLIN | EPOLLET; // 边缘触发
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) {
        perror("epoll_ctl: server_fd");
        close(server_fd);
        close(epoll_fd);
        exit(EXIT_FAILURE);
    }

    // 等待事件
    while (1) {
        nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait failed");
            break;
        }

        for (int n = 0; n < nfds; ++n) {
            if (events[n].data.fd == server_fd) {
                // 处理新连接
                handle_new_connection(server_fd, epoll_fd);
            } else if (events[n].events & EPOLLIN) {
                // 处理客户端请求
                handle_client_request(events[n].data.fd);
            }
        }
    }

    // 清理
    close(server_fd);
    close(epoll_fd);
    return 0;
}

void handle_new_connection(int server_fd, int epoll_fd) {
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int client_fd;

    while ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len)) > 0) {
        printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        // 将客户端套接字设置为非阻塞模式
        int flags = fcntl(client_fd, F_GETFL, 0);
        fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);

        // 将客户端套接字添加到 epoll 实例
        struct epoll_event event;
        event.data.fd = client_fd;
        event.events = EPOLLIN | EPOLLET; // 边缘触发
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) {
            perror("epoll_ctl: client_fd");
            close(client_fd);
        }
    }

    if (client_fd == -1) {
        if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) {
            perror("accept failed");
        }
    }
}

void handle_client_request(int client_fd) {
    char buffer[1024];
    ssize_t bytes_read;

    while ((bytes_read = read(client_fd, buffer, sizeof(buffer) - 1)) > 0) {
        buffer[bytes_read] = '\0';
        printf("Received from client: %s\n", buffer);

        // 回复客户端
        write(client_fd, buffer, bytes_read);
    }

    if (bytes_read == 0) {
        printf("Client disconnected\n");
        close(client_fd);
    } else if (bytes_read == -1) {
        if (errno != EAGAIN && errno != EINTR) {
            perror("read failed");
            close(client_fd);
        }
    }
}

1.epoll 的三大核心优势

1. O(1) 时间复杂度的事件检测

  • 传统 select/poll 缺陷

    • 每次调用需要遍历所有监控的文件描述符(FD)

    • 时间复杂度 O(n),万级连接时性能急剧下降

  • epoll 优化

    • 通过红黑树(epoll_ctl)和就绪链表(epoll_wait)分离监控与事件获取

    • 仅返回就绪的 FD,与总连接数无关

    • 注册用红黑树是 O(logN),触发是就绪链表 O(1),整体远快于 poll/selectO(N)

2. 无重复内存拷贝

  • select/poll

    • 每次调用需要将 FD 集合从用户态拷贝到内核态

    • 万级连接时内存拷贝开销显著

  • epoll

    • 通过 epoll_ctl 预先注册 FD 到内核

    • epoll_wait 只需传递一个空的事件数组

    • 内存开销对比(监控1万个FD):

      方法单次系统调用内存拷贝量
      select~80KB(sizeof(fd_set))
      epoll~4KB(epoll_event数组)

3. 支持边缘触发(ET)模式

  • 水平触发(LT)

    • 默认模式,只要 FD 可读/可写就会持续通知

    • 可能导致重复唤醒

  • 边缘触发(ET)

    • 仅在状态变化时通知一次

    • 必须一次性处理完所有数据

    • 性能提升场景

2.epoll 的底层实现

1. 关键数据结构

组件数据结构作用
兴趣列表红黑树存储所有监控的 FD(快速查找/插入/删除)
就绪队列双向链表存放已就绪的 FD 事件
回调机制内核回调函数当 FD 就绪时自动加入就绪队列

2. 工作流程

[用户态]
   │
   ├── epoll_create() 创建epoll实例
   │
   ├── epoll_ctl()    添加/修改/删除监控的FD(红黑树维护)
   │
   └── epoll_wait()   获取就绪事件(从就绪链表拷贝事件数组)
           ↑
[内核态]    │
   └── 当FD就绪时,内核回调函数将其加入就绪链表
  • 将它插入 epoll 的 红黑树 rbr,用于快速查找管理;

  • 为该 fd 的“事件触发点”注册一个 回调函数 ep_poll_callback

  • 当 fd 上的事件发生,比如 socket 可读,会调用这个回调函数;

  • 回调函数会把对应 epitem 加入到 epoll 实例的 rdlist 链表

  • 然后你调用 epoll_wait(),从这个链表里直接取出就绪 fd。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值