Linux 多路转接select

ops/2025/2/1 11:31:53/

Linux 多路转接select

1. select

select() 是一种较老的多路转接IO接口,它有一定的缺陷导致它不是实现多路转接IO的最优选择,但 poll()epoll() 都是较新版的Linux系统提供的,一些小型嵌入式设备的存储很小,只能使用老版本的 Linux 系统(老内核的代码编译体积小),select()在这些设备上可能是唯一的选择

2 select的工作原理

调用 select() 会在内核中建立 readfdswritefdsexceptfds 三张位图(如果参数设置为空就不建立),分别对应监听读事件、写事件、异常事件。对位图进行添加、删除等操作都必须要使用系统提供的位图操作系统调用(见函数声明部分)。调用 select() 时,向位图输入的时要关心的文件描述符,当事件就绪时,位图的内容就变文件描述符是否有事件发生

select() 会返回就绪的文件描述符的数量,用户自行设置判断条件,对相应的文件描述符进行读、写、或异常处理操作。

Select1

3. 函数声明

#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

int nfds 表示文件描述符的范围,需要输入等待的最大值fd + 1。(硬要解释就是遍历的是[ 0, fd + 1 )的左闭右开区间,但是需要手动 +1 就匪夷所思)

fd_set *readfds readfds 表示只关心事件。

fd_set *writefds writefds 只关心事件。

fd_set *exceptfds exceptfds 只关心异常事件。

struct timeval *timeout 表示执行流调用 select() 后等待事件就绪的最大时间。如果等待就绪成功,其内部会存储等待剩余的时间;如果等待超时,执行流会跳出 select()。(详细见下文)

reval 返回值大于0,则返回值表示reval 个文件描述符就绪;返回值等于0表示等待超时;返回值小于0代表 select() 出错(如传入的文件描述符不存在)。

4. fd_set

fd_set 是文件描述符集,是一个数据类型,本质是一个位图结构,它的比特位的位置表示文件描述符编号,如第二个比特位表示二号文件描述符。 fd_set 能存储的上线取决于内核编译它时的取值,在云服务器上测试理论最多存储 4096 4096 4096 个文件描述符,这也是 select() 的缺点之一,它能存储的最大文件描述符是定长的,且对于现代服务器来说很小

输入时,fd_set 比特位的内容表示是否关心该 fd 事件,1 表示关心,0 表示不关心;输出时,比特位的内容表示事件是否发生

如图表示第 0、1、6、8 号文件描述符被存进去。

Select2

由于 fd_set 是位图结构,对位图做操作必须使用系统提供的专门的系统调用

void FD_CLR(int fd, fd_set *set)

将某个文件描述符对应的位清零,表示它不在监听范围内。

int  FD_ISSET(int fd, fd_set *set)

用于检查某个文件描述符是否在集合中被设置。

void FD_SET(int fd, fd_set *set)

将某个文件描述符对应的位设置为 1,表示它在监听范围内。

void FD_ZERO(fd_set *set)

用来清除(初始化)整个 fd_set

fd_set简化源码:

/* Macros for manipulating `fd_set`.  */
#define __FD_SETSIZE        1024    /* fd_set 能保存的最大文件描述符数 *//* 一组文件描述符,每个比特位表示一个文件描述符的状态。 */
typedef struct {unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(unsigned long))];
} fd_set;/* FD_ZERO: 清空 fd_set 中的所有位。 */
#define FD_ZERO(fdsetp) \(memset (fdsetp, 0, sizeof (fd_set)))/* FD_SET: 将文件描述符 `fd` 在 fd_set 中置位。 */
#define FD_SET(fd, fdsetp) \((fdsetp)->fds_bits[(fd) / (8 * sizeof (unsigned long))] |= \(1UL << ((fd) % (8 * sizeof (unsigned long)))))/* FD_CLR: 清除文件描述符 `fd` 在 fd_set 中的位。 */
#define FD_CLR(fd, fdsetp) \((fdsetp)->fds_bits[(fd) / (8 * sizeof (unsigned long))] &= \~(1UL << ((fd) % (8 * sizeof (unsigned long)))))/* FD_ISSET: 检查文件描述符 `fd` 在 fd_set 中是否被置位。 */
#define FD_ISSET(fd, fdsetp) \(((fdsetp)->fds_bits[(fd) / (8 * sizeof (unsigned long))] & \(1UL << ((fd) % (8 * sizeof (unsigned long))))) != 0)

5. struct timeval

timeval 是一个时间戳类结构体,用于存储执行流进入 select() 后等待事件就绪的最大时间。它的源码长这样:

#ifndef _STRUCT_TIMEVAL
#define _STRUCT_TIMEVAL/* 用于存储时间的结构体,包含秒和微秒。 */
struct timeval {long tv_sec;        /* 秒。 */long tv_usec;       /* 微秒。 */
};#endif /* _STRUCT_TIMEVAL */

