之前一直看不明白的IO模型,今天看了一会就明白了,知识真的是迭代学习的过程

BIO(Blocking IO)

IO的过程就是读取数据的过程,这里以客户端请求服务端的过程为例,客户端向服务端请求的过程本质上就是服务端读取客户端发送来的数据的过程,下文也用这个例子讲解,BIO就是阻塞式IO,见下图

img

BIO中一个线程只能同时处理一个IO,多个客户端发起请求时,只能创建多个线程处理,线程读取数据调用read指令,read是阻塞的,线程等待数据从内核态拷贝到用户态,拷贝完毕后线程才能继续处理。

NIO(NonBlocking IO,多路复用模型)

NIO与BIO的区别有两点,第一,一个线程可以同时处理多个IO,宏观上同时处理,微观上串行处理,NIO引入了Selector(选择器)、Channel、Buufer,模型如下图

img

客户端会把数据写到Buffer里面,然后Channel负责把数据从Buffer交给线程,Channel本身无法存储数据,其内部没有存储结构,Selector和Channel的结合就是多路复用的实现,这就是第二个区别,操作系统提供了三种操作select、poll、epoll,Selector对这三种操作进行了封装,提供上层调用接口,实现的效果就是,线程不需要轮询每个通道看数据是不是准备好了,而是数据准备好了通知线程,这三个系统调用的区别如下,具体见下文的多路服用模型,大白话说明区别就是select有限,poll无限,epoll具体到文件描述符和提高性能

  • select:是最早实现的 I/O 多路复用机制,几乎所有的操作系统都支持。它使用一个 fd_set 数据结构来存储需要监视的文件描述符集合,调用 select 函数时,进程会被阻塞,直到集合中的某个或多个文件描述符的 I/O 事件就绪,或者超时。select 函数返回后,需要遍历整个 fd_set 来找出哪些文件描述符的事件就绪。
  • poll:与 select 类似,也是用于监视多个文件描述符的 I/O 事件。它使用一个 pollfd 结构体数组来存储需要监视的文件描述符及其感兴趣的事件。poll 函数会阻塞进程,直到有文件描述符的事件就绪。与 select 不同的是,poll 没有最大文件描述符数量的限制。
  • epoll:是 Linux 特有的 I/O 多路复用机制,它使用事件驱动的方式来处理 I/O 事件。epoll 通过 epoll_create 创建一个 epoll 实例,使用 epoll_ctl 来添加、修改或删除需要监视的文件描述符及其事件,使用 epoll_wait 等待事件的发生。当有事件就绪时,epoll 会直接返回就绪的文件描述符列表,无需遍历所有文件描述符。

通道判断数据是不是准备好了也是通过read命令,但是这里的read是非阻塞的,如果没有准备好会直接返回结果,这个表现就体现了NIO的NonBlocking,不是说线程调用select等不会阻塞,线程调用select还是会被阻塞,直到有数据准备好了才会提醒,当数据准备好了读取的过程还是阻塞的,只有读完了再次调用select才能继续监控其他数据。

AIO(Aysc IO)

而AIO对NIO的优化是:读取数据不阻塞,把读取数据的工作交给操作系统,操作系统读完了通过回调函数告诉线程一声,因此这是真正实现了非阻塞式IO。

多路复用模型

I/O 多路复用(I/O Multiplexing)中的 “多路复用” 意味着在单个线程中同时管理多个 I/O 通道(如网络连接、文件描述符等),让一个线程可以处理多个 I/O 流的 I/O 事件,避免为每个 I/O 流创建一个单独的线程,从而提高系统资源的利用率和程序的并发处理能力。

  1. select
    select 是最古老的 I/O 多路复用机制之一,在 Unix 系统中使用较为广泛。
    select 使用一个位图来存储文件描述符,监视的文件描述符数量有限制,通常是 1024 个(可以通过修改文件描述符限制来提高数量,但仍受限制)。
    select 每次调用时都需要将待监视的文件描述符集合从用户态拷贝到内核态,这会导致性能开销随着文件描述符数量的增加而增加。
    select 对监视的文件描述符集合的扫描是线性的,随着文件描述符数量的增加,性能下降明显。
  2. poll
    poll 是对 select 的改进,使用了链表结构来存储文件描述符,因此没有文件描述符数量的限制。
    与 select 类似,poll 每次调用时需要将待监视的文件描述符集合从用户态拷贝到内核态,但性能上相对于 select 有所提升。
    poll 的主要缺点是,随着文件描述符数量的增加,性能会逐渐下降,因为 poll 仍然是线性扫描文件描述符集合。
  3. epoll
    epoll 是 Linux 下的一种高性能 I/O 多路复用机制,相比于 select 和 poll,epoll 的性能更好。
    epoll 使用了事件驱动的方式,不需要每次调用时将待监视的文件描述符集合从用户态拷贝到内核态,而是采用了基于回调的方式,当文件描述符就绪时,内核直接通知应用程序。
    epoll 支持三种工作模式:EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLET(边缘触发模式),可以更加灵活地满足不同的需求。
    epoll 对于大量文件描述符的管理具有更好的性能和扩展性,不会因为文件描述符数量的增加而导致性能下降。

触发模式:在 epoll 中,有两种触发模式可以选择:水平触发(Level-Triggered,简称 LT)和边缘触发(Edge-Triggered,简称 ET)。这两种触发模式在处理文件描述符就绪事件时有所不同:

1.水平触发(LT)(只要没准备好每次都告诉)

在水平触发模式下,当文件描述符就绪时,epoll_wait 函数会立即返回,并且会返回所有处于就绪状态的文件描述符。
如果应用程序没有处理完所有就绪事件,并且该文件描述符上的事件状态没有改变,下一次调用 epoll_wait 时,该文件描述符仍然会被返回。
水平触发模式是 epoll 的默认触发模式。
2.边缘触发(ET)(只提醒一次,这次你没处理好,下次我再也不提醒了)

在边缘触发模式下,当文件描述符就绪时,epoll_wait 函数只会返回一次,并且只返回该文件描述符上自上次 epoll_wait 调用后发生的就绪事件。
如果应用程序没有处理完所有就绪事件,并且该文件描述符上的事件状态没有改变,下一次调用 epoll_wait 时,该文件描述符不会再次被返回,除非有新的就绪事件发生。
边缘触发模式要求应用程序对文件描述符的就绪事件立即进行处理,否则可能会错过事件。