select、poll、epoll
缓存 IO
数据传输过程中,会先被拷贝到内核的缓冲区中,然后再从缓冲区拷贝到应用程序的地址空间。这些拷贝操作的开销是很大的。
阻塞 / 非阻塞 vs 同步 / 异步
- 阻塞 / 非阻塞指的是 程序 请求 IO 操作后,如果资源没有准备好,程序该如何处理:阻塞是指程序等待;非阻塞是指程序继续执行(同时不断轮询 IO 资源是否准备好)。
- 同步 / 异步指的是 操作系统 在收到程序的 IO 请求后,如果资源没有准备好,操作系统该如何响应:同步会到资源准备好时才进行响应;异步会立即响应,当资源准备好时会用事件机制通知程序。
5 种 IO 模型
- 同步阻塞:应用进程被阻塞,直到数据复制到进程缓冲区时才返回。
- 同步非阻塞:应用进程执行系统调用之后,内核返回一个错误码,表示 IO 资源尚未准备好;然后应用程序继续执行其他代码,同时不断 轮询,查看 IO 是否就绪。
- 多路复用 IO:将系统调用拆分为多个组件,让单个进程能够处理多个 IO 事件。select、poll、epoll 都是多路复用 IO 的实现。多路复用的优势是能处理更多连接,而对单个连接的处理速度并没有加快。
- 信号驱动 IO:应用进程执行 sigaction 系统调用,内核立即返回,应用进程可以继续执行;内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到后在 信号处理程序 中调用 recvfrom 将数据从内核复制到应用进程中。
- 异步 IO:用户进程执行 aio_read 系统调用,内核直接返回,然后用户进程继续执行;等到数据就绪时,内核直接复制数据到进程,然后向进程发送通知。(异步 IO 中由 内核负责将数据拷贝到用户进程,同步 IO 是由进程自己来拷贝数据。)
select、poll、epoll 的区别
文件描述符(file descriptor):用于表述指向文件的引用,形式上是一个非负整数,实际上它是一个索引值,指向内核为每个进程所维护的打开文件的记录表,当程序打开一个文件或者创建一个文件时,内核向进程返回一个文件描述符。
select
函数监视的文件描述符有三类:writefds、readfds、exceptfds,调用 select
后会阻塞,直到有描述符就绪(socket 可读、可写或异常发生时)或者超时,函数才返回。阻塞期间,如果某个客户端发送了数据,socket 收到数据后需要唤醒 select,但 select 只知道发生了事件,不知道具体是哪个 socket 发生了事件,所以 需要遍历所有文件描述符(socket)。select 在几乎所有平台上都能用,但是 单个进程能监视的文件描述符的数量有限,在 Linux 上一般为 1024.
poll
用一个 pollfd
类型的结构来监视文件描述符和事件,其中包含文件描述符、要监视的事件、发生的事件三个字段。pollfd 监视的文件描述符没有上限。和 select 一样,有事件发生时,也需要遍历所有 socket 才能找到目标事件。
epoll
解决了 select/poll
的两个问题:
-
每次调用 select 都会把监控的 fds 复制到内核里。epoll 提供了
epoll_ctl
方法,用来维护 epoll 所监控的所有 socket,如果要新加一个 socket 或者删除一个 socket,调用该方法即可。它会将维护的这个 socket 集合映射到内核中,就不用每次都复制了。 -
socket 唤醒 select 时,不能告诉它具体是哪个 socket 发生了事件。引入了一个 ready_list 双向链表,会将发生事件的 socket 加入到这个链表中,那么唤醒 epoll 时,后者就会直到哪些 socket 发生了事件。遍历 ready_list 处理事件是,有两种模式:
- ET:边缘触发,遍历 ready_list 时,在读取 socket 中的事件之后会把 socket 从 ready_list 中移除。
- LT:水平触发,读取 socket 中的事件之后,如果这个 socket 返回了感兴趣的事件,就不会把它删除。
比如有一个客户端同时发来了 5 个数据包,按正常逻辑,这个 socket 只会往 ready_list 中加入一次,用户程序拿到这个 socket 之后,把其中的 5 个数据包都读完即可。如果读取第一个包时发生了异常,那么 —— 如果是 ET 模式,那么后面的 4 个数据包就读不了了,因为 socket 已经被移除了;而在 LT 模式下,因为 socket 发生了感兴趣的事件,所以这个 socket 不会被删除,后面的 4 个数据包还是可以正常访问。
Reactor 线程模型
传统阻塞 IO 模型
- 线程数量可能过大:请求和处理线程一一对应,每收到一个连接请求就需要用一个线程去处理。所以当并发量很大时,就会因为创建了大量线程而占用过多资源。
- 阻塞:且连接创建后,如果当前线程暂时没有数据可读,线程就会阻塞在 read 操作,浪费线程资源。
Reactor 模式
针对阻塞 IO 模型的两个缺点,Reactor 模式提出了对应的解决方案:
- 使用线程池,将连接完成后的业务处理任务提交到线程池即可,无需为每个连接创建线程。
- 采用 IO 多路复用,多个连接共用一个阻塞对象,当其中某个连接有新的数据时,线程就会被唤醒。
Reactor 模式也叫 Dispatcher 模式,收到连接请求后,通过 eventDispatcher 分派给事件处理器。
Reactor 负责 监听和分发事件。
Reactor 模式根据 Reactor 数量 和 处理线程数量 的不同,可以分成三种类型:
- 单 Reactor 单线程
- 单 Reactor 多线程
- 多 Reactor 多线程
1️⃣ 单 Reactor 单线程
Reactor 通过 select 监听客户端请求事件(select 可以通过一个阻塞对象监听多路连接请求),收到事件后通过 dispatch 进行分派:
- 如果是连接请求,则由 Acceptor 通过 accept 命令建立连接,然后创建一个 Handler 处理后续业务;
- 如果不是连接请求,则分派给对应的 Handler 来处理。Handler 会完成 read → 业务已处理 → send 这个完成流程。
适用于客户端数量有限的情况,如果客户端连接较多,会处理不过来。
2️⃣ 单 Reactor 多线程
Reactor 依旧通过 select 监听客户端请求事件,通过 dispatch 进行分派,但是在 Handler 中使用线程池去处理实际的业务。
- 如果收到连接请求,就分派给 Acceptor,后者通过 accept 命令建立连接,然后创建 Handler 处理后续请求;
- 如果不是连接请求,就分派给对应的 Handler 去处理。Handler 不再处理实际的业务,而只是负责响应,将实际的业务处理交给线程池。
可以充分利用 CPU 资源,单仍然使用单个 Reactor 线程,在高并发场景下容易出现性能瓶颈。
3️⃣ 多 Reactor 多线程
主 Reactor 负责创建新的连接,连接创建后,会把这个连接交给子 Reactor 来负责。 具体过程为:
- 主 Reactor 通过 select 监听请求事件,如果是连接请求,则分派给 Acceptor 去处理,建立连接后,主 Reactor 将连接分配给子 Reactor。
- 子 Reactor 将连接放入连接队列进行监听,并为每个连接创建 Handler 来处理业务。有新事件发生时,子 Reactor 调用对应 Handler 来处理。同样地,Handler 只是负责响应,具体的业务处理交给线程池。
主 Reactor 可以创建多个子 Reactor,一个 Reactor 处理多个连接(都放在连接队列中)。
标签:02,面试题,Java,Reactor,线程,IO,连接,select,socket From: https://www.cnblogs.com/lzh1995/p/16758032.html