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 函数详解吧.

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

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

相关推荐
曲幽6 天前
告别重复劳动:SQL Server存储过程实战手册,从入门到高效协作
sql·select·cursor·declare·trigger·procedure
源代码•宸8 天前
Golang原理剖析(channel面试与分析)
开发语言·经验分享·后端·面试·golang·select·channel
源代码•宸8 天前
Golang原理剖析(channel源码分析)
开发语言·后端·golang·select·channel·hchan·sudog
源代码•宸15 天前
Golang语法进阶(Sync、Select)
开发语言·经验分享·后端·算法·golang·select·pool
Ronin3051 个月前
【Linux网络】多路转接select
linux·网络·select·多路转接
AI2中文网2 个月前
AppInventor2 使用 SQLite(三)带条件过滤查询表数据
数据库·sql·sqlite·select·app inventor 2·appinventor·tableview
无聊的小坏坏3 个月前
Select 服务器实战教学:从 Socket 封装到多客户端并发
服务器·select·io多路复用
Wy_编程4 个月前
高并发服务器-多路IO转接-select
服务器·select·高并发
眰恦ゞLYF5 个月前
服务器类型与TCP并发服务器构建(SELECT)
服务器·select·io多路复用