代码:https://gitee.com/nanyi-c/linux/tree/master/day50
一、I/O多路转接之select
1.初始select
系统提供select函数来实现多路复用输入/输出模型
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
2.select函数原型
select
是一个在 Unix 和类 Unix 操作系统中的系统调用,用于监控多个文件描述符,等待其中一个或多个文件描述符变得“就绪”。就绪可以意味着可读、可写或者发生异常
参数说明
参数 | 描述 |
---|---|
nfds | 这是你监控的文件描述符集(readfds、writefds、exceptfds)中最高文件描述符的编号加1。简单来说,它是监控的文件描述符范围的上限 |
readfds | 输入输出型参数,只关心读事件 |
writefds | 输入输出型参数,只关心写事件 |
exceptfds | 输入输出型参数,只关心异常事件 |
timeout | 输入输出型参数,设置为 NULL ,select 将无限期阻塞,直到至少有一个文件描述符就绪。如果 timeout 设置为非 NULL 值,它将在指定的秒数和微秒数后超时 |
以下是 timeval
结构体的定义:
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则发生函数返回,返回值为0
struct timeval {long tv_sec; /* seconds */long tv_usec; /* microseconds */
};
select
调用的返回值和含义如下:
- 返回值大于0:表示就绪的文件描述符数量。输入输出型参数
- 返回0:表示超时发生,没有文件描述符就绪。
- 返回-1:表示出错,并且设置
errno
来指示错误类型。
错误值可能为:
-
EBADF 文件描述词为无效的或该文件已关闭
-
EINTR 此调用被信号所中断
-
EINVAL 参数n为负值
-
ENOMEM 核心内存不足
常见的程序片段如下:
fs_set readset;
FD_SET (fd, &readset);
select(fd+1, &readset, NULL, NULL, NULL) ;
if(FD_ISSET (fd, readse) ([....}
1.select要正常工作,需要借助一个辅助数组,来保存所有合法fd
2.每次使用都要重置
3.就绪了,循环检测处理所有事件
关于fd_set结构
在Linux系统中,fd_set
是一个数据结构,用于表示一组文件描述符的集合。它通常与 select
系统调用一起使用,以便同时监控多个文件描述符的状态(是否可读、可写或有异常发生)。
fd_set
是一个固定大小的位掩码,其中每一位代表一个文件描述符。在内部,它通常是一个长整型数组,数组中的每个元素代表一定范围内的文件描述符。由于 fd_set
的大小是固定的,所以它有一个最大文件描述符的限制,这个限制在 Linux 系统中通常是 FD_SETSIZE
(通常定义为 1024)。
常见宏操作
FD_ZERO(fd_set *set)
:将fd_set
清零,即初始化fd_set
,使其不包含任何文件描述符。FD_SET(int fd, fd_set *set)
:将指定的文件描述符fd
添加到fd_set
集合中。FD_CLR(int fd, fd_set *set)
:从fd_set
集合中移除指定的文件描述符fd
。FD_ISSET(int fd, fd_set *set)
:检查指定的文件描述符fd
是否在fd_set
集合中。这个宏在select
调用后使用,以确定哪些文件描述符已经就绪。
3.理解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) 阻塞等待。
- 若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为 0000,0011
注意 :没有事件发生的 fd=5 被清空。
4.socket就绪条件
读就绪
- socket 内核中,接收缓冲区中的字节数,大于等于低水位标记 SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于 0。
- socket TCP 通信中,对端关闭连接,此时对该 socket 读,则返回 0。
- 监听 socket 上有新的连接请求。
- socket 上有未处理的错误。
写就绪
- socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于 0。
- socket 的写操作被关闭(close 或者 shutdown),如果此时进行写操作的话,会触发 SIGPIPE 信号。
- socket 使用非阻塞 connect 连接成功或失败之后,socket 上有未读取的错误。
异常就绪
- socket上收到带外数据,关于带外数据,和TCP紧急模式相关(回忆TCP协议头中,有一个紧急指针的字段)
二、SelectServer.hpp
1.基础框架
2.设计Loop
测试
每隔3s轮询一次
如果设置为nullptr,那么就永久阻塞式等待,直到有新链接
无论哪种方式我们建立链接时,服务器会疯狂输出,这是因为我们还没有对收到新链接后怎么做
如果事件就绪,但是不处理,select会一直通知我,直到我处理了
我们重新设置一下
此时便不会疯狂输出,还会显示剩余时间
时间就绪后就可以处理事件了,在rfds内
因为rfds是输入输出型参数,这里已经返回了哪些事已经就绪的
处理事件
首先我们要判断我们的文件描述符是不是就绪的,如果是,就可以建立连接了
测试
已经获得了一个新的sockfd
接下来我们可以读取吗?绝对不能读!读取的时候,条件不一定满足(建立连接–》不发请求,(底层没有数据)读的时候被阻塞,单进程绝对挂掉)
谁最清楚底层fd的数据是否就绪了呢??通过select!
想办法把新的fd添加给select,由select统一进行监管。
select 为什么等待的fd会越来越多??
只要将新的fd,添加到fd_array中即可
初始化数组,并且把0号位给listen套接字
重新设计Loop,并且找到最大的文件fd,使用for循环可以处理多个文件描述符,进行动态更新和确定最大文件描述符
添加一个debug函数便于我们调试以及查看信息
HandlerEvent
函数负责检测哪些文件描述符就绪,并根据它们是监听套接字还是普通套接字来调用相应的处理函数。
处理新链接,并把新链接的fd添加到fd_array中
处理普通fd就绪 进行IO
测试
三、select总结
小结:
- select要正常工作,需要借助一个辅助数组,来保存所有合法fd
- 每次使用都要重置
- 就绪了,循环检测处理所有事件
缺点
- 每次调用 select,都需手动设置 fd 集合,从接口使用角度来说也非常不便。
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。
- select 可监控的文件描述符数量太少。