五种IO模型
1、阻塞等待:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。
2、非阻塞等待:如果内核没有将数据准备好,系统调用仍然会返回,并且会返回EWUOLDBLOCK或者EAGAIN错误码。
3、信号驱动:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
4、多路转接(多路转接):能够同时等待多个文件句柄的就绪状态。
5、异步IO:由内核在数据拷贝完成时,通知应用程序。(而信号驱动是告诉应用程序什么时候可以开始拷贝数据)。
前四种都属于同步IO,第五种属于异步IO。同步IO和异步IO,区别在于同步IO会参与IO结果的获取的过程。而异步IO则是不会参与IO结果获取的过程,直接拿到最终的IO结果。
IO的本质就是等和拷贝数据。也就是说没有数据的时候,就需要等待数据,当有数据的时候,再将数据拷贝走。而高效的IO,要达到高效,关键在于减少等的比重才能达到效果。在上述的五种IO模型中,多路转接的方式,一次等待多个文件描述符,在某种意义上等的效率更高,也就是说一次等通知多个就绪能够进行拷贝的文件描述符。
I/O多路转接之select
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的; 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
函数原型
参数解释:
nfds表示最大文件描述符+1。
readfds、writefds、exceptfds(位图结构)分别需要检测的可读文件描述符集合、可写文件描述符集合和异常文件描述符集合。
timeout表示设置select()的等待时间。(timeout取值(NULL、0、特定时间)
NULL表示阻塞等待,
0则表示仅检测描述符集合的状态,然后立即返回,
特定时间值表示select的等待时间)。
操作fd_set位图结构的接口
函数返回值
执行成功---->则返回文件描述词状态已改变的个数
如果返回0---->代表在描述词状态改变前已超过timeout时间,没有返回
当有错误发生时则---->返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的 值变成不可预测。
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缺点
每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
select支持的文件描述符数量太小
I/O多路转接之poll
函数原型
参数解释:
fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.
其中events和revents的取值可以为如下。比如POLLIN或者POLLOUT但是某一位为1的宏,可以设置进events或者内核设置进revents中。
nfds表示fds数组的长度
timeout表示poll函数的超时时间, 单位是毫秒(ms).
返回值
返回值 < 0, 表示出错;
返回值 = 0, 表示poll函数等待超时;
返回值 > 0, 表示poll由于监听的文件描述符就绪而返回
poll的优点
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比 select更方便
poll并没有最大数量限制 (但是数量过大后性能也是会下降,因为需要轮询的检测就绪的事件).
poll的缺点
在监听的文件句柄增多时
和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
I/O多路转接之epoll
函数原型
创建epoll句柄
epoll的函数与select和poll的函数不同,epoll的函数有三个分别独立完成各自功能。
参数size表示所创建的epoll模型,最大能监听文件句柄的数目。
返回值
success -----> return 正数。fail -----> return -1
将要监控的文件描述符进行注册
参数:
epfd表示我们所创建的epoll模型的文件句柄。
op表示关心添加的文件句柄的什么行为。用三个宏来表示。
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;
fd表示我们需要添加关心的文件句柄
event表示我们关心添加的文件句柄的什么事件。epoll_event结构体如下:
其中events可以是一下宏的集合
返回值:
success -----> return 0。fail -----> return -1
等待文件描述符就绪
参数:
epfd表示我们所创建的epoll模型的文件句柄。
events表示系统监听到文件句柄的事件就绪并拷贝至用户的结构体数组
maxevents表示events数组的大小。
timeout表示超时时间(毫秒,0会立即返回,-1是永久阻塞)。
返回值:
success ----> return 就绪的文件描述符的个数。
超时 -----> return 0
fail ----> return -1
多路转接的工作原理
首先,epoll的使用是一个单进程,因此我们可以通过进程找到对应的epoll句柄。
select和poll的原理:忽略其中的1、2和文件句柄,就单单看3。当外设将数据通过驱动刷新到对应文件句柄中,该文件句柄就就绪了。但是使用者需要轮询遍历3这个队列。
epoll的原理:现在我们需要将上图看作一个整体,epoll模型中存在一个红黑树存放关心的文件句柄,并带有回调函数。当外设将数据通过驱动刷新到对应文件句柄中,该文件句柄就就绪了。然后调用其回调函数,将3中就绪的文件句柄添加到2中的就绪队列中。也就是说,epoll不用再轮询遍历3这个队列了,直接遍历2这个就绪队列就能拿到所有就绪的文件句柄了。
其次,epoll中维护着红黑树、就绪队列等数据结构,在Linux中都是交由文件管理的。着就是为什么要创建epoll模型,也就是epoll文件句柄了。
epoll中的三个函数epoll_create、epoll_ctl、epoll_wait,epoll_create函数负责创建句柄,并初始化队列3的大小。epoll_ctl函数负责向红黑树中添加关心的文件句柄,并注册回调函数。epoll_wait函数则是遍历就绪队列2,拿到就绪的文件句柄。