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 优化为适应链表方式)

相关推荐
云和数据.ChenGuang1 小时前
Django 应用安装脚本 – 如何将应用添加到 INSTALLED_APPS 设置中 原创
数据库·django·sqlite
woshilys1 小时前
sql server 查询对象的修改时间
运维·数据库·sqlserver
Hacker_LaoYi1 小时前
SQL注入的那些面试题总结
数据库·sql
2401_857439692 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
建投数据2 小时前
建投数据与腾讯云数据库TDSQL完成产品兼容性互认证
数据库·腾讯云
SoraLuna3 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Hacker_LaoYi3 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
岁月变迁呀3 小时前
Redis梳理
数据库·redis·缓存
独行soc3 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw