IO多路转接

1.select

初识select

系统提供 select 函数来实现多路复用输入 / 输出模型 .
select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的 ;
程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变 ;

select函数模型

select的函数原型如下: #include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

参数解释

参数 nfds 是需要监视的最大的文件描述符值 +1 ;
rdset,wrset,exset 分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
参数 timeout 为结构 timeval ,用来设置 select() 的等待时间

参数timeout取值

NULL :则表示 select ()没有 timeout , select 将一直被阻塞,直到某个文件描述符上发生了事件 ;
0 :仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
特定的时间值:如果在指定的时间段里没有事件发生, select 将超时返回。

关于fd_set结构


其实这个结构就是一个整数数组 , 更严格的说 , 是一个 " 位图 ". 使用位图中对应的位来表示要监视的文件描述符 .

提供了一组操作 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 的全部位

timeval结构

timeval 结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

函数返回值

执行成功则返回文件描述词状态已改变的个数
如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
当有错误发生时则返回 -1 ,错误原因存于 errno ,此时参数 readfds , writefds, exceptfds 和 timeout 的值变成不可预测。
错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数 n 为负值。
ENOMEM 核心内存不足

select执行流程

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

* ( 1 )执行 fd_set set; FD_ZERO(&set); 则 set 用位表示是 0000,0000 。 * ( 2 )若 fd = 5, 执行 FD_SET(fd,&set); 后set 变为 0001,0000( 第 5 位置为 1) * ( 3 )若再加入 fd = 2 , fd=1, 则 set 变为 0001,0011 * ( 4 )执行select(6,&set,0,0,0)阻塞等待 * ( 5 )若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为0000,0011。注意:没有事件发生的 fd=5 被清空

select就绪条件

读就绪

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

写就绪

socket 内核中 , 发送缓冲区中的可用字节数 ( 发送缓冲区的空闲位置大小 ), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写 , 并且返回值大于 0;
socket 的写操作被关闭 (close 或者 shutdown). 对一个写操作被关闭的 socket 进行写操作 , 会触发 SIGPIPE信号;
socket 使用非阻塞 connect 连接成功或失败之后 ;
socket 上有未读取的错误 ;

异常就绪

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

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 的第一个参数。

select的优点

1.可移植性好:select几乎在所有的平台上都支持,具有良好的跨平台兼容性。

2.超时精度高:select对于超时值的精度可以达到微秒级别,比poll的毫秒级别精度更高。

select的缺点

1.每次调用 select, 都需要手动设置 fd 集合 , 从接口使用角度来说也非常不便 .
2.每次调用 select ,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
3.同时每次调用 select 都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时也很大
4.select 支持的文件描述符数量太小
5.代码编写难度大

select的一般编码格式

1.需要有一个第三方数组,用来保存所以合法的fd

2.while(true)

{

1.遍历数组,更新出最大值

2.遍历数组,添加所有需要关心的fd到fd_set位图中

3.调用select进行事件检测

4.遍历数组,找到就绪的事件,根据就绪事件,完成对应的动作

}

2.poll

poll函数接口

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd 结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

参数说明

fds 是一个 poll 函数监听的结构列表 . 每一个元素中 , 包含了三部分内容 : 文件描述符 , 监听的事件集合 , 返回的事件集合.
nfds 表示 fds 数组的长度 .
timeout 表示 poll 函数的超时时间 , 单位是毫秒 (ms)

events和revents的取值

返回结果

返回值小于 0, 表示出错 ;
返回值等于 0, 表示 poll 函数等待超时 ;
返回值大于 0, 表示 poll 由于监听的文件描述符就绪而返回 .

poll的优点

1.效率高
2.输入输出参数分离,不需要进行大量的重置
3.poll参数级别,没有可以管理的fd上限

poll的缺点

1.poll依旧需要不少的遍历,在用户层检测事件就绪与内核检测fd就绪,都是一样的,用户还需要维护数组

2.poll需要内核到用户的拷贝

3.poll的代码也比较复杂

3.epoll

按照man手册的说法: 是为处理大批量句柄而作了改进的****poll.
epoll 有 3 个相关的系统调用 .
1.epoll_create

int epoll_create(int size);

2.epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件 , 而是在这里先注册要监听的事件类型 .
第一个参数是 epoll_create() 的返回值 (epoll 的句柄 ).
第二个参数表示动作,用三个宏来表示 .
第三个参数是需要监听的 fd.
第四个参数是告诉内核需要监听什么事

第二个参数的取值

EPOLL_CTL_ADD :注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD :修改已经注册的 fd 的监听事件;
EPOLL_CTL_DEL :从 epfd 中删除一个 fd

struct epoll_event结构如下:


events 可以是以下几个宏的集合:

EPOLLIN : 表示对应的文件描述符可以读 ( 包括对端 SOCKET 正常关闭 );
EPOLLOUT : 表示对应的文件描述符可以写 ;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 ( 这里应该表示有带外数据到来 );
EPOLLERR : 表示对应的文件描述符发生错误 ;
EPOLLHUP : 表示对应的文件描述符被挂断 ;
EPOLLET : 将 EPOLL 设为边缘触发 (Edge Triggered) 模式 , 这是相对于水平触发 (Level Triggered) 来说的 .
EPOLLONESHOT :只监听一次事件 , 当监听完这次事件之后 , 如果还需要继续监听这个 socket 的话 , 需要再次把这个socket 加入到 EPOLL 队列里 .

