【Linux】多路转接之select

select是 Linux 系统提供的多路 IO 复用系统调用,核心作用是:

  • 让程序一次等待多个文件描述符(fd),而不是阻塞在单个read/write上
  • 当被监听的fd中,有一个或多个发生状态变化(可读 / 可写 / 异常)时,select就会返回,通知程序处理就绪事件
  • 本质是把 IO 的 "等" 和 "拷贝" 两步拆开:select负责 "等" 数据就绪,数据就绪后再调用read/write做 "拷贝"

1. select函数

1.1 函数原型

cpp 复制代码
#include <sys/select.h>
 
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
struct timeval *timeout);

参数解释:

参数 含义
nfds 监听的最大文件描述符值 + 1,内核需要遍历到这个值
readfds 监听可读事件的 fd 集合(用户→内核传参,内核会修改)
writefds 监听可写事件的 fd 集合
exceptfds 监听异常事件的 fd 集合
timeout 超时时间:NULL永久阻塞;0 非阻塞;>0 timeout时间内等待

关于timeval结构:

cpp 复制代码
struct timeval
{
    long tv_sec;  // 秒
    long tv_usec; // 微秒(1秒 = 1000000微秒)
};

返回值含义:

  • > 0:有事件就绪,返回值是就绪的 fd 总数
  • = 0:超时,无任何事件就绪
  • < 0:调用出错(如被信号中断、参数非法)

1.2 fd_set 位图操作宏

fd_set 是select用来批量管理文件描述符(fd)的核心数据结构,本质是一个固定大小的位图

  • 每一个bit对应一个文件描述符编号
  • bit 为 1:表示该 fd 被监听(用户关心它的事件)
  • bit 为 0:表示该 fd 不被监听
cpp 复制代码
FD_ZERO(fd_set *set);       // 清空集合,所有bit置0
FD_SET(int fd, fd_set *set);  // 将fd加入集合,对应bit置1
FD_CLR(int fd, fd_set *set);  // 将fd从集合移除,对应bit置0
FD_ISSET(int fd, fd_set *set); // 判断fd是否在集合中(是否就绪)

2. 理解select执行过程

  1. 准备阶段
    • 用 FD_ZERO 清空 readfds/writefds/exceptfds 集合
    • 用 FD_SET 把需要监听的 fd 加入对应的集合,同时记录最大的maxfd
    • 用数组保存所有活跃 fd,避免每次重新扫描
  2. 调用 select
    • 程序阻塞,内核遍历从 0 到maxfd的所有 fd,等待事件就绪或超时
    • 内核会修改fd_set,清除无事件的 fd,仅保留就绪的 fd
  3. 返回处理
    • select 返回后,用FD_ISSET遍历所有活跃 fd,找出就绪的 fd(位图bit位置:代表fd编号;位图bit值(0/1):代表 "是否关心这个 fd 的事件")
    • 对就绪的 fd 调用 read/write 完成数据读写
  4. 重置集合
    • 由于 fd_set 已被内核修改,下次调用 select 前必须重新FD_ZERO + FD_SET

3. socket就绪条件

这里重点研究读事件就绪

3.1 读就绪(POLLIN /readfds)

满足以下任意一种情况,fd 读就绪

  1. 接收缓冲区数据 ≥ 低水位标记 SO_RCVLOWAT:可以无阻塞读,read返回值 > 0
  2. 对端关闭连接(FIN 包到来):此时read返回 0
  3. 监听 socket 有新连接请求:可以accept
  4. socket 上有未处理的错误

3.2 写就绪(POLLOUT /writefds)

满足以下任意一种情况,fd 写就绪

  1. 发送缓冲区空闲空间 ≥ 低水位标记 SO_SNDLOWAT:可以无阻塞写,write返回值 > 0。
  2. socket 写端被关闭:再写会触发 SIGPIPE信号。
  3. 非阻塞 connect 成功 / 失败:连接建立完成。
  4. socket 上有未读取的错误

4. select的特点

特点

  1. fd 数量受限于 fd_set 大小
    • 常见系统中sizeof(fd_set)字节,对应 4096 bit,因此默认最多监听 4096 个 fd
    • 调整上限需修改内核并重新编译
  2. 必须额外维护 fd 数组
    • select返回后,无事件的 fd 会被从集合中清空
    • 必须用数组保存所有活跃 fd,每次调用前重新FD_SET,同时更新maxfd
  3. 兼容性强:几乎所有 Unix-like 系统都支持select,跨平台兼容性最好

5. select缺点

  • 每次调用 select,都需要手动设置 fd 集合,从接口使用角度来说也非常不便
  • 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
  • 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
  • select 支持的文件描述符数量太小

6. select使用示例

检测标准输入输出

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(0, &read_fds);
    for (;;)
    {
        printf("> ");
        fflush(stdout);
        int ret = select(1, &read_fds, NULL, NULL, NULL);
        if (ret < 0)
        {
            perror("select");
            continue;
        }
        if (FD_ISSET(0, &read_fds))
        {
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);
            printf("input: %s", buf);
        }
        else
        {
            printf("error! invaild fd\n");
            continue;
        }
        FD_ZERO(&read_fds);
        FD_SET(0, &read_fds);
    }
    return 0;
}

