IO多路复用:select、poll、epoll

一.阻塞非阻塞IO

阻塞IO:阻塞 I/O 是指当一个 I/O 操作被请求时,调用该操作的线程或进程会一直等待,直到 I/O 操作完成后才继续执行。例如,调用 recvfrom()等函数时,如果没有数据到达,进程会一直阻塞等待,直到数据到达并读取完成。

阻塞IO缺点:阻塞IO存在两次上下文切换,且一般单线程只能监控单连接。第一次阻塞发生在用户太读取数据数据并未准备好,用户态进程会被挂起并切换至内核态。第二次阻塞发生在从内核RingBuffer拷贝数据至用户空间的时候,数据拷贝完毕会发生内核态到用户空间的上下文切换。

非阻塞IO:非阻塞 I/O 是指 I/O 操作在被调用时,如果无法立即完成,则不会让线程或进程阻塞,而是立刻返回。调用方可以重复请求或轮询该操作的状态,直到 I/O 操作完成。例如, recv() 进行非阻塞读取时,如果没有数据到达,则立即返回一个错误,调用方可以在稍后再次调用检查数据是否已到达。非阻塞IO的CPU开销较大,且存在一次进程上下切换,因为Linux内核本质上不存在异步IO(内核缓存区拷贝数据时)。

IO多路复用:I/O 多路复用(I/O Multiplexing)是一种在单个线程或进程中同时监控多个 I/O 描述符(如 socket)的技术。通过 I/O 多路复用,程序可以在一个线程中等待多个 I/O 操作完成,无需为每个 I/O 操作创建独立的线程或进程,从而高效地处理大量并发 I/O 请求。

二.Select

select() 函数用于监控多个文件描述符(如 sockets)是否发生了 I/O 事件(如可读、可写、异常),从而实现 I/O 多路复用。它会阻塞等待,直到至少一个文件描述符状态发生变化或超时后返回。

cpp 复制代码
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

int select(
    int nfds,                     // 监控的文件描述符集里最大文件描述符加1
    fd_set *readfds,              // 监控有读数据到达文件描述符集合,引用类型的参数
    fd_set *writefds,             // 监控写数据到达文件描述符集合,引用类型的参数
    fd_set *exceptfds,            // 监控异常发生达文件描述符集合,引用类型的参数
    struct timeval *timeout);     // 定时阻塞监控时间

fd_set 文件描述符集合

select 函数参数中的 fd_set 类型表示文件描述符的集合。

由于文件描述符 fd 是一个从 0 开始的无符号整数,所以可以使用 fd_set 的二进制每一位来表示一个文件描述符。某一位为 1,表示对应的文件描述符已就绪。比如比如设 fd_set 长度为 1 字节,则一个 fd_set 变量最大可以表示 8 个文件描述符。当 select 返回 fd_set = 00010011 时,表示文件描述符 1、2、5 已经就绪。

cpp 复制代码
#include <sys/select.h>
int FD_ZERO(int fd, fd_set *fdset);  // 将 fd_set 所有位置 0
int FD_CLR(int fd, fd_set *fdset);   // 将 fd_set 某一位置 0
int FD_SET(int fd, fd_set *fd_set);  // 将 fd_set 某一位置 1
int FD_ISSET(int fd, fd_set *fdset); // 检测 fd_set 某一位是否为 1

使用select监控多个文件描述符的Demo如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAX_SOCKETS 5
#define PORT 8080

