Linux 高级 IO:I/O 多路转接 select 接口和原理讲解
前言
本文基于 Linux 高级 IO 模型系列,重点讲解 I/O 多路转接中的 select 机制。主要涵盖 select 接口参数、返回值含义、fd_set 位图操作以及 timeout 设置,并通过逐步完善代码实现一个 select 版本的 TCP 服务器。
一、接口和原理讲解
1. select 参数与返回值
select 接口共有 5 个参数,后四个为输入输出型参数。
- nfds: 等待多个文件描述符中最大的那个值加 1。
- readfds: 读事件位图(fd_set*)。
- writefds: 写事件位图(fd_set*)。
- exceptfds: 异常事件位图(fd_set*)。
- timeout: 超时时间结构体(struct timeval*)。
返回值 n 的含义:
- n > 0: 表示有 n 个文件描述符就绪。
- n = 0: 表示等待超时,无错误,无 fd 就绪。
- n < 0: 表示等待出错。
2. fd_set 位图操作
fd_set 是内核提供的位图类型,用于传递用户关心的文件描述符集合。系统提供了四个宏进行操作:
FD_ZERO(&fdset): 初始化位图,全部置 0。
FD_SET(fd, &fdset): 设置指定位置为 1,表示关心该 fd。
FD_CLR(fd, &fdset): 移除指定位置,置为 0。
FD_ISSET(fd, &fdset): 判断指定位置是否为 1,即是否就绪。
输入输出含义:
- 输入: 用户告诉内核需要关心哪些 fd 的事件。
- 输出: 内核返回哪些 fd 的事件已经就绪。
3. timeout 参数
{5, 0}: 阻塞等待 5 秒,若期间无事件则返回,剩余时间更新。
{0, 0}: 非阻塞立即返回。
NULL: 阻塞等待直到有事件就绪。
二、select 版本的 TCP 服务器
1. 项目结构
- Main.cc: 主函数入口。
- SelectServer.hpp: 服务器封装类。
- Socket.hpp: 套接字封装。
- Log.hpp: 日志工具。
- makefile: 编译脚本。
2. 代码实现步骤
Main.cc
#include "SelectServer.hpp"
#include <memory>
int main() {
unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
SelectServer.hpp (基础版)
定义默认端口 8080,包含监听套接字和端口号成员。
#include <iostream>
#include <sys/select.h>
#include "Log.hpp"
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
class SelectServer {
public:
SelectServer(uint16_t port = defaultport) : _port(port) {}
bool Init() {
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Start() {}
~SelectServer() { _listensock.Close(); }
private:
Sock _listensock;
uint16_t _port;
};
完善一:启动循环与 select 调用
在 Start 函数中添加死循环,使用 select 等待 listensock 的读事件。
void Start() {
int listensock = _listensock.Fd();
for (;;) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock, &rfds);
struct timeval timeout = {2, 0};
int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
switch (n) {
case 0: cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl; break;
case -1: cout << "select error" << endl; break;
default: cout << "get a new link" << endl; break;
}
}
}
注意:每次调用 select 前需重置 rfds 和 timeout,因为它们是输入输出参数。
完善二:非阻塞模式
将 timeout 设置为 {0, 0},实现非阻塞 IO 效果。
完善三:阻塞模式
将 timeout 设置为 nullptr,实现阻塞等待。
完善四:管理客户端连接
引入 fd_array 数组存储客户端连接的 sock 文件描述符,限制最大数量为 sizeof(fd_set)*8 (通常 1024)。
static const int fd_num_max = sizeof(fd_set) * 8;
int defaultfd = -1;
for(int i=0; i<fd_num_max; i++) fd_array[i] = defaultfd;
当 accept 获取新连接时,查找数组中值为 -1 的位置存入;若已满则关闭连接并打印警告。
完善五:遍历数组设置 select 参数
在 Start 循环中遍历 fd_array,将所有有效 fd 通过 FD_SET 加入 rfds,并计算 maxfd 更新 nfds 参数。
int maxfd = fd_array[0];
for(int i=0; i<fd_num_max; i++) {
if(fd_array[i] != defaultfd) {
FD_SET(fd_array[i], &rfds);
if(maxfd < fd_array[i]) maxfd = fd_array[i];
}
}
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
完善六:处理读写事件
在 HandlerEvent 中遍历 rfds,区分 listensock 和 client_sock。
listensock 就绪:调用 Accept 获取连接。
client_sock 就绪:调用 read 读取数据。
- n > 0: 打印数据。
- n == 0: 客户端断开,close 并清理数组。
- n < 0: 出错,close 并清理数组。
完善七:代码重构
将逻辑拆分为 Accepter (连接管理器), Recver (数据读取器), Dispatcher (分发器),提高可读性。
三、select 的缺点
- 数量限制: fd_set 位图大小固定(通常 1024),不支持扩容。
- 性能开销: 每次调用 select 都需要在用户态和内核态之间拷贝 fd_set 数据。
- 遍历效率: 无论有多少 fd 就绪,内核都需要遍历所有传入的 fd 来检查状态;用户层也需要遍历数组来找出就绪的 fd。
- 重置频繁: 每次调用 select 前必须重新设置 fd_set 和 timeout。
随着并发连接数增加,select 的效率会显著下降,因此后续可学习 poll 和 epoll 模型。
四、源代码
Main.cc
#include "SelectServer.hpp"
#include <memory>
int main() {
unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
makefile
selectserver:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f selectserver
SelectServer.hpp
#include <iostream>
#include <sys/select.h>
#include <unistd.h>
#include "Log.hpp"
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
static const int fd_num_max = sizeof(fd_set) * 8;
int defaultfd = -1;
class SelectServer {
public:
SelectServer(uint16_t port = defaultport) : _port(port) {
for(int i = 0; i < fd_num_max; i++) fd_array[i] = defaultfd;
}
bool Init() {
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter() {
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if(sock < 0) ;
(Info, , clientip.(), clientport, sock);
pos = ;
(; pos < fd_num_max; pos++) {
(fd_array[pos] == defaultfd) ;
}
(pos == fd_num_max) {
(Warning, , sock);
(sock);
} {
fd_array[pos] = sock;
();
}
}
{
buffer[];
n = (fd, buffer, (buffer));
(n > ) {
buffer[n] = ;
cout << << buffer << endl;
} (n == ) {
(Info, , fd);
(fd);
fd_array[pos] = defaultfd;
} {
(Warning, , fd);
(fd);
fd_array[pos] = defaultfd;
}
}
{
( i = ; i < fd_num_max; i++) {
fd = fd_array[i];
(fd == defaultfd) ;
((fd, &rfds)) {
(fd == _listensock.()) {
();
} {
(fd, i);
}
}
}
}
{
listensock = _listensock.();
fd_array[] = listensock;
(;;) {
fd_set rfds;
(&rfds);
maxfd = fd_array[];
( i = ; i < fd_num_max; i++) {
(fd_array[i] != defaultfd) {
(fd_array[i], &rfds);
(maxfd < fd_array[i]) maxfd = fd_array[i];
(Info, , maxfd);
}
}
timeout = {, };
n = (maxfd + , &rfds, , , );
(n) {
: cout << << timeout.tv_sec << << timeout.tv_usec << endl; ;
: cout << << endl; ;
: cout << << endl; (rfds); ;
}
}
}
{
cout << ;
( i = ; i < fd_num_max; i++) {
(fd_array[i] != defaultfd) cout << fd_array[i] << ;
}
cout << endl;
}
~() { _listensock.(); }
:
Sock _listensock;
_port;
fd_array[fd_num_max];
};
Log.hpp
#pragma once
#include <iostream>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdarg>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log {
public:
Log() { printMethod = Screen; path = "./log/"; }
void Enable(int method) { printMethod = method; }
~() {}
{
(level) {
Info: ;
Debug: ;
Warning: ;
Error: ;
Fatal: ;
: ;
}
}
{
t = ();
* ctime = (&t);
leftbuffer[SIZE];
(leftbuffer, (leftbuffer), ,
(level).(), ctime->tm_year + , ctime->tm_mon + ,
ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
(s, format);
rightbuffer[SIZE];
(rightbuffer, (rightbuffer), format, s);
(s);
logtxt[ * SIZE];
(logtxt, (logtxt), , leftbuffer, rightbuffer);
(level, logtxt);
}
{
(printMethod) {
Screen: std::cout << logtxt << std::endl; ;
Onefile: (LogFile, logtxt); ;
Classfile: (level, logtxt); ;
: ;
}
}
{
std::string _logname = path + logname;
fd = (_logname.(), O_WRONLY | O_CREAT | O_APPEND, );
(fd < ) ;
(fd, logtxt.(), logtxt.());
(fd);
}
{
std::string filename = LogFile;
filename += ;
filename += (level);
(filename, logtxt);
}
:
printMethod;
std::string path;
};
Log lg;
Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
const int backlog = 10;
enum { SocketErr = 1, BindErr, ListenErr };
class Sock {
public:
Sock() {}
void Socket() {
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0) {
lg(Fatal, "socket error, %s : %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(uint16_t port) {
local;
(&local, , (local));
local.sin_family = AF_INET;
local.sin_port = (port);
local.sin_addr.s_addr = INADDR_ANY;
len = (local);
((sockfd_, ( sockaddr*)&local, len) < ) {
(Fatal, , (errno), errno);
(BindErr);
}
}
{
((sockfd_, backlog) < ) {
(Fatal, , (errno), errno);
(ListenErr);
}
}
{
peer;
len = (peer);
newfd = (sockfd_, ( sockaddr*)&peer, &len);
(newfd < ) {
(Warning, , (errno), errno);
;
}
ipstr[];
(AF_INET, &(peer.sin_addr), ipstr, (ipstr));
*clientip = ipstr;
*clientport = (peer.sin_port);
newfd;
}
{
peer;
(&peer, , (peer));
peer.sin_family = AF_INET;
peer.sin_port = (serverport);
(AF_INET, serverip.(), &(peer.sin_addr));
len = (peer);
n = (sockfd_, ( sockaddr*)&peer, len);
(n == ) {
std::cerr << << serverip << << serverport << << std::endl;
;
}
;
}
{
(sockfd_ > ) (sockfd_);
}
{ sockfd_; }
~() {}
:
sockfd_;
};
总结
本文详细讲解了 Linux 高级 IO 中 select 接口的原理与用法,并通过逐步完善代码实现了支持多客户端连接的 TCP 服务器。文章涵盖了 select 的参数设置、fd_set 位图操作、超时控制、连接管理与事件分发等核心逻辑,最后分析了 select 模型的局限性,为后续学习 poll 和 epoll 打下基础。