// 返回值表示有多少个 fd 就绪, 同一 fd 若是有多个就绪事件会被统计屡次
int select(int maxfdpl, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
复制代码
三个文件描述符集中
所包含的 最大文件描述符大1
. 其实是要检查的文件描述符数量, 数组下标从0开始. 有了这个值, 就不用检查全部的描述符.timeval结构数组
// 两个值都为0的话, 此时select()不回阻塞, 会一直轮询.
// 有一个不为0的话, 则会给 select() 设定一个等待时间的上限值
struct timeval{
time_t tv_sec; // 秒
suseconds tv_usec;// 微妙级别的精度
}
复制代码
// 每一个 unsigned long 型能够表示多少个bit, 是经过 bitmap 的记录, 一个bit能够记录一位数, 好比8位就能够标记8个fd
#define __NFDBITS (8 * sizeof(unsigned long))
// 默认的 FD_SETSIZE 为 1024, 要修改的话必须修改 glibc 中的头文件定义, 而后从新编译, 可是通常连接数量过多, 使用后面的 epoll 性能更佳
#define __FD_SETSIZE 1024
// 假设一共须要记录 __FD_SETSIZE(默认1024) 个fd, 须要多少个长整型数
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)
typedef struct {
// 使用 long 数组来表示
unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;
typedef __kernel_fd_set fd_set;
复制代码
错误发生
select() 就已经调用超时
计算屡次
.只支持水平触发markdown
数据须要从用户空间(程序)复制到内核空间数据结构
每次将3个数据集的数据发送到内核, 都须要进程先重置文件描述符集为须要监听的 fd并发
fd很是多的时候, 3个描述符集合都须要轮询, 很是消耗CPU异步
select 返回后, 程序并不知道是哪些 fd 准备就绪, 而只知道一共有多少个就绪了, 须要进程本身对传递过去的集合进行遍历和判断
socket
// 调用
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
// 包含文件描述符的结构
struct pollfd{
int fd; // 自身的描述符号 fd
short events; // 订阅的事件
short revents; // 响应的事件
}
复制代码
调用函数
包含 fd 的结构性能
每一个fd都有属于自身的 pollfd 结构, 它将 感兴趣事件和触发的事件分红了 events 和 revents
. events 的值告诉内核咱们关心的是描述符的哪些事件
; 当某个 fd 有事件触发了以后, 就由 内核修改 revents
的数据, 互不干扰, 因此没必要像 select 那样, 每次调用都必须重置 fd 集合.ui
不须要每次由进程重制 fd 集合spa
数组大小没有限制
只支持水平触发
跟 select() 同样须要将数据在用户空间和内核空间来回复制
可移植性较高但没有 select() 高
不适合 fd 数量多的时候, fd 多了性能不如 epoll
跟 select() 同样, poll返回后, 程序并不知道是哪些 fd 准备就绪, 而只知道一共有多少个就绪了, 须要进程本身对传递过去的集合进行遍历和判断
"一般程序调用这些系统调用(select() * poll() )所检查的文件描述符集合都是相同的, 可是内核并不会记录他们."
后面信号驱动I/O 以及 epoll 均可以使内核记录下进程中感兴趣的文件描述符, 经过这种机制消除了 select() 和 poll() 的性能扩展问题.这种方案是
根据发生的 I/O 事件
来延展,而与被检查的文件描述符数量无关
, 当须要检查大量的文件描述符的时, 信号驱动 I/O 和 epoll 能提供更好的性能表现.
后面的这两种信号驱动I/O 和 epoll 跟前面两种 select 和 poll 有所不一样, select 和 poll 没法让内核记住进程所感兴趣的 fd , 因此每次都要将感兴趣的 fd 从用户空间复制到内核空中, 浪费CPU很是消耗CPU时间, 而且每次调用返回都要检查全部的fd, 才能知道是哪些fd触发了事件, 这也是他们不适合大量 fd 操做的缘由.
信号驱动I/O, 进程请求内核: 当文件描述符上有课执行的I/O 操做时,向进程发送一个信号.
具体的步骤在后面
1.为内核发送的通知信号安装一个信号处理例程, 默认状况下, 这个通知信号时 SIGIO.
2.设定文件描述符的属主(owner) , 也就是当文件描述符上课执行I/O时会接收到通知信号的进程或进程组. 一般设置调用进程为属主. 可经过 fcntl()的 F_SETOWN 操做完成:
fcntl(fd, F_SETOWN, pid);
复制代码
3.设定 O_NONBLOCK 标志使其能变成非阻塞 I/O
4.经过打开 O_ASYNC 标志使其能变成信号驱动I/O, 这个和第3步能够合并成一个操做, 由于它们都须要用到 fcntl()的 F_SETFL
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
复制代码
5.调用进程执行完这些后就能够执行其它任务了, 接下来若是有相应的 fd 有事件触发, 内核会给进程发送一个信号进行通知, 经过设置的信号例程
6.信号驱动I/O提供的是边缘触发通知, 这表示一旦进程被通知I/O就绪, 就应该尽量多的执行I/O(尽量的多读取字节), 若是fd是设置的非阻塞, 表示须要在循环中I/O系统调用直到失败位置, 此时的错误码为 EAGAIN 或者 EWOULDBLOCK.
在 Linux2.4 或者更早的版本能应用于 套接字、终端、伪终端以及其它特定类型的设备上.Linux2.6 可用于管道和 FIFO.自Linux2.6.25以后, 也能在 inotify 文件描述符上使用.
在启动信号驱动I/O前安装信号处理例程 : 因为接收到 SIGIO 信号默认行为是终止进程, so 须要在驱动信号I/O前先为 SIGIO 信号安装处理例程, 若是先启动信号驱动/IO, 则在安装例程以前进程可能先被终止了.
在其它一些UNIX实现上, 信号 SIGIO 的默认行为是被忽略.
产生新的输入会生成一个信号, 即便以前的输入没有被读取.终端出现文件结尾的状况, 此时也会发送输入就绪的信号(伪终端不会).终端没有输出就绪,断开连接也不会有信号.在Linux 中,2.4.19版本后对伪终端的从设备端提供了“输出就绪”的信号,当伪终端主设备侧读取了输入后就会产生这个信号.
管道和FIFO的读端
,信号的产生状况:
对于管道或FIFO的写端
,信号会在下列状况中产生
信号驱动I/O适用于UNIX和Internet下的数据报套接字,信号产生状况:
信号驱动I/O适用于UNIX和Internet下的流式套接字,产生状况:
当 notify 文件描述符成为可读状态时会产生一个信号, 也就是由 inotify 文件描述符监视的其中一个文件上有事件发生时.
能够排队的实时信号数量是有限的, 当达到了数量限制以后, 通知会恢复为默认的SIGIO信号, 出现这种现象表示信号队列溢出了. 出现这种状况, 会失去有关fd上发生的I/O事件的信息, 由于 SIGIO 信号不会排队, SIGIO信号处理例程不接受 siginfo_t 结构体参数, so 信号处理例程不能肯定是哪个fd上产生了信号.
上面的问题的一种解决方式是增长事实信号数量的限制来减少信号队列溢出的可能性, 可是并不能彻底排除.
采用 F_SETSIG 来创建实时信号做为 “I/O就绪” 通知的程序必须为 SIGIO 安装处理例程.若是发送了SIGIO信号, 程序能够先经过 sigwaitinfo() 先将队列中的实时信号所有获取, 临时切换到 select() 或 poll(), 经过它们获取剩余的发生 I/O 事件的文件描述符列表.
优势:
当有事件触发的时候, 由内核经过发送信号的方式主动通知进程
不须要由用户进程复制fd数组到内核
缺点:
只支持边缘触发
信号处理很差可能会致使进程出问题.
epoll 同I/O多路复用和信号驱动同样, Linux的epoll(event poll) 能够检查多个文件描述符上的I/O就绪状态.
epoll 是 Linux 系统中独有的, 在2.6版本后新增, epoll API 的核心数据结构称做 epoll 实例,它和一个打开的文件描述符相关联, 这个文件描述符不是用来作I/O操做的, 它是内核数据结构的句柄, 这些内核数据结构实现了两个目的:
(ready list 是 interest list 的子集)
系统调用 epoll_create() 建立 epoll 实例, 返回表明该实例的文件描述符(对于每个打开的fd建立一个与之对应的 epoll 实例表示该fd)
// size 仅仅是指明内部数据结构的初始大小划分,2.6.8以后这个参数被忽略使用
int epoll_create(int size);
复制代码
在这个fd再也不使用, 经过close()关闭, 当全部的 epoll 实例相关的文件描述符都被关闭的时候, 实例被销毁, 相关的资源都返还给系统(多个fd可能引用到了相同的 epoll 实例, 这是因为调用了 fork() 或者 dup() 这样的函数)
系统调用 epoll_ctl() 操做同 epoll 实例相关联的兴趣列表, 经过 epoll_ctl() 能够增长新的描述符到列表中、将已有的文件描述符从该列表删除, 以及修改表明文件描述符上事件类型的位掩码.(新增、删除感兴趣列表、感兴趣事件)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
复制代码
参数fd代表要修改的fd, 能够是管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另外一个 epoll 实例的fd.
event 事件的结构体
struct epoll_event{
uint32_t events; // epoll events(bit mask), 位操做,多个事件进行 逻辑& 操做
epoll_data_t data; // User data
}
typedef union epoll_data{
void *ptr; //
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
复制代码
系统调用 epoll_wait() 返回与 epoll 实例相关联的就绪列表中的成员.( 获取在感兴趣列表中已经就绪的)
int epoll_wait(int epfd, struct epoll_event * evlist, int maxevents, int timeout);
复制代码
参数 evlist 所指向的结构体数组中返回的是有关 就绪态文件描述符的信息
events 字段返回了 在该描述符上已经发生的事件掩码
参数 timeout 用来肯定 epoll_wait()的阻塞行为
调用成功后,epoll_wait()返回数组 evlist 中的元素个数。若是在 timeout 超时间隔内没有任何文件描述符处于就绪态的话,返回 0。出错时返回−1,并在 errno 中设定错误码以表示错误缘由.
对于 epoll 检查的每个文件描述符, 能够指定位掩码来表示感兴趣的事件, 这些掩码和poll() 所使用的位掩码有紧密联系..
主要优势:
检查大量文件描述符的时, epoll 的性能比 select() 和 poll() 高不少.
epoll 既支持水平触发, 也支持边缘触发. 与之相反, select() 和 poll() 只支持水平触发, 信号驱动I/O 只支持边缘触发.
性能表现上, epoll 跟信号驱动I/O差很少, 可是epoll有一些优势赛过信号驱动I/O:
能够避免复杂的信号处理流程(好比信号队列溢出时的处理)
灵活性高, 能够指定咱们但愿检查的事件类型(例如检查 socket 的读就绪事件、写就绪事件、或者二者都检查)
当缓冲区中有数据可读/有空间可写的时候, 就会响应事件.
一个问题
使用水平触发的时候, 在socket缓冲区可写的时候, 会一直触发写事件, 因此一种处理方法就是, 在要写数据的时候才去注册写事件, 写完数据后取消写事件.
若是数据区一直有数据, 可是进程还没处理完数据, 会一直触发事件消耗性能, 这也是水平触发的一个缺点.
边缘触发的话, 是靠着新事件的产生才会触发的
.
你的事件已经反馈给进程了, 可是若是进程对此次事件的数据没读取完, 那剩余的数据就会在缓冲区里面, 不会再触发事件, 除非有新的数据到来(跟水平触发不一样,水平对只要有数据,就会触发事件)
, 这样你就能够读取到以前的数据了, 因此这也是边缘触发的一个问题, 当有事件发生的时候, 尽量的多读取数据, 防止数据停留在了缓冲区, 而进程读取不到完整的数据没法对请求进行处理. 客户端极可能就会请求超时.
若是开发团队的实力足够强的话,使用边缘触发进行开发.不然使用水平触发的性能已经足够.
在链接数多、并发高的状况下,使用边缘触发能更好的体现性能优点
select() 和 poll() 相对于 信号驱动和epoll() 在不一样os之间的可移植性更高, 可是当fd过多的时候, 效率也远低于后二者.
poll 和 select 只支持 水平触发
select() 和 poll() 的操做相似, 每次检查都是进程主动将数组 拷贝
到内核
select 传递的是3个事件(读、写、异常)数组,而且每次传递到内核前都须要清空原数据
poll 每次是将进程感兴趣的fd复制到内核中, 而且使用了 event 和 revent 将感兴趣事件和触发的事件分开了, 这样就不须要每次都初始化数组的数据.
select 和 poll 都是主动的进行检查, 由于这两种调用内核并不记录进程感兴趣的fd和事件.后面这两种信号驱动I/O 和 epoll, 都能使内核记住他们感兴趣的fd和事件, 因此不须要每次都进行数组的拷贝.
信号驱动只支持边缘触发
如上面所说, 信号驱动和epoll对于接收来讲是相似的, 都是内核通知进程, 而不是进程每隔一段时间去检查对应的文件描述符上是否有事件发生. 当文件描述符数量过多的时候, 性能优点很是明显, 由于不须要将 fd 复制到内核中, 而且 都是由内核主动通知进程
.
epoll支持水平触发和边缘触发
epoll 支持水平触发和边缘触发. epoll 的机制相似于信号驱动, 都是进程告诉内核对哪些 fd 感兴趣, 而后对应的fd上有事件的时候, 由内核主动通知进程
, 进程再进行相应的处理.
select、poll 和 信号驱动、epoll 的区别在于 主动 or 被动接受事件, select 和 poll 是主动去检查对应fd上是否有事件发生, 每次都须要复制数组到内核中, 而后由内核修改参数返回, 而 信号驱动和 epoll 在进程添加感兴趣fd和对应事件后, 每次通知都由内核主动的通知进程, 由于内核知道须要通知哪些, 不须要进程主动去询问和将数组从用户进程复制到内核, 因此信号驱动和epoll的性能显著的高于 select 和 poll.