💻
(或称为多任务I/O服务器)是一种高效管理多个I/O操作的技术,允许单线程或单进程同时监控和处理多个I/O事件(如网络套接字、文件描述符等)
select
、poll
、epoll
等),由内核帮助应用程序高效地监视多个文件描述符(包括网络连接、管道、文件等)的状态变化,而不是让应用程序自己轮询每个连接的状态传统阻塞I/O模型中,每个I/O操作会阻塞线程直至完成。若需处理多个连接,通常需为每个连接分配独立线程/进程,导致资源消耗大、上下文切换频繁。 而I/O多路转接通过单线程监控多个I/O流,仅在I/O就绪时触发操作,避免了阻塞和资源浪费。
💻 系统提供
函数来实现多路复用 输入 / 输出 模型.
select
系统调用是用来让我们的程序监视多个文件描述符的状态变化的;select
这里等待, 直到被监视的文件描述符有一个或多个发生了状态改变;核心原理
select
是一种 同步I/O多路复用 机制,允许程序在一个线程中监听多个文件描述符(如套接字、文件等)的可读、可写或异常事件。💤 select
的函数原型如下:
#include <sys/select.h>
int select(
int nfds, // 监控的最大文件描述符值 +1
fd_set *readfds, // 监听可读事件的描述符集合
fd_set *writefds, // 监听可写事件的描述符集合
fd_set *exceptfds, // 监听异常事件的描述符集合
struct timeval *timeout // 超时时间(NULL为无限等待)
);
// 操作fd_set的宏:
FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 添加描述符到集合
FD_ISSET(int fd, fd_set *set); // 检查描述符是否在集合中
FD_CLR(int fd, fd_set *set); // 从集合移除描述符
📚 参数解释:
nfds
是需要监视的最大的文件描述符值 +1rdset
, wrset
, exset
分别对应于需要检测的可读文件描述符的集合 , 可写文件描述符的集合 及 异常文件描述符的集合timeout
为 结构体 timeval
, 用来设置 select()
的等待时间/* A time value that is accurate to the nearest
microsecond but also has a range of years. */
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
📚 参数 timeout 取值:
NULL
: 则表示 select()
没有 timeout
, select
将一直被阻塞, 直到某个文件描述符上发生了事件
0
: 仅检测描述符集合的状态, 然后立即返回, 并不等待外部事件的发生(非阻塞)
struct timeval timeout = {10, 0}
: 如果在指定的时间段里没有事件发生,select
将超时返回
typedef long int __fd_mask;
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FDELT(d) ((d) / __NFDBITS)
#define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE //__FD_SETSIZE等于1024
/* Access macros for `fd_set'. */
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp)
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp)
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp)
fd_set
的接口, 来比较方便的操作位图void FD_CLR(int fd, fd_set *set); // 用来清除描述词组 set 中相关 fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关 fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组 set 中相关 fd 的位
void FD_ZERO(fd_set *set); // 用来清除描述词组 set 的全部位
🙅 错误值可能为:
EBADF
: 文件描述词为无效的或该文件已关闭EINTR
:此调用被信号所中断EINVAL
: 参数 n 为负值ENOMEM
: 核心内存不足🦈 理解 select
模型的关键在于理解 fd_set
, 为说明方便, 取 fd_set
长度为 1 字节, fd_set
中的每一 bit 可以对应一个文件描述符 fd_set
。 则 1 字节长的 fd_set
最大可以对应 8 个 fd.
fd_set
; FD_ZERO(&set);
则 set
用位表示是 0000,0000FD_SET(fd,&set)
; 后 set
变为 0001,0000(第 5 位置为 1)set
变为 0001,0011select(6,&set,0,0,0)
阻塞等待select
返回, 此时 set
变为 0000,0011。 注意: 没有事件发生的 fd=5 被清空socket
内核中, 接收缓冲区中的字节数, 大于等于低水位标记 SO_RCVLOWAT
. 此时可以无阻塞的读该文件描述符, 并且返回值大于 0;socket
TCP 通信中, 对端关闭连接, 此时对该 socket
读, 则返回 0;socket
上有新的连接请求;socket
上有未处理的错误;socket
内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记 SO_SNDLOWAT
, 此时可以无阻塞的写, 并且返回值大于 0;socket
的写操作被关闭(close 或者 shutdown). 对一个写操作被关闭的 socket
进行写操作, 会触发 SIGPIPE 信号;socket
使用非阻塞 connect 连接成功或失败之后;socket
上有未读取的错误;sizeof(fd_set)
的值. 我这边服务器上 sizeof(fd_set)= 512, 每 bit 表示一个文件描述符, 则我服务器上支持的最大文件描述符是 512*8=4096.select
监控集的同时, 还要再使用一个数据结构 array
保存放到 select
监控集中的 fd, select
返回后, array
作为源数据和 fd_set
进行 FD_ISSET
判断**select
返回后会把以前加入的但并无事件发生的 fd 清空, 则每次开始 select
前都要重新从 array
取得 fd 逐一加入(FD_ZERO 最先)
, 扫描 array
的同时 取得 fd 最大值 maxfd
, 用于 select
的第一个参数备注: fd_set 的大小可以调整, 可能涉及到重新编译内核.
优点 | 缺点 |
---|---|
跨平台支持(所有UNIX/Linux系统) | 文件描述符数量受限(默认1024,由FD_SETSIZE定义) |
简单易用,适合少量并发场景 | 线性扫描,时间复杂度O(n)(效率随描述符数量下降) |
超时机制灵活 | 每次调用需重置fd_set(额外内存拷贝开销) |
FD_SETSIZE
宏定义(通常1024),需重新编译内核修改。select
的轮询效率远低于 epoll
或 kqueue
。select
是水平触发模式,若未处理就绪事件,会持续通知。read
/write
阻塞整个程序。示例一:检测标准输入输出
#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);
while(true)
{
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! Invalid fd\n");
continue;
}
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
}
return 0;
}
示例二:TCP 服务器使用 select
处理多客户端
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/select.h>
#include <cstring>
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 创建TCP socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket选项(允许地址重用)
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(8080);
// 绑定socket到端口
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);
}
fd_set readfds; // 描述符集合
int client_sockets[MAX_CLIENTS] = {0}; // 客户端socket数组
int max_sd;
while (true) {
FD_ZERO(&readfds); // 清空集合
FD_SET(server_fd, &readfds); // 添加服务器socket到监听集合
max_sd = server_fd;
// 添加所有客户端socket到集合
for (int i = 0; i < MAX_CLIENTS; i++) {
int sd = client_sockets[i];
if (sd > 0) {
FD_SET(sd, &readfds);
if (sd > max_sd) max_sd = sd;
}
}
// 调用select,阻塞等待事件
int activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
perror("select error");
}
// 检查服务器socket是否有新连接
if (FD_ISSET(server_fd, &readfds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 将新客户端socket加入数组
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == 0) {
client_sockets[i] = new_socket;
std::cout << "New client connected, socket fd: " << new_socket << std::endl;
break;
}
}
}
// 处理客户端数据
for (int i = 0; i < MAX_CLIENTS; i++) {
int sd = client_sockets[i];
if (FD_ISSET(sd, &readfds)) {
int valread = read(sd, buffer, BUFFER_SIZE);
if (valread == 0) { // 客户端断开连接
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
std::cout << "Client disconnected" << std::endl;
close(sd);
client_sockets[i] = 0; // 清除socket
} else { // 处理数据
buffer[valread] = '\0';
std::cout << "Received: " << buffer << std::endl;
send(sd, buffer, strlen(buffer), 0); // 回显数据
}
}
}
}
return 0;
}
SO_REUSEADDR
允许地址重用(避免端口占用)。select
监听流程 fd_set
管理需要监听的描述符集合。select
阻塞等待事件,返回就绪的描述符数量。FD_ISSET
),调用 accept
接受新连接。read
返回0,表示客户端断开连接,关闭socket并清理数组。【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,比如:多路转接之
epoll
、poll
模型,请持续关注我 !!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有