IO模型
概念
同步/异步
同步:一个任务的完成需要依赖另外一个任务,只有被依赖的任务完成时才完成。同步是可靠的任务序列。
异步:不需要等待被异步的任务完成,只要自己完成调用就算完成。异步是不可靠的任务序列。
实现异步的三种方式:轮询状态、通知、回调
阻塞/非阻塞
阻塞:在调用结果返回前,不能处理其他任务。
非阻塞:在依赖任务完成前,可以处理其他任务。
组合
| 方式 | 场景 |
|---|---|
| 同步阻塞 | 调用者一直等待被依赖的任务完成。 |
| 同步非阻塞 | 调用者必须等待被依赖完成,但是期间可以做其他任务。 |
| 异步阻塞 | 调用者不必等待被依赖任务立刻完成,但是期间不能做其他事情。 |
| 异步非阻塞 | 调用者调用被依赖任务后,期间可以去做其他事情。 |
多路复用
内核一旦发现指定进程的一个或者多个IO条件准备读取,它就通知该进程。其最大优势是系统开销小,系统不必创建进程/线程。IO多路复用的实现方式主要有select、poll、epoll三种,本质上是同步的,读写过程是阻塞的。
实现
select
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select具体良好的跨平台性,但受限于最大文件描述符数量,64位linux平台默认为2048,可通过cat /proc/sys/fs/file-max文件查看。
select函数的具体案例为::select(maxFd + 1, &rfds, &wfds, &efds, tv)),如果tv参数为nullptr,则select函数会阻塞住,直到有某个文件描述符状态发生变化;如果设置为0则会立即返回;如果设置为其他时间则达到这个时间后超时返回。
在DRA实际使用中,如果有定时任务,tv参数设置为下一个定时任务的执行时间,否则设置为nullptr。
poll
poll和select没有本质区别,其基于链表来存储,因此没有最大连接数的限制。
缺点:
- 需要在
poll或者select函数返回后遍历文件描述符来获取就绪的socket,如果有大量的连接但极少处于就绪状态则效率会比较低下。 - 大量的fd数组被整体从内核空间拷贝到用户空间,大量拷贝可能没有意义。
epoll
epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的拷贝只需一次。epoll支持水平触发LT(level trigger)和边缘触发ET(edge trigger),最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
epoll使用过程中需要三个接口:
int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
异步AIO
Linux提供了AIO库函数实现异步,但是用的很少。其他开源的库有libevent、libev、libuv等。
总结
简单说,select和poll是轮询的方式,epoll是通知(callback)的方式。
高并发程序一般使用同步非阻塞方式而非多线程+同步阻塞方式,因为线程太多调度开销也会很大。