tv_sec 表示秒, tv_usec 表示微秒,设置为 { n, 0 } ,表示阻塞等待,在 n 秒内反复轮询直到有事件就绪就返回, n 秒后,没有事件就绪也会返回;设置为 { 0, 0 } 表示非阻塞等待,轮询一次就返回;也可以设置为 NULL 表示一直阻塞等待

tv_sectv_usec 等待结束后并不会被销毁或重置,设置为 { n, 0 } 时,如果在 n 秒内有事件就绪,可以查看它的剩余时间。

6. select() 的使用

这里演示 select()服务器中的使用,用于管理 accept() 传来的文件描述符,因此其只关心读事件。如果不及时处理 select() ,也没有关系,因为事件就绪但是不处理,select() 就会一直通知,直到就绪被处理。注意select() 要正常工作,需要借助一个辅助数组,来保存所有合法的文件描述符。select() 每次使用都要重置。

//select_server.h
#pragma once#include <iostream>
#include <sys/select.h>
#include "Socket.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"using namespace socket_ns;class SelectServer
{const static int gnum = sizeof(fd_set) * 8;	//fd_set的理论最大容量const static int gdefaultfd = -1;	//初始化 fd_array 的值public:SelectServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>()){_listensock->BuildListenSocket(_port);}void InitServer(){for (int i = 0; i < gnum; i++){fd_array[i] = gdefaultfd;}fd_array[0] = _listensock->Sockfd(); // 默认直接添加listensock到数组中}// 处理新连接的void Accepter(){// 我们叫做连接事件就绪,等价于读事件就绪InetAddr addr;int sockfd = _listensock->Accepter(&addr);if (sockfd > 0){LOG(DEBUG, "get a new link, client info %s:%d\n", addr.Ip().c_str(), addr.Port());bool flag = false;for (int pos = 1; pos < gnum; pos++){if (fd_array[pos] == gdefaultfd){flag = true;fd_array[pos] = sockfd;LOG(INFO, "add %d to fd_array success!\n", sockfd);break;}}if (!flag){LOG(WARNING, "Server Is Full!\n");::close(sockfd);}}}// 处理普通的fd就绪的void HandlerIO(int i){char buffer[1024];ssize_t n = ::recv(fd_array[i], buffer, sizeof(buffer) - 1, 0); // 这里读取不会阻塞if (n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string content = "<html><body><h1>hello bite</h1></body></html>";std::string echo_str = "HTTP/1.0 200 OK\r\n";echo_str += "Content-Type: text/html\r\n";echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";echo_str += content;// echo_str += buffer;::send(fd_array[i], echo_str.c_str(), echo_str.size(), 0); // 临时方案}else if (n == 0){LOG(INFO, "client quit...\n");// 关闭fd::close(fd_array[i]);// select 不要在关心这个fd了fd_array[i] = gdefaultfd;}else{LOG(ERROR, "recv error\n");// 关闭fd::close(fd_array[i]);// select 不要在关心这个fd了fd_array[i] = gdefaultfd;}}// 一定会存在大量的fd就绪,可能是普通sockfd,也可能是listensockfdvoid HandlerEvent(fd_set &rfds){// 事件派发for (int i = 0; i < gnum; i++){if (fd_array[i] == gdefaultfd)continue;// fd一定是合法的fd// 合法的fd不一定就绪, 判断fd是否就绪if (FD_ISSET(fd_array[i], &rfds)){// 读事件就绪// 1. listensockfd 2. normal sockfd就绪if (_listensock->Sockfd() == fd_array[i]){Accepter();}else{HandlerIO(i);}}}}void Loop(){while (true){// 1. 文件描述符进行初始化fd_set rfds;FD_ZERO(&rfds);int max_fd = gdefaultfd;// 2. 合法的fd 添加到rfds集合中for (int i = 0; i < gnum; i++){if (fd_array[i] == gdefaultfd)continue;FD_SET(fd_array[i], &rfds);// 2.1 更新出最大的文件fd的值if (max_fd < fd_array[i]){max_fd = fd_array[i];}}struct timeval timeout = {30, 0};// _listensock->Accepter();// 不能,listensock && accept 我们把他也看做IO类的函数。只关心新链接到来,等价于读事件就绪!int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, nullptr ,&timeout); // 只关心读事件switch (n){case 0:LOG(DEBUG, "time out, %d.%d\n", timeout.tv_sec, timeout.tv_usec);break;case -1:LOG(ERROR, "select error\n");break;default:LOG(INFO, "haved event ready, n : %d\n", n); // 如果事件就绪,但是不处理,select会一直通知HandlerEvent(rfds);PrintDebug();break;}}}void PrintDebug(){std::cout << "fd list: ";for (int i = 0; i < gnum; i++){if (fd_array[i] == gdefaultfd)continue;std::cout << fd_array[i] << " ";}std::cout << "\n";}private:uint16_t _port;std::unique_ptr<Socket> _listensock;// 1. select要正常工作,需要借助一个辅助数组,来保存所有合法fdint fd_array[gnum];
};