int main() {
    int sockets[MAX_SOCKETS];    // 保存5个socket文件描述符
    fd_set readfds;              // 用于存储需要监控的文件描述符集合
    struct sockaddr_in address;
    int max_sd, activity, new_socket;
    char buffer[1024];

    // 创建并绑定 5 个 socket
    for (int i = 0; i < MAX_SOCKETS; i++) {
        if ((sockets[i] = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
            perror("Socket creation failed");
            exit(EXIT_FAILURE);
        }

        address.sin_family = AF_INET;
        address.sin_addr.s_addr = INADDR_ANY;
        address.sin_port = htons(PORT + i);  // 每个socket不同端口

        if (bind(sockets[i], (struct sockaddr*)&address, sizeof(address)) < 0) {
            perror("Bind failed");
            close(sockets[i]);
            exit(EXIT_FAILURE);
        }

        if (listen(sockets[i], 3) < 0) {
            perror("Listen failed");
            close(sockets[i]);
            exit(EXIT_FAILURE);
        }
    }

    printf("Waiting for connections...\n");

    while (1) {
        // 初始化文件描述符集合
        FD_ZERO(&readfds);
        max_sd = 0;

        // 将每个 socket 文件描述符加入到集合中
        for (int i = 0; i < MAX_SOCKETS; i++) {
            FD_SET(sockets[i], &readfds);
            if (sockets[i] > max_sd) {
                max_sd = sockets[i];
            }
        }

        // 使用 select 监控文件描述符集合中的变化
        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);

        if ((activity < 0) && (errno != EINTR)) {
            perror("Select error");
        }

        // 检查每个 socket 是否有活动(数据可读或新连接)
        for (int i = 0; i < MAX_SOCKETS; i++) {
            if (FD_ISSET(sockets[i], &readfds)) {
                // 接受新的连接
                if ((new_socket = accept(sockets[i], NULL, NULL)) < 0) {
                    perror("Accept error");
                    exit(EXIT_FAILURE);
                }
                printf("New connection on socket %d\n", sockets[i]);

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

                close(new_socket);  // 关闭新连接
            }
        }
    }

    // 关闭 sockets
    for (int i = 0; i < MAX_SOCKETS; i++) {
        close(sockets[i]);
    }

    return 0;
}

三.Select性能分析

1)调用 select 时会陷入内核,这时需要将参数中的 fd_set 从用户空间拷贝到内核空间,select 执行完后,还需要将 fd_set 从内核空间拷贝回用户空间,高并发场景下这样的拷贝会消耗极大资源;(epoll 优化为不拷贝)

2)进程被唤醒后,不知道哪些连接已就绪即收到了数据,需要遍历传递进来的所有 fd_set 的每一位,不管它们是否就绪;(epoll 优化为异步事件通知)

3)select 只返回就绪文件的个数,具体哪个文件可读还需要遍历;(epoll 优化为只返回就绪的文件描述符,无需做无效的遍历)

2.同时能够监听的文件描述符数量太少。受限于 sizeof(fd_set) 的大小,在编译内核时就确定了且无法更改。一般是 32 位操作系统是 1024,64 位是 2048。(poll、epoll 优化为适应链表方式)

相关推荐
牛油果子哥q2 小时前
多模态大模型工程入门:图文Embedding、图文RAG、图片解析、C++多模态接口封装实战
开发语言·c++·embedding
庆登登登2 小时前
MySQL Workbench 鸿蒙 PC 适配全记录:从 GTK_X11 桌面程序到 ArkUI 原生数据库工作台
数据库·mysql·harmonyos
Bs_MoneyMagnet2 小时前
基于springboot+vue的爱心众筹系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·管理系统
2601_963282772 小时前
政企无线对讲系统落地:从设备采购到长期运维的完整工程实践
大数据·运维·数据库
熊文豪2 小时前
OceanBaseVS金仓:一条 SQL 的两条路——KingbaseES 的性能竞争力从哪来
数据库·sql·电科金仓
AIFQuant2 小时前
Python股票实时价格告警系统:WebSocket订阅与REST快照实战
开发语言·python·websocket·a股行情
大模型码小白2 小时前
数据可视化:AI 生成 HTML5 动态交互式数据图表
前端·数据库·人工智能·深度学习·机器学习·信息可视化·html5
风哥2号2 小时前
数据库教程FGMT26‑1‑GoldenGate数据库容灾迁移01(OGG同构异构、数据库迁移、数据同步、容灾复制)
数据库·oracle
wuyk5552 小时前
从零吃透 MQTT 通信|第 10 章 阿里云 / 腾讯云 MQTT 设备完整对接实战,三元组、签名、设备上云调试
c语言·开发语言·stm32·学习·阿里云·云计算·腾讯云
名字还没想好☜2 小时前
Java Collectors.groupingBy 进阶:多级分组、下游收集器与统计聚合一次搞定
java·windows·后端·python·spring