3.epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在 epoll 监控的事件中已经发送的事件 .

参数 events 是分配好的 epoll_event 结构体数组 .
epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存 ).
maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的 size.
参数 timeout 是超时时间 ( 毫秒, 0 会立即返回, -1 是永久阻塞 ).
如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时 , 返回小于 0 表示函数失败.

epoll的工作原理

首先,epoll区别于select和poll的点在于,就绪是通过下层主动来的。

epoll模型:

当某一进程调用 epoll_create 方法时, Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成员与epoll 的使用方式密切相关。
每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件.
这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来 ( 红黑树的插入时间效率是lgn ,其中 n 为树的高度 ).
而所有添加到 epoll 中的事件都会与设备 ( 网卡 ) 驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
这个回调方法在内核中叫 ep_poll_callback, 它会将发生的事件添加到 rdlist 双链表中 .
在 epoll 中,对于每一个事件,都会建立一个 epitem 结构体 .
struct eventpoll{
....
/* 红黑树的根节点,这颗树中存储着所有添加到 epoll 中的需要监控的事件 */
struct rb_root rbr;
/* 双链表中则存放着将要通过 epoll_wait 返回给用户的满足条件的事件 */
struct list_head rdlist;
....
};

struct epitem{
struct rb_node rbn;// 红黑树节点
struct list_head rdllink;// 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向其所属的 eventpoll 对象
struct epoll_event event; // 期待发生的事件类型
}
当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem元素即可.
如果 rdlist 不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户 . 这个操作的时间复杂度
是 O(1).

epoll的优点

1.接口使用方便 : 虽然拆分成了三个函数 , 但是反而使用起来更方便高效 . 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
2.数据拷贝轻量 : 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中 , 这个操作并不频繁( 而 select/poll 都是每次循环都要进行拷贝 )
3.事件回调机制 : 避免使用遍历 , 而是使用回调函数的方式 , 将就绪的文件描述符结构加入到就绪队列中 ,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪 . 这个操作时间复杂度 O(1). 即使文件描述符数目很多, 效率也不会受到影响 .
4.没有数量限制 : 文件描述符数目无上限

epoll工作方式

epoll有2种工作方式-水平触发(LT)和边缘触发(ET).

水平触发 Level Triggered 工作模式

epoll 默认状态下就是 LT 工作模式 .
当 epoll 检测到 socket 上事件就绪的时候 , 可以不立刻进行处理 . 或者只处理一部分 .
如上面的例子 , 由于只读了 1K 数据 , 缓冲区中还剩 1K 数据 , 在第二次调用 epoll_wait 时 , epoll_wait 仍然会立刻返回并通知socket 读事件就绪 .
直到缓冲区上所有的数据都被处理完 , epoll_wait 才不会立刻返回 .
支持阻塞读写和非阻塞读写
边缘触发 Edge Triggered 工作模式
如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志 , epoll 进入 ET 工作模式 .
当 epoll 检测到 socket 上事件就绪时 , 必须立刻处理 .
如上面的例子 , 虽然只读了 1K 的数据 , 缓冲区还剩 1K 的数据 , 在第二次调用 epoll_wait 的时候 ,
epoll_wait 不会再返回了 .
也就是说 , ET 模式下 , 文件描述符上的事件就绪后 , 只有一次处理机会 .
ET 的性能比 LT 性能更高 ( epoll_wait 返回的次数少了很多 ). Nginx 默认采用 ET 模式使用 epoll.
只支持非阻塞的读写

相关推荐
Ma_Hong_Kai9 天前
创建线程、socket通信、recv非阻塞
select·recv·sock·createthread·非阻塞recv使用
IF YOU~12 天前
vue element 切换 select 下拉框的 单选多选报错
vue.js·select
Peter_chq25 天前
【计算机网络】多路转接之epoll
linux·服务器·c语言·网络·c++·后端·epoll
Peter_chq1 个月前
【计算机网络】多路转接之select
linux·c语言·开发语言·网络·c++·后端·select
Peter_chq1 个月前
【计算机网络】多路转接之poll
linux·c语言·开发语言·网络·c++·后端·poll
橘色的喵1 个月前
C++编程:嵌入式Linux-ARM与外设中断交互的程序设计
linux·arm开发·select·interrupt·中断·低延迟·设备交互
螺蛳粉只吃炸蛋的走风2 个月前
网络编程IO多路复用之poll模式
网络·c++·面试·poll·阻塞与非阻塞
hope_wisdom2 个月前
C++网络编程之IO多路复用(一)
网络·c++·select·io多路复用
Winston Wood2 个月前
Android中的epoll机制
android·性能优化·i/o·epoll
小乌龟不会飞2 个月前
高并发服务的核心机制——IO多路转接--->epoll
linux·epoll·1024程序员节