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结构
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结构
函数返回值
执行成功则返回文件描述词状态已改变的个数如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回当有错误发生时则返回 -1 ,错误原因存于 errno ,此时参数 readfds , writefds, exceptfds 和 timeout 的值变成不可预测。错误值可能为:EBADF 文件描述词为无效的或该文件已关闭EINTR 此调用被信号所中断EINVAL 参数 n 为负值。ENOMEM 核心内存不足
select执行流程
* ( 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.
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结构如下:
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);
参数 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).
epoll 默认状态下就是 LT 工作模式 .当 epoll 检测到 socket 上事件就绪的时候 , 可以不立刻进行处理 . 或者只处理一部分 .如上面的例子 , 由于只读了 1K 数据 , 缓冲区中还剩 1K 数据 , 在第二次调用 epoll_wait 时 , epoll_wait 仍然会立刻返回并通知socket 读事件就绪 .直到缓冲区上所有的数据都被处理完 , epoll_wait 才不会立刻返回 .支持阻塞读写和非阻塞读写
如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志 , epoll 进入 ET 工作模式 .当 epoll 检测到 socket 上事件就绪时 , 必须立刻处理 .如上面的例子 , 虽然只读了 1K 的数据 , 缓冲区还剩 1K 的数据 , 在第二次调用 epoll_wait 的时候 ,epoll_wait 不会再返回了 .也就是说 , ET 模式下 , 文件描述符上的事件就绪后 , 只有一次处理机会 .ET 的性能比 LT 性能更高 ( epoll_wait 返回的次数少了很多 ). Nginx 默认采用 ET 模式使用 epoll.只支持非阻塞的读写