目录
1.几种I/O模型的区别-故事举例
常见的IO模型包括:阻塞IO、非阻塞IO、IO多路复用、异步IO,他们的区别到底是什么呢?
举个你去饭堂吃饭的例子,你好比用户程序,饭堂好比操作系统。
- 阻塞 I/O就是,你去饭堂吃饭,但是饭堂的菜还没做好,然后你就一直在那里等(阻塞),等了好长一段时间终于等到饭堂阿姨把菜端了出来(数据准备的过程),但是你还得继续等(阻塞)阿姨把菜(内核空间)打到你的饭盒里(用户空间),经历完这两个过程,你才可以离开。
- 非阻塞 I/O就是,你去了饭堂,问阿姨菜做好了没有,阿姨告诉你没,你就离开了,加入做菜需要十分钟,这十分钟内你每隔一两分钟就来问一次阿姨好了没,这间隔的时间你可以去做别的事。十分钟后阿姨说做好了,于是阿姨帮你把菜(内核空间)打到你的饭盒里(用户空间),这个过程你是得等待的(阻塞)。
- I/O多路复用就是,你去饭堂吃饭,发现有一排窗口,饭堂阿姨告诉你这些窗口都还没做好菜,等做好了再通知你,于是你需要等(select调用中-阻塞),过了一会阿姨通知你菜做好了,但是不知道哪个窗口的菜做好了。于是你只能一个一个窗口去确认 (轮询),后面发现 5号窗口菜做好了,于是你让 5号窗口的阿姨帮你打菜到饭盒里,这个打菜的过程你是要等待的(阻塞),虽然时间不长。打完菜后,你自然就可以离开了。
- 异步 I/O 就是,你让饭堂阿姨将菜做好并把菜打到饭盒里后,把饭盒送到你面前,整个过程你都不需要任何等待(阻塞),也不涉及把菜(内核空间)打到你的饭盒里(用户空间)的过程。
2.IO多路复用中select、poll、epoll区别
首先select、poll、epoll都是同步IO,也就是说在读写事件就绪后他们都要自己负责读写(需要阻塞)。三者的区别如下:
select
时间复杂度O(n)
具体过程是将已连接的socket都放在文件描述符里(固定长度的 BitsMap),通过遍历的方式来检查是否有文件描述符有就绪的读写事件。
且其中还涉及到文件描述符在用户空间、内核空间之间的复制,而且两空间还需要分别遍历一遍。
在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
poll
时间复杂度O(n)
与select的原理相似,区别只是不再用 BitsMap 来存储所关注的文件描述符,用动态数组取而代之,以链表形式来组织,突破了 select 的文件描述符个数限制。
epoll
时间复杂度O(1)
epoll方式在内核中维护了一个链表——就绪事件列表,当用户调用epoll_wait时只会返回就绪事件对应的文件描述符,就能省下遍历的过程,检测的时间性能大大提升。
除此之外epoll还在内核中用红黑树存储了所有可能发生socket事件的文件描述符,因此还省掉了文件描述符在用户空间、内核空间之间的复制过程。
3.IO多路复用的水平触发模式(LT)、边缘触发模式(ET)
水平触发模式(LT)
水平触发是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;
边缘触发模式(ET)
边缘触发是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
触发模式注意事项
一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。
因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。
4.IO多路复用与多进程/线程模型的区别
多路复用Io(比如epoll调用)虽然会阻塞负责epoll_wait等待的进程,但他通过一个阻塞的进程就可以同时等待多个事件(多个客户端请求);
而多进程模型要等待多个客户端请求的话就需要创建多个进程(或线程)。每个进程等待一个客户端请求,于是多个进程之间的切换开销就大。