fd_array[gnum] 是一个用来存储所有合法 fd 的辅助数组,它里面所有的元素最初被初始化为 -10 个元素默认存储 listensockfd,其他位置存储 accept() 传递过来的合法 fd 。当连接关闭时,再 close(fd_array[i]) 关闭对应位置的 fd ,并将 fd_array[i] = -1 重置为初始化状态。

这里 select() 用来多路转接 accept() 传递过来的文件描述符, listensockaccept() 也可以看作 IO 类的函数,但这两个函数只关心新连接到来,也就是只关心读事件就绪,所以 select() 中只有 rfds ,其他被置为了 NULL

7. select() 缺点

  1. 每次调用 select(), 都需要手动设置 fd 集合,接口使用不方便。

  2. 每次调用select(), 都需要把 fd 集合从用户态拷贝到内核态, 这个开销在 fd 很多时会很大。

  3. 同时每次调用 select() 都需要在内核遍历传递进来的所有 fd, 这个开销在 fd 很多时也很大。

  4. select()支持的文件描述符数量是定长的,而且太小。


http://www.ppmy.cn/ops/154728.html

相关文章

【Flask】在Flask应用中使用Flask-Limiter进行简单CC攻击防御

前提条件 已经有一个Flask应用。已经安装了Flask和redis服务。 步骤1&#xff1a;安装Redis和Flask-Limiter 首先&#xff0c;需要安装redis和Flask-Limiter库。推荐在生产环境中使用Redis存储限流信息。 pip install redis Flask-Limiter Flask-Limiter会通过redis存储限…

2025年01月31日Github流行趋势

项目名称&#xff1a;Qwen2.5项目地址url&#xff1a;https://github.com/QwenLM/Qwen2.5项目语言&#xff1a;Shell历史star数&#xff1a;13199今日star数&#xff1a;459项目维护者&#xff1a;jklj077, JustinLin610, bug-orz, huybery, JianxinMa项目简介&#xff1a;Qwen…

DeepSeek模型:开启人工智能的新篇章

DeepSeek模型&#xff1a;开启人工智能的新篇章 在当今快速发展的技术浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;已经成为了推动社会进步和创新的核心力量之一。而DeepSeek模型&#xff0c;作为AI领域的一颗璀璨明珠&#xff0c;正以其强大的功能和灵活的用法&…

前端八股CSS:盒模型、CSS权重、+与~选择器、z-index、水平垂直居中、左侧固定,右侧自适应、三栏均分布局

一、盒模型 题目&#xff1a;简述CSS的盒模型 答&#xff1a;盒模型有两种类型&#xff0c;可以通过box-sizing设置 1.标准盒模型&#xff08;content-box&#xff09;:默认值&#xff0c;宽度和高度只包含内容区域&#xff0c;不包含内边距、边框和外边距。 2.边框盒模型&a…

C++初阶 -- 初识STL和string类详细使用接口的教程(万字大章)

目录 一、STL 1.1 什么是STL 1.2 STL的版本 1.3 STL的六大组件 二、string类 2.1 string类的基本介绍 2.2 string类的默认成员函数 2.2.1 构造函数 2.2.2 析构函数 2.2.3 赋值运算符重载 2.3 string类对象的容量操作 2.3.1 size和length 2.3.2 capacity 2.3.3 r…

实验二 数据库的附加/分离、导入/导出与备份/还原

实验二 数据库的附加/分离、导入/导出与备份/还原 一、实验目的 1、理解备份的基本概念&#xff0c;掌握各种备份数据库的方法。 2、掌握如何从备份中还原数据库。 3、掌握数据库中各种数据的导入/导出。 4、掌握数据库的附加与分离&#xff0c;理解数据库的附加与分离的作用。…

MapReduce简单应用(一)——WordCount

目录 1. 执行过程1.1 分割1.2 Map1.3 Combine1.4 Reduce 2. 代码和结果2.1 pom.xml中依赖配置2.2 工具类util2.3 WordCount2.4 结果 参考 1. 执行过程 假设WordCount的两个输入文本text1.txt和text2.txt如下。 Hello World Bye WorldHello Hadoop Bye Hadoop1.1 分割 将每个文…

OceanBase 读写分离探讨

版本信息 OceanBase: 4.2.1.10 OBProxy: 4.3.3.0 租户类型&#xff1a; MySQL租户 弱一致性读 官方声明 默认情况下,所有的请求都是发送到数据的 Leader 副本上&#xff0c;即强一致性的请求&#xff0c;因为 OLAP 的分析计算&#xff0c;一般对于数据的一致性要求不高&…