Linux 网络 I/O
Table of Contents
1. 基本概念
在 32 位 Linux 系统下,虚拟存储器最大的寻址是 2^32,即 4G。操作系统会留 1G 给内核,另外 3G 给用户进程。所以通常存在内核态和用户态。
一次 IO 操作的流程,分两步:
- 网卡到内核 buffer 的读写 => 等待数据,等待数据过程可能比较耗时,因为不一定有数据可读,不一定允许可写,所以一般需要阻塞;
- 内核 buffer 到用户 buffer 的读写。所谓同步 IO 和异步 IO 都是针对这一步的;
基于这两步,Linux 有 5 种 IO 模型:
- 阻塞 IO:数据不返回,一直等待(过程啥也干不了),处理多个 fd,则需要多个线程或者进程;
- 非阻塞 IO:请求没有数据,立刻返回。所以非阻塞一般伴随着忙轮询,效率太低;
- IO 多路复用:一个线程同时处理多个 fd,有数据返回,无数据阻塞;
- 信号驱动 IO:所有事件都通过信号通知,性能低,一般不会出现网络编程中;
- 异步 IO,不阻塞进程,而且第二步的数据拷贝也是异步的,等拷贝完了再通知用户程序;
前 4 种 IO 模型都是同步 IO,只有最后一种是异步 IO。
2. epoll 或者 kqueue 的原理是什么?
- 阻塞 I/O 的缺点是:一个线程只能处理一个流的 I/O 事件,如果想要同时处理多个流,那么就需要多个线程(或者进程),效率太低。
- 非阻塞 I/O 往往伴随着忙轮询,这种情况下,一个线程可以处理多个流了,但是需要不断的对所有的流进行询问,在所有流都没有数据的时候,空耗费 CPU。
- I/O 多路复用: select/poll->epoll,它可以同时监听多个流,在空闲的时候回把当前线程阻塞掉,当有一个或者多个 I/O 事件时,唤醒。
本质上它也是轮询,但不过不是忙轮询,它轮询的时候表示一定有了数据。但问题在于,当有数据发生时,它并不知道哪些流有数据,所以:
- select 无差别轮询,效率为 O(n),它会把所有的流全部遍历一遍,所以随着流的变多,效率会越来越低
- epoll,event poll,即所谓事件驱动。它不同于忙轮询也不同于无差别轮询,它会把流发生的事件通知给我们,保证每一次对流的操作都是有意义的。 也就是说,它可以一次性定位到哪些流的变更,而且告诉我们变更具体是什么(事件)。
- 不管是 select 还是 epoll,它是如何监听多个流,知道流有没有数据呢?答案不是轮询,而是中断。网卡收到数据以后会给操作系统发出一个中断, 然后操作系统再通知给内核(epoll),再然后才有了上面的轮询。 — 这个说法是否准确有待考证
3. 一文看懂 IO 多路复用,Linux IO模式及 select、poll、epoll详解
什么是 IO 多路复用?单线程或单进程同时监测若干个文件描述符是否可以执行 IO 操作的能力。
epoll 的事件通知机制(epoll_wait)分为:LT 和 ET。
- LT:只要有数据可读,就会通知,应用程序可以不立即处理;
- ET:状态变化才会通知,一次没有读取完毕数据的情况下,再有数据写入是不会通知的;应用程序必须立刻处理。更加高效(并没有确切的考证)。
LT 是默认值,LT 不易遗漏事件,不易产生 bug,ET 容易导致遗漏数据。
4. 一些补充
I/O 多路复用方案:
- Linux:select, poll,epoll
- MacOS/FreeBSD:kqueue
- Windows/Solaris:IOCP