select是 Linux 系统提供的多路 IO 复用系统调用核心作用是让程序一次等待多个文件描述符fd而不是阻塞在单个read/write上当被监听的fd中有一个或多个发生状态变化可读 / 可写 / 异常时select就会返回通知程序处理就绪事件本质是把 IO 的 “等” 和 “拷贝” 两步拆开select负责 “等” 数据就绪数据就绪后再调用read/write做 “拷贝”1. select函数1.1 函数原型#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非阻塞0timeout时间内等待关于timeval结构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 不被监听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执行过程准备阶段用 FD_ZERO 清空 readfds/writefds/exceptfds 集合用 FD_SET 把需要监听的 fd 加入对应的集合同时记录最大的maxfd用数组保存所有活跃 fd避免每次重新扫描调用 select程序阻塞内核遍历从 0 到maxfd的所有 fd等待事件就绪或超时内核会修改fd_set清除无事件的 fd仅保留就绪的 fd返回处理select 返回后用FD_ISSET遍历所有活跃 fd找出就绪的 fd位图bit位置代表fd编号位图bit值(0/1)代表 “是否关心这个 fd 的事件”对就绪的 fd 调用 read/write 完成数据读写重置集合由于 fd_set 已被内核修改下次调用 select 前必须重新FD_ZERO FD_SET3. socket就绪条件这里重点研究读事件就绪3.1 读就绪POLLIN /readfds满足以下任意一种情况fd 读就绪接收缓冲区数据 ≥ 低水位标记 SO_RCVLOWAT可以无阻塞读read返回值 0对端关闭连接FIN 包到来此时read返回 0监听 socket 有新连接请求可以acceptsocket 上有未处理的错误3.2 写就绪POLLOUT /writefds满足以下任意一种情况fd 写就绪发送缓冲区空闲空间 ≥ 低水位标记 SO_SNDLOWAT可以无阻塞写write返回值 0。socket 写端被关闭再写会触发 SIGPIPE信号。非阻塞 connect 成功 / 失败连接建立完成。socket 上有未读取的错误4. select的特点特点fd 数量受限于fd_set大小常见系统中sizeof(fd_set)字节对应 4096 bit因此默认最多监听 4096 个 fd调整上限需修改内核并重新编译必须额外维护 fd 数组select返回后无事件的 fd 会被从集合中清空必须用数组保存所有活跃 fd每次调用前重新FD_SET同时更新maxfd兼容性强几乎所有 Unix-like 系统都支持select跨平台兼容性最好5. select缺点每次调用 select都需要手动设置 fd 集合从接口使用角度来说也非常不便每次调用 select都需要把 fd 集合从用户态拷贝到内核态这个开销在 fd 很多时会很大同时每次调用 select 都需要在内核遍历传递进来的所有 fd这个开销在 fd 很多时也很大select 支持的文件描述符数量太小6. select使用示例检测标准输入输出#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#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_uniqueTcpSocket()), _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_ptrSocket _listensock; bool _isrunning; int _fd_array[size]; };