I/O模式

阻塞 I/O

一般设备有两种操作:读和写。其中伴随读缓冲区和写缓冲区。

  1. 读缓冲区无数据:
    当读数据的速率超过写数据的速率,可能造成缓冲区无数据可读的情况,此时用户程序如果是阻塞 I/O 模式,那么用户程序将会进入等待,在linux中的具体操作就是从系统的 "Runable Queue” 中删除该进程,将其加入到等待队列“Wait Queue”。只有当设备通过“中断”告知了内核缓冲区已有数据或者内核轮询发现了缓冲区有数据,内核会唤醒该等待队列上的进程,把他们加入可 “Runable Queue”。
  2. 写缓冲区满数据:
    类似,当写数据的速率超过读数据的速率,可能造成缓冲区无空闲内存可写的情况,这时内核同样的会阻塞写线程直到缓冲区可写。

非阻塞 I/O

类似上面的情况,当无数据可读可写时,执行 read/write(一般 read/write 方法会返回此次调用读取或写了多少字节) 后,内核可直接返回 0,并不阻塞当前线程,把无数据可读可写的后果交由用户程序自己处理。

那什么时候可读和可写?这种 I/O 框架一般存在一种叫 Selector 选择器的线程,由这个线程去轮询所有的已注册的文件描述符(这包括普通文件,套接字等) 缓冲区,当出现缓冲区可读或可写时,找出在其上面注册了相应的可读事件或可写事件的回调方法,进行调用。但这种方式在处理大量描述符时效果不是很好,因为为缓冲区的可读可写的判断,需要进入内核态。

Epoll

在服务器进程中,一个 server 进程可能需要管理成百上千个 Socket。

他们的可读可写如果是阻塞模式,那将造成灾难性的后果,一个网络差的客户端连接,足以拖延整个系统。

如果可读可写是异步模式,大量的 Socket 管理将拖累 Selector,让 Selector 线程跑满 CPU

Linux 使用了 Epoll 模式,改善了上面的情况。

  • 通过使用一片用户态和内核态共享的内存空间,因为此片空间为共享的,所以用户程序向此内存写数据时不需要陷入内核态。
  • 用户程序需要写入什么数据?epoll 在此空间维护了一颗红黑树用来保存 Socket,一个就绪列表来引用就绪的 Socket。用户程序可以将自己的 Socket 添加至红黑树,并向 epoll 注册相关事件(可读、可写、连接...)的监视函数。
  • 当网卡接受到了数据或发送了数据后,向操作系统发出了一个硬中断通知操作系统,操作系统得知后,在 Epoll 红黑树中找到对应的 socket ,加入到就绪队列,。
  • Epoll 关注了就绪列表,通过就绪列表中的 Socket 向关心相关事件的进程发送通知,至此用户程序可以读取或写入数据了。

全程没有阻塞,所有操作都是异步的(异步改变世界),也没有用户态和内核态的非必要切换。

03-05 15:31