当前位置: 首页 > news >正文

IO 多路转接之 select

文章目录

  • IO 多路转接之 select
    • 1、初识 select
    • 2、select 函数及其参数解释
    • 3、select 函数返回值
    • 4、select 的执行过程
    • 5、socket 就绪条件
      • 5.1、读就绪
      • 5.2、写就绪
      • 5.3、异常就绪
    • 5、select 的特点
    • 6、select 的缺点
    • 7、select 使用实例
      • 7.1、只检测检测标准输入输出
      • 7.2、使用 select 的服务器响应程序

img

IO 多路转接之 select

1、初识 select

select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的。

程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。


2、select 函数及其参数解释

select 函数:

/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 参数 nfds 是需要监视的最大的文件描述符值+1

  • readfds,writefds,exceptfds 分别对应于需要检测的可读文件描述符的集合,可读写文件描述符的集合及异常文件描述符的集合

  • 参数 timeout 为结构 timeval,用来设置 select()的等待时间

struct timeval {long    tv_sec;         /* seconds */long    tv_usec;        /* microseconds */
};

timeout 参数:

  • NULL:则表示 select()没有 timeout,select 将一直被阻塞,直到某个文件描述符上发生了事件。

  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。

  • 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回(比如设置tv_sec=1,tv_usec=1000,那就是每隔1.1秒返回一次等到的文件描述符)。

fd_set 结构:

