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)的方式。
高并发程序一般使用同步非阻塞方式而非多线程+同步阻塞方式,因为线程太多调度开销也会很大。