当只检测文件描述符0(标准输入)时,因为输入条件只有在你有输入信息的时候,才成立,所以如果一直不输入,就会产生超时信息。

7. 实现SelectServer代码

思路:

SelectServer代码实现

SelectServer.hpp

cpp 复制代码
#include <iostream>
#include <memory>
#include <sys/select.h>
#include "Socket.hpp"

using namespace SocketModule;
using namespace LogModule;

class SelectServer
{
public:
    const static int size = sizeof(fd_set) * 8;
    const static int defaultfd = -1;

    SelectServer(int port)
        : _listensock(std::make_unique<TcpSocket>()), _isrunning(false)
    {
        _listensock->BuildTcpSocketMethod(port);
        for (int i = 0; i < size; i++)
        {
            _fd_array[i] = defaultfd;
        }
        _fd_array[0] = _listensock->Fd();
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 把accept交给select
            //  因为: listensockfd,也是一个fd,进程怎么知道listenfd上面有新连接到来了呢?
            //  auto res = _listensock->Accept(); // 我们在select这里,可以进行accept吗?
            //  将listensockfd添加到select内部,让OS帮我关心listensockfd上面的读事件

            // 每次进入循环,select都要把所有fd都加入位图
            // 准备一个数组,存放之前所有

            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = defaultfd;

            for (int i = 0; i < size; i++)
            {
                // 跳过无效/空位
                if (_fd_array[i] == defaultfd)
                {
                    continue;
                }
                FD_SET(_fd_array[i], &rfds);

                if (maxfd < _fd_array[i])
                {
                    maxfd = _fd_array[i];
                }
            }

            PrintFd();

            // 放入select
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            if (n < 0)
            {
                LOG(LogLevel::ERROR) << "select error";
                continue;
            }
            else if (n == 0)
            {
                // 超时
                LOG(LogLevel::INFO) << "time out...";
            }
            else
            {
                // 有事件就绪了,有可能是新到来的fd有可能是就绪
                // 可以派发任务
                Dispatcher(rfds);
            }
        }
        _isrunning = false;
    }
    void Dispatcher(fd_set &rfds)
    {
        for (int i = 0; i < size; i++)
        {
            if (_fd_array[i] == defaultfd)
            {
                // 空的
                continue;
            }
            if (FD_ISSET(_fd_array[i], &rfds)) // 检查这个fd是否已经读就绪了
            {

                if (_fd_array[i] == _listensock->Fd())
                {
                    // 说明是新来的连接
                    Accepter();
                }
                else
                {
                    // 派发任务
                    Recver(_fd_array[i], i);
                }
            }
        }
    }

    void Accepter()
    {
        InetAddr client;
        int sockfd = _listensock->Accept(&client);
        if (sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "accept error";
        }
        else
        {
            // accept到新连接,把新fd交给select
            LOG(LogLevel::INFO) << "get a new link, sockfd: "
                                << sockfd << ", client is: " << client.StringAddr();
            int pos = 0;
            for (; pos < size; pos++)
            {
                if (_fd_array[pos] == defaultfd)
                    break;
            }
            if (pos == size)
            {
                LOG(LogLevel::WARNING) << "select server full";
                close(sockfd);
            }
            else
            {
                _fd_array[pos] = sockfd;
            }
        }
    }

    void Recver(int fd, int pos)
    {
        char buffer[1024];
        ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say@ " << buffer << std::endl;
        }
        else if (n == 0)
        {
            LOG(LogLevel::INFO) << "clien quit...";
            // 1. 把位图中fd不要就绪了
            _fd_array[pos] = defaultfd;
            // 2. 关闭fd
            close(fd);
        }
        else
        {
            LOG(LogLevel::ERROR) << "recv error";
            // 1. 把位图中fd不要就绪了
            _fd_array[pos] = defaultfd;
            // 2. 关闭fd
            close(fd);
        }
    }

    void PrintFd()
    {
        std::cout << "_fd_array[]:";
        for (int i = 0; i < size; i++)
        {
            if (_fd_array[i] == defaultfd)
                continue;
            std::cout << _fd_array[i] << " ";
        }
        std::cout << "\r\n";
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~SelectServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    int _fd_array[size];
};
相关推荐
溜达的大象1 小时前
从到处找资源到统一检索:飞牛NAS部署Pansou实战记录
网络·云计算
木古古182 小时前
搞一个高效的c/c++开发环境,工具VIm+自研vim插件+Shell脚本
linux·编辑器·vim
茫忙然3 小时前
U 盘搭建免驱 Linux 便携系统教程
linux·服务器
2401_868534783 小时前
园区网设计
网络
宋浮檀s4 小时前
春秋云镜——CVE-2020-25540
网络·安全·web安全
一起逃去看海吧4 小时前
dify-03
java·linux·开发语言
fengyehongWorld4 小时前
Linux 根据端口进行的相关查询
linux
天天进步20154 小时前
Tunnelto 源码解析 #4:Wormhole 控制通道:WebSocket 如何建立一条“隧道控制线”
网络·websocket·网络协议
lihao lihao4 小时前
linux匿名管道
linux·运维·服务器