/* fd_set for select and pselect.  */
typedef struct{/* XPG4.2 requires this member name.  Otherwise avoid the namefrom 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;typedef long int __fd_mask;
typedef __fd_mask fd_mask;

其实这个结构就是一个整数数组,更严格的说,是一个 “位图”。使用位图中对应的位来表示要监视的文件描述符,提供了一组操作 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全部位

3、select 函数返回值

下面是执行man select后的查找到的 select 的返回值信息。

RETURN VALUEOn success, select() and pselect() return the number of file descriptors contained in the three returned descriptor sets (that is,  the  total  number  ofbits  that  are  set  in  readfds, writefds, exceptfds) which may be zero if the timeout expires before anything interesting happens.  On error, -1 is re‐turned, and errno is set to indicate the error; the file descriptor sets are unmodified, and timeout becomes undefined.ERRORSEBADF  An invalid file descriptor was given in one of the sets.  (Perhaps a file descriptor that was already closed, or one on  which  an  error  has  oc‐curred.)  However, see BUGS.EINTR  A signal was caught; see signal(7).EINVAL nfds is negative or exceeds the RLIMIT_NOFILE resource limit (see getrlimit(2)).EINVAL The value contained within timeout is invalid.ENOMEM Unable to allocate memory for internal tables.
  • 执行成功则返回文件描述词状态已改变的个数
  • 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回(没有文件描述符就绪)
  • 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfds,writefds,exceptfds 和 timeout 的值变成不可预测。

错误值可能为:

  • EBADF:文件描述词为无效的或该文件已关闭
  • EINTR:此调用被信号所中断
  • EINVAL:参数 nfds 为负值或者 timeout 无效
  • ENOMEM:核心内存不足

常用的使用场景:

fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){……}

4、select 的执行过程

理解 select 模型的关键在于理解 fd_set,为说明方便,取 fd_set 长度为 1 字节,fd_set 中的每一 bit 可以对应一个文件描述符 fd。则 1 字节长的 fd_set 最大可以对应 8 个 fd。

  • 执行 fd_set set; FD_ZERO(&set); 则 set 用位表示是 0000,0000。
  • 若 fd=5,执行 FD_SET(fd,&set);后 set 变为 0001,0000(第 5 位置为 1)
  • 若再加入 fd=2,fd=1,则 set 变为 0001,0011
  • 执行 select(6,&set,0,0,0)阻塞等待,这里 6 就是 最大文件描述符+1
  • 若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为 0000,0011。注意:没有事件发生的 fd=5 被清空,所以在执行 select 之前,需要保存所有需要监听的文件描述符,以便于被清空后,可以重新设置。

5、socket 就绪条件

5.1、读就绪

  • socket 内核中,接收缓冲区中的字节数,大于等于低水位标记 SO_RCVLOWAT (有足够的数据可以读的意思吧),此时可以无阻塞的读该文件描述符,并且返回值大于 0
  • socket TCP 通信中,对端关闭连接,此时对该 socket 读,则返回 0
  • 监听的 socket 上有新的连接请求
  • socket 上有未处理的错误

5.2、写就绪

  • socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于 0

  • socket 的写操作被关闭(close 或者 shutdown),对一个写操作被关闭的 socket进行写操作, 会触发 SIGPIPE 信号

  • socket 使用非阻塞 connect 连接成功或失败之后

  • socket 上有未读取的错误


5.3、异常就绪

socket 上收到带外数据。

关于带外数据,和 TCP 紧急模式相关(回忆 TCP 协议头中,有一个紧急指针的字段)。

这个数据需要紧急处理。


5、select 的特点

  • 可监控的文件描述符个数取决于 sizeof(fd_set)的值
  • 我这边服务器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则我服务器上支持的最大文件描述符个数是 512*8=4096
  • 将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd。
  • 一是用于再 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判断。
  • 二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO 最先),扫描 array 的同时取得 fd 最大值 maxfd,用于 select 的第一个参数。

备注:
fd_set 的大小可以调整,可能涉及到重新编译内核。感兴趣的同学可以自己去收集相关资料。

这里的特点感觉像是缺点哈哈哈,后面学习到的 epoll 可以完美解决这些问题。


6、select 的缺点

  • 每次调用 select,都需要手动设置 fd 集合,从接口使用角度来说也非常不便

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

  • 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大 select 支持的文件描述符数量太小

  • select 最多只能监听 sizeof(fd_set) 个文件描述符,这在有些场景下可能不够


7、select 使用实例

7.1、只检测检测标准输入输出

#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.2、使用 select 的服务器响应程序

主要的代码:

  • SelectServer.hpp文件
#pragma once#include <iostream>
#include <memory>
#include <sys/select.h>
#include <sys/time.h>
#include <string>
#include "Socket.hpp"using namespace socket_ns;// select需要一个第三方数组保存文件描述符
class SelectServer
{const static int N = sizeof(fd_set) * 8;const static int defaultfd = -1;public:SelectServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>()){InetAddr client("0", port);_listensock->BuildListenSocket(client);for (int i = 0; i < N; ++i)_fd_array[i] = defaultfd;_fd_array[0] = _listensock->SockFd(); // listen文件描述符一定是第一个}void AcceptClient(){InetAddr clientaddr;socket_sptr sockefd = _listensock->Accepter(&clientaddr);int fd = sockefd->SockFd();if (fd >= 0){LOG(DEBUG, "Get new Link ,sockefd is :%d ,client info : %s:%d", fd, clientaddr.Ip().c_str(), clientaddr.Port());}// 把新到的文件描述符交给select托管,使用辅助数组int pos = 1;for (; pos < N; pos++){if (_fd_array[pos] == defaultfd)break;}if (pos == N){::close(fd);// 满了LOG(WARNING, "server full ...");return;}else{_fd_array[pos] = fd;LOG(WARNING, "%d sockfd add to select array", fd);}LOG(DEBUG, "cur fdarr[] fd list : %s", RfdsToStr().c_str());}void ServiceIO(int pos){char buff[1024];ssize_t n = ::recv(_fd_array[pos], buff, sizeof(buff) - 1, 0);if (n > 0){buff[n] = 0;LOG(DEBUG, "client # %s", buff);std::string message = "Server Echo# ";message += buff;::send(_fd_array[pos], message.c_str(), message.size(), 0);}else if (n == 0){LOG(DEBUG, "%d socket closed!", _fd_array[pos]);::close(_fd_array[pos]);_fd_array[pos] = defaultfd;LOG(DEBUG, "cur fdarr[] fd list : %s", RfdsToStr().c_str());}else{LOG(DEBUG, "%d recv error!", _fd_array[pos]);::close(_fd_array[pos]);_fd_array[pos] = defaultfd;LOG(DEBUG, "cur fdarr[] fd list : %s", RfdsToStr().c_str());}}void HandlerRead(fd_set &rfds){for (int i = 0; i < N; i++){if (_fd_array[i] == defaultfd)continue;if (FD_ISSET(_fd_array[i], &rfds)){if (_fd_array[i] == _listensock->SockFd()){AcceptClient();}else{// socket读事件就绪ServiceIO(i);}}}}void Loop(){while (true){// int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);fd_set rfds;FD_ZERO(&rfds);int maxfd = defaultfd;for (int i = 0; i < N; ++i){if (_fd_array[i] == defaultfd)continue;FD_SET(_fd_array[i], &rfds);if (_fd_array[i] > maxfd)maxfd = _fd_array[i];}struct timeval t = {2, 0}; // 隔一段时间阻塞// struct timeval t = {0, 0}; // 不阻塞// int n = select(maxfd + 1, &rfds, nullptr, nullptr, &t);int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr); // 永久阻塞if (n > 0){// 处理读文件描述符HandlerRead(rfds);}else if (n == 0){//  时间到了LOG(DEBUG, "time out ...");}else{// 错误LOG(FATAL, "select error ...");}}}std::string RfdsToStr(){std::string rfdstr;for (int i = 0; i < N; ++i){if (_fd_array[i] != defaultfd){rfdstr += std::to_string(_fd_array[i]);rfdstr += " ";}}return rfdstr;}~SelectServer() {}private:uint16_t _port;std::unique_ptr<TcpSocket> _listensock;int _fd_array[N];
};

整体服务代码


OKOK,IO 多路转接之 select就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页


http://www.mrgr.cn/news/32878.html

相关文章:

  • 【浅水模型MATLAB】尝试复刻SCI论文中的溃坝流算例
  • 两个有序序列的中位数
  • C++ 11
  • Python语法(二)——函数
  • 大连孤独症培训学校谁家好:专注关爱,开启明天
  • 接口自动化测试框架搭建详解
  • pg入门14—pg中的domain是什么
  • Java笔试面试题AI答之设计模式(2)
  • appimage 软件创建桌面快捷图标
  • Python基于大数据的Boss直聘招聘可视化系统,附源码
  • 【图灵完备 Turing Complete】游戏经验攻略分享 Part.5 编程
  • 如何查看linux版本
  • 详解c++:new和delete
  • Android Studio开发发布教程
  • mfc140u.dll丢失不用愁?超详细mfc140u.dll丢失的解决方法
  • 通过标签实现有序:优化你的 FastAPI 生成的 TypeScript 客户端
  • 网页聊天——测试报告——Selenium自动化测试
  • 头歌数据库系统原理数据模型测试
  • 软件设计师——操作系统
  • 【星际探秘 乾坤之外】