目前Linux系统中提供了5种IO处理模型
1.阻塞IO
2.非阻塞IO
3.IO多路复用
4.信号驱动IO
5.异步IO
内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
阻塞IO(BIO)
当我们发起一次IO操作后一直等待成功或失败之后才返回,在这期间程序不能做其它的事情。阻塞IO操作只能对单个文件描述符进行操作。接受一个读写请求后,就无法接受其他请求,串行化的执行IO操作。也可以多个线程,一个读写请求就开一个线程,线程之间切换很浪费时间。
非阻塞IO(NIO)
服务端接受一个IO请求后,先放进fds集合(存放文件描述符的集合)里,轮询这个集合看看有没有就绪的(就是已经建立好网络连接的)IO的思想实现非阻塞,但也会浪费不必要的CPU资源。一次也只能对单个描述符进行操作。
IO多路复用
IO多路复用解决的本质问题是在用更少的资源完成更多的事
单线程或者单进程环境下IO多路复用都可以同时检测多个文件描述符是否可以执行IO操作,有select、poll、epoll三种机制。实现方式:首先都会对一组文件描述符进行相关事件的注册,然后阻塞等待某些事件的发生或等待超时。
常见软件的IO多路复用方案
redis: Linux下 epoll(level-triggered),没有epoll用select
nginx: Linux下 epoll(edge-triggered),没有epoll用select
select和poll都是状态持续通知(level-triggered)的机制,且不可改变,只要文件描述符中有IO操作可以进行,那么select和poll都会返回以通知程序。
select
select将监听的文件描述符分为三组,每一组监听不同的需要进行的IO操作。readfds是需要进行读操作的文件描述符,writefds是需要进行写操作的文件描述符,exceptfds是需要进行异常事件处理的文件描述符。这三个参数可以用NULL来表示对应的事件不需要监听。
当select返回时,每组文件描述符会被select过滤,只留下可以进行对应IO操作的文件描述符。
poll
和select用三组文件描述符不同的是,poll只有一个pollfd数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。
poll vs select
1.poll传参对用户更友好,不需要分开三组(select)传入参数。
2.poll会比select性能稍好些,因为select是每个bit位都检测,假设有个值为1000的文件描述符,select会从第一位开始检测一直到第1000个bit位。但poll检测的是一个数组。
3.支持select的系统更多,兼容更强大.
4.select提供精度更高(到microsecond)的超时时间,而poll只提供到毫秒的精度。
select和epoll都需要把文件描述符的集合从用户态拷贝到内核态,这个开销在fd很多时会很大。且对socket扫描时都是是线性扫描,采用轮询的方法,效率较低(高并发时)
epoll vs poll&select
1.**select&poll是O(N)的复杂度,epoll是O(1)**在需要同时监听的文件描述符数量增加时,都用epoll。
2.epoll内部用一个文件描述符挂载需要监听的文件描述符,这个epoll的文件描述符可以在多个线程/进程共享,所以epoll的使用场景要比select&poll要多。
10、epoll函数接口
#include <sys/epoll.h>
// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {
/红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件/
struct rb_root rbr;
/双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件/
struct list_head rdlist;
};
// API
int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程
11、epoll使用示例
int main(int argc, char argv[])
{
/*
- 在这里进行一些初始化的操作,
- 比如初始化数据和socket等。
*/
// 内核中创建ep对象
epfd=epoll_create(256);
// 需要监听的socket放到ep中
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
while(1) {
// 阻塞获取
nfds = epoll_wait(epfd,events,20,0);
for(i=0;i<nfds;++i) {
if(events[i].data.fd==listenfd) {
// 这里处理accept事件
connfd = accept(listenfd);
// 接收新连接写到内核对象中
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
} else if (events[i].events&EPOLLIN) {
// 这里处理read事件
read(sockfd, BUF, MAXLINE);
//读完后准备写
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
} else if(events[i].events&EPOLLOUT) {
// 这里处理write事件
write(sockfd, BUF, n);
//写完后准备读
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
}
}
return 0;
}
12、epoll缺点
epoll只能工作在linux下
13、epoll LT 与 ET模式的区别
epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。
LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作
ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,或者遇到EAGAIN错误
14、epoll应用
redis
nginx
信号驱动IO
信号驱动IO是利用信号机制,让内核告知应用程序文件描述符的相关事件。但信号驱动IO在网络编程的时候通常很少用到,因为在网络环境中,和socket相关的读写事件太多了。
异步IO
异步IO和信号驱动IO差不多,但它比信号驱动IO可以多做一步:相比信号驱动IO需要在程序中完成数据从用户态到内核态(或反方向)的拷贝,异步IO可以把拷贝这一步也帮我们完成之后才通知应用程序(异步,不会阻塞当前程序)。我们使用 aio_read 来读,aio_write 写。
同步IO vs 异步IO
1.同步IO指的是程序会一直阻塞到IO操作如read、write完成
2.异步IO指的是IO操作不会阻塞当前程序的继续执行