select 函数详解


author: hjjdebug

date: 2026年 01月 25日 星期日 15:08:37 CST

descrip: select 函数详解.


文章目录

  • [0: I/O 多路复用是什么意思 ??](#0: I/O 多路复用是什么意思 ??)
  • [1. select 函数可以同时支持多少路I/O ?](#1. select 函数可以同时支持多少路I/O ?)
    • [1.1. server_fd 是一个整数](#1.1. server_fd 是一个整数)
    • [1.2 read_fds 是什么?](#1.2 read_fds 是什么?)
    • [1.3 read_fds 赋值.](#1.3 read_fds 赋值.)
  • [2. 使用select 的注意事项.](#2. 使用select 的注意事项.)
    • [2.1 timeout 值必需每次都要初始化.](#2.1 timeout 值必需每次都要初始化.)
    • [2.2 fd_set 值必需每次都要初始化.](#2.2 fd_set 值必需每次都要初始化.)

0: I/O 多路复用是什么意思 ??

如果一个程序需要用到多个I/O, 例如从键盘读数, 从3个socket 中读数,这就是4路I/O 读操作.

因为对I/O 的读写, read, write 是阻塞的, 当条件未满足时会被挂起, 为了保证各I/O 互相独立,

通常用多线程来应对各I/O 的读写. 对每一个I/O 都启动一个线程来进行读写.

那么能不能用一个线程来监测多个I/O 呢, 有数据了或可以写了再对不同的fd 操作,

这就是select 函数出场的时机. 就是一个线程看管多个I/O

于是我们明白

I/O多路复用, 就是多个I/O, 复用同一个线程的意思. 复用就是重复使用.

所以这种简化, 让人容易迷糊, 你会问 "谁复用?", "I/O 复用是什么意思?" "什么叫I/O复用"

你还不如直接说, "I/O多路复用" 就是 "多路I/O只用一个线程监测".

前者说得高大上不明不白, 就是词不达意, 后者简单直接一目了然.

1. select 函数可以同时支持多少路I/O ?

谁说了也不好使, 我从网上抄了一段代码, 我们调试一下看看吧.

测试代码:

cpp 复制代码
$ cat main.c
#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>
#include <fcntl.h>
#include <errno.h>

#define MAX_CLIENTS 100  // 定义最大客户端数量,远大于16
#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    int max_fd;
    int client_sockets[MAX_CLIENTS] = {0}; // 存储所有客户端套接字
    fd_set read_fds; // 用于存储当前需要监控的文件描述符集合

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

    // 设置套接字选项,允许重用地址
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        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");
        exit(EXIT_FAILURE);
    }

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

    // 初始化客户端套接字数组
    for (int i = 0; i < MAX_CLIENTS; i++) {
        client_sockets[i] = 0;
    }

    // 主循环
    while (1) {
        // 清空读描述符集合
        FD_ZERO(&read_fds);

        // 将服务器套接字添加到集合中
        FD_SET(server_fd, &read_fds);
        max_fd = server_fd;

        // 将所有活跃的客户端套接字添加到集合中
        for (int i = 0; i < MAX_CLIENTS; i++) {
            if (client_sockets[i] > 0) {
                FD_SET(client_sockets[i], &read_fds);
                if (client_sockets[i] > max_fd) {
                    max_fd = client_sockets[i];
                }
            }
        }

        // 调用 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)) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                continue;
            }
            printf("New connection, socket fd is %d\n", new_socket);

            // 找到一个空闲的客户端槽位
            int i;
            for (i = 0; i < MAX_CLIENTS; i++) {
                if (client_sockets[i] == 0) {
                    client_sockets[i] = new_socket;
                    printf("Adding to list of sockets as %d\n", i);
                    break;
                }
            }
            if (i == MAX_CLIENTS) {
                printf("Too many clients, rejecting new connection\n");
                close(new_socket);
            }
        }

        // 检查所有客户端套接字是否有数据可读
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sock = client_sockets[i];
            if (sock > 0 && FD_ISSET(sock, &read_fds)) {
                char buffer[BUFFER_SIZE] = {0};
                int valread = read(sock, buffer, BUFFER_SIZE - 1);
                if (valread <= 0) {
                    // 客户端断开连接
                    printf("Client disconnected, socket fd %d\n", sock);
                    close(sock);
                    client_sockets[i] = 0;
                } else {
                    // 收到数据,回显给客户端
                    printf("Received: %s from client %d\n", buffer, i);
                    write(sock, buffer, strlen(buffer));
                }
            }
        }
    }

    return 0;
}

调试:

1.1. server_fd 是一个整数

(gdb) p server_fd

$1 = 3

只所以server_fd 为3, 是因为0,1,2已经被系统占用了, 懂的都懂.

1.2 read_fds 是什么?

(gdb) p read_fds

$2 = {

__fds_bits = {0 <repeats 16 times>}

}

(gdb) ptype read_fds

type = struct {

__fd_mask __fds_bits[16];

}

(gdb) ptype __fd_mask

type = long

原来read_fds 是一个16个长整形数.

当时看到16, 就朦胧的联想到是不是只能检测16个I/O呢? 实际上不是这意思.

16位长整形,在64位机器上是1024个bit位, 它能够监测1024个I/O

下面解释为什么bit位就对应着fd.

1.3 read_fds 赋值.

FD_SET(server_fd, &read_fds);

(gdb) p read_fds

$3 = {

__fds_bits = {8, 0 <repeats 15 times>}

}

server_fd 为3, read_fds 在bit3位置置1,形成8.

FD_SET(client_sockets[i], &read_fds);

(gdb) p client_sockets[i]

$6 = 4

(gdb) p read_fds

$7 = {

__fds_bits = {24, 0 <repeats 15 times>}

}

24==0x18, 8是bit3的1, 0x10 是bit4的1, 对应client_socket[0]的描述符4

至此我们就明白了, 你监测的fd, 要转换位bitmap 上的对应的位置1.

至此, select 的工作原理就算说明白了.

所以说,64位机器上最多监测1024个描述符.

2. 使用select 的注意事项.

如果select 是在一个循环中调用, 注意.

2.1 timeout 值必需每次都要初始化.

否则你就搞不对.

测试程序没有使用timeout, 所以也就没有值的初始化问题了.

如果你使用了timeout, 则执行完select 后, 其值 timeout.tv_sec, timeout.tv_usec

就都变成0 了, 不初始化, 意味着第二次调用时其值为0,调用select时会立即返回超时.

这是我测试2秒超时, 程序一运行, 程序哗哗的打印超时发现的.

这里timeout 即是一个输入参数,也是一个输出参数,传递的是timeout地址.

说实话,这种设计不太好,容易让人犯错误,不过知道了也就无所谓了.

2.2 fd_set 值必需每次都要初始化.

否则你就读不到数据.

这里也是一个坑, 因为select 函数也是传递的fd_set的地址,它是输入参数,也是输出参数, 当select 返回时,保留的是准备就绪的描述符集.

而不再是你感兴趣的描述符集合了, 如果在循环中不初始化,你将得不到预期结果.

这就是参数即当输入,也当输出的风险, 我发现它.

是我用select 监测键盘输入, 我设置超时1秒,2秒,就不能从键盘获取输入,而设置超时

5,6,7,8秒就总能从键盘接受到数据, 为什么?

因为设置2秒钟超时, 我启动程序后,2秒内来不及从键盘输入字符. 例如输入hello还要敲回车,

结果超时,结果read_fds 此时是空的,结果再进入循环调用select, 由于read_fds 没有初始化,将不监测键盘, 所以收不到键盘输入.

而5,6,7,8秒, 我有足够时间反应,启动程序后又输入测试字符加回车键,select 执行收到字符

将返回准备好的read_fds, 这跟初始化的read_fds 数值是相等的, 都是bit1 置1,

所以下一轮执行select, read_fds 未进行初始化还是能接受到数据,

如果设置都正确, 别说2秒, 0.2秒,0.02秒超时都是可以的.

这次经历让我认识到了初始化的重要性.

这是函数参数设计的缺陷,即当输入也当输出,但好处是节省了参数个数.

这也无所谓好赖,必需要符合原设计者的约定. 这就是审题的必要性了. 所以对于一个新的函数,新的接口要了解其用途,这是学习成本.

所以若非必要,我们也不愿意学习新接口.

用select函数很久了,也只是用用而已,也没碰到什么问题,没有认真想过,

这次再用,自己手写时,碰到了一点问题,认真的思考了一下,

感觉算是搞懂了, 写篇博客吧, 也没多少内容, 就是select 函数本身,起名就叫select 函数详解吧.

连个函数原型都没有写也叫详解吗? 那些基本的东西大家搜一下就行了,我这就忽略了.

这里侧重的是实现原理和注意事项.

相关推荐
我在人间贩卖青春7 天前
多路复用select函数
select·多路复用
Trouvaille ~9 天前
【Linux】select 多路转接深度剖析:从位图原理到字典服务器实现
linux·运维·服务器·c++·select·多路转接·io模型
Trouvaille ~9 天前
【Linux】poll 多路转接:select 的改良版,以及它留下的遗憾
linux·运维·服务器·操作系统·select·poll·多路复用
xu_yule1 个月前
网络和Linux网络-13(高级IO+多路转接)五种IO模型+select编程
linux·网络·c++·select·i/o
曲幽1 个月前
告别重复劳动:SQL Server存储过程实战手册,从入门到高效协作
sql·select·cursor·declare·trigger·procedure
源代码•宸1 个月前
Golang原理剖析(channel面试与分析)
开发语言·经验分享·后端·面试·golang·select·channel
源代码•宸1 个月前
Golang原理剖析(channel源码分析)
开发语言·后端·golang·select·channel·hchan·sudog
源代码•宸2 个月前
Golang语法进阶(Sync、Select)
开发语言·经验分享·后端·算法·golang·select·pool
Ronin3052 个月前
【Linux网络】多路转接select
linux·网络·select·多路转接