From 52b34dc5a618999f500a2c2cb4b39fbbf4cb0e0e Mon Sep 17 00:00:00 2001 From: "595208882@qq.com" Date: Thu, 12 Aug 2021 21:41:57 +0800 Subject: [PATCH] adjust --- JAVA.md | 434 ------------------ OS.md | 433 +++++++++++++++++ images/{JAVA => OS}/IO多路复用.png | Bin images/{JAVA => OS}/Proactor模式.png | Bin images/{JAVA => OS}/epoll工作流程.jpg | Bin images/{JAVA => OS}/poll工作流程.jpg | Bin .../select、poll、epoll对比.png | Bin images/{JAVA => OS}/select工作流程.jpg | Bin images/{JAVA => OS}/信号驱动式IO.png | Bin .../单Reactor单进程线程.png | Bin .../单Reactor多线程多进程.png | Bin images/{JAVA => OS}/同步阻塞IO.png | Bin images/{JAVA => OS}/同步非阻塞IO.png | Bin .../多Reactor多进程线程.png | Bin images/{JAVA => OS}/异步IO.png | Bin images/{JAVA => OS}/异步非阻塞IO.png | Bin images/{JAVA => OS}/阻塞IO.png | Bin images/{JAVA => OS}/非阻塞IO.png | Bin 18 files changed, 433 insertions(+), 434 deletions(-) rename images/{JAVA => OS}/IO多路复用.png (100%) rename images/{JAVA => OS}/Proactor模式.png (100%) rename images/{JAVA => OS}/epoll工作流程.jpg (100%) rename images/{JAVA => OS}/poll工作流程.jpg (100%) rename images/{JAVA => OS}/select、poll、epoll对比.png (100%) rename images/{JAVA => OS}/select工作流程.jpg (100%) rename images/{JAVA => OS}/信号驱动式IO.png (100%) rename images/{JAVA => OS}/单Reactor单进程线程.png (100%) rename images/{JAVA => OS}/单Reactor多线程多进程.png (100%) rename images/{JAVA => OS}/同步阻塞IO.png (100%) rename images/{JAVA => OS}/同步非阻塞IO.png (100%) rename images/{JAVA => OS}/多Reactor多进程线程.png (100%) rename images/{JAVA => OS}/异步IO.png (100%) rename images/{JAVA => OS}/异步非阻塞IO.png (100%) rename images/{JAVA => OS}/阻塞IO.png (100%) rename images/{JAVA => OS}/非阻塞IO.png (100%) diff --git a/JAVA.md b/JAVA.md index 33c5185..a8bc06b 100644 --- a/JAVA.md +++ b/JAVA.md @@ -3841,440 +3841,6 @@ public void vectorTest(){ - -# I/O - -Linux/Unix常见IO模型:**阻塞(Blocking I/O)**、**非阻塞(Non-Blocking I/O)**、**IO多路复用(I/O Multiplexing)**、 **信号驱动 I/O(Signal Driven I/O)**(不常用)和**异步(Asynchronous I/O)**。网络IO操作主要涉及到**内核**和**进程**,其主要分为两个过程: - -- 内核等待数据可操作(可读或可写)——阻塞与非阻塞 -- 内核与进程之间数据的拷贝——同步与异步 - - - -## 基础概念 - -**① 阻塞(Blocking)和非阻塞(Non-blocking)** - -阻塞和非阻塞发生在内核等待数据可操作(可读或可写)时,指做事时是否需要等待应答。 - -- **阻塞:** 内核检查数据不可操作,则不立即返回 -- **非阻塞:** 内核检查数据不可操作,则立即返回 - -**② 同步(Synchronous)和异步(Asynchronous)** - -同步和异步发生在内核与进程交互时,进程触发IO操作后是否需要等待或轮询查看结果。 - -- **同步:** 触发IO操作 → 等待或轮询查看结果 -- **异步:** 触发IO操作 → 直接返回去做其它事,IO处理完后内核主动通知进程 - - - -### 阻塞I/O - -当用户程序执行 `read` ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,`read` 才会返回。阻塞等待的是 **内核数据准备好** 和 **数据从内核态拷贝到用户态** 两个过程。过程如下图: - -![阻塞IO](images/JAVA/阻塞IO.png) - - - -### 非阻塞I/O - -非阻塞的 `read` 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,`read` 调用才可以获取到结果。过程如下图: - -![非阻塞IO](images/JAVA/非阻塞IO.png) - -注意,**这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。** - - - -### 同步I/O - -无论 `read` 和 `send` 是 `阻塞I/O`,还是 `非阻塞I/O` 都是同步调用。因为在 `read` 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,即这个过程是同步的,如果内核实现的拷贝效率不高,`read` 调用就会在这个同步过程中等待比较长的时间。 - - - -### 异步I/O - -真正的异步 I/O 是`内核数据准备好` 和 `数据从内核态拷贝到用户态` 这两个过程都不用等待。 - -当我们发起 `aio_read` (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。过程如下图: - -![异步IO](images/JAVA/异步IO.png) - - - -## Reactor模式 - -`Reactor 模式` 即 I/O 多路复用监听事件,收到事件后根据事件类型分配(Dispatch)给某个进程/线程。其主要由 `Reactor` 和 `处理资源池` 两个核心部分组成: - -- **Reactor**:负责监听和分发事件。事件类型包含连接事件、读写事件 -- **处理资源池**:负责处理事件。如:read -> 业务逻辑 -> send - -Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于: - -- Reactor 的数量可以只有一个,也可以有多个 -- 处理资源池可以是单个进程/线程,也可以是多个进程/线程 - - - -将上面的两个因素排列组设一下,理论上就可以有 4 种方案选择: - -- **单 Reactor 单进程/线程** -- **单 Reactor 多进程/线程** -- **多 Reactor 单进程/线程**:相比 `单Reactor单进程/线程` 方案不仅复杂而且没有性能优势,因此可以忽略 -- **多 Reactor 多进程/线程** - - - -### 单Reactor单进程/单线程 - -一般来说,C 语言实现的是`单Reactor单进程`的方案,因为 C 语编写完的程序,运行后就是一个独立的进程,不需要在进程中再创建线程。而 Java 语言实现的是「单 Reactor 单线程」的方案,因为 Java 程序是跑在 Java 虚拟机这个进程上面的,虚拟机中有很多线程,我们写的 Java 程序只是其中的一个线程而已。以下是「`单 Reactor单进程`」的方案示意图: - - -![单Reactor单进程线程](images/JAVA/单Reactor单进程线程.png) - -可以看到进程里有 `Reactor`、`Acceptor`、`Handler` 这三个对象: - -- `Reactor` 对象的作用是监听和分发事件 -- `Acceptor` 对象的作用是获取连接 -- `Handler` 对象的作用是处理业务 - -对象里的 `select`、`accept`、`read`、`send` 是系统调用函数,`dispatch` 和 `业务处理` 是需要完成的操作,其中 `dispatch` 是分发事件操作。 - - - -**工作流程** - -- `Reactor` 对象通过 `select` (IO多路复用接口) 监听事件,收到事件后通过 `dispatch` 进行分发,具体分发给 `Acceptor` 对象还是 `Handler` 对象,还要看收到的事件类型 -- 如果是连接建立的事件,则交由 `Acceptor` 对象进行处理,`Acceptor` 对象会通过 `accept` 方法 获取连接,并创建一个 `Handler` 对象来处理后续的响应事件 -- 如果不是连接建立事件, 则交由当前连接对应的 `Handler` 对象来进行响应 -- `Handler` 对象通过 `read` -> 业务处理 -> `send` 的流程来完成完整的业务流程 - - - -**优缺点** - -- **优点** - - 因为全部工作都在同一个进程内完成,所以实现起来比较简单 - - 不需要考虑进程间通信,也不用担心多进程竞争 - -- **缺点** - - 因为只有一个进程,无法充分利用 多核 `CPU` 的性能 - - `Handler`对象在业务处理时,整个进程是无法处理其它连接事件,如果业务处理耗时比较长,那么就造成响应的延迟 - - - -**使用场景** - -单Reactor单进程的方案`不适用计算机密集型的场景`,`只适用于业务处理非常快速的场景`。如:Redis 是由 C 语言实现的,它采用的正是「单Reactor单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。 - - - -### 单Reactor多线程/多进程 - -如果要克服`单 Reactor 单线程/单进程`方案的缺点,那么就需要引入多线程/多进程,这样就产生了**单Reactor多线程/多进程**的方案。具体方案的示意图如下: - -![单Reactor多线程多进程](images/JAVA/单Reactor多线程多进程.png) - -**工作流程** - -- `Reactor` 对象通过 `select` (IO 多路复用接口) 监听事件,收到事件后通过 `dispatch` 进行分发,具体分发给 `Acceptor` 对象还是 `Handler` 对象,还要看收到的事件类型 -- 如果是连接建立的事件,则交由 `Acceptor` 对象进行处理,`Acceptor` 对象会通过 `accept` 方法获取连接,并创建一个 `Handler` 对象来处理后续的响应事件 -- 如果不是连接建立事件, 则交由当前连接对应的 `Handler` 对象来进行响应 -- `Handler` 对象不再负责业务处理,只负责数据的接收和发送,`Handler` 对象通过 `read` 读取到数据后,会将数据发给子线程里的 `Processor` 对象进行业务处理 -- 子线程里的 `Processor` 对象就进行业务处理,处理完后,将结果发给主线程中的 `Handler` 对象,接着由 `Handler` 通过 `send` 方法将响应结果发送给 `client` - - - -**单Reator多线程** - -- **优势**:能够充分利用多核 `CPU` 的能力 - -- **缺点**:带来了多线程竞争资源问题(如需加互斥锁解决) - -**单Reactor多进程** - -- **缺点** - - 需要考虑子进程和父进程的双向通信 - - 进程间通信远比线程间通信复杂 - - - -另外,`单Reactor` 的模式还有个问题,因为一个 `Reactor` 对象承担所有事件的 `监听` 和 `响应` ,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能瓶颈。 - - - -### 多Reactor多进程/多线程 - -要解决 `单Reactor` 的问题,就是将 `单Reactor` 实现成 `多Reactor`,这样就产生了 **多Reactor多进程/线程** 方案。其方案的示意图如下(以线程为例): - -![多Reactor多进程线程](images/JAVA/多Reactor多进程线程.png) - -**工作流程** - -- 主线程中的 `MainReactor` 对象通过 `select` 监控连接建立事件,收到事件后通过 `Acceptor` 对象中的 `accept` 获取连接,将新的连接分配给某个子线程 -- 子线程中的 `SubReactor` 对象将 `MainReactor` 对象分配的连接加入 `select` 继续进行监听,并创建一个 `Handler` 用于处理连接的响应事件 -- 如果有新的事件发生时,`SubReactor` 对象会调用当前连接对应的 `Handler` 对象来进行响应 -- `Handler` 对象通过 `read` -> 业务处理 -> `send` 的流程来完成完整的业务流程 - - - -**方案优势** - -`多Reactor多线程` 的方案虽然看起来复杂的,但是实际实现时比 `单Reactor多线程`的方案要简单的多,原因如下: - -- **分工明确**:主线程只负责接收新连接,子线程负责完成后续的业务处理 -- **主线程和子线程的交互很简单**:主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端 - - - -**应用场景** - -- `多Reactor多线程`:开源软件 `Netty`、`Memcache` - -- `多Reactor多进程`:开源软件 `Nginx`。不过 Nginx 方案与标准的多Reactor多进程有些差异,具体差异: - - 主进程仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而由子进程的 Reactor 来 accept 连接 - - 通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程 - - - -## Proactor模式 - -**Reactor 和 Proactor 的区别** - -- **Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件** - - 在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 `read` 方法来完成数据的读取,也就是要应用进程主动将 `socket` 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据 - - 简单理解:**来了事件**(有新连接、有数据可读、有数据可写)**操作系统通知应用进程,让应用进程来处理**(从驱动读取到内核以及从内核读取到用户空间) -- **Proactor 是异步网络模式, 感知的是已完成的读写事件** - - 在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据 - - 简单理解:**来了事件**(有新连接、有数据可读、有数据可写)**操作系统来处理**(从驱动读取到内核,从内核读取到用户空间),**处理完再通知应用进程** - -无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 **Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件**。 - - - -Proactor 模式的示意图如下: - -![Proactor模式](images/JAVA/Proactor模式.png) - -**工作流程** - -- Proactor Initiator 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 -- Asynchronous Operation Processor 注册到内核 -- Asynchronous Operation Processor 负责处理注册请求,并处理 I/O 操作; -- Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor -- Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理 -- Handler 完成业务处理 - - - -**平台支持** - -- **Linux**:在 `Linux` 下的 `异步I/O` 是不完善的,`aio` 系列函数是由 `POSIX` 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步。并且仅仅支持基于本地文件的 `aio` 异步操作,网络编程中的 `socket` 是不支持的,这也使得基于 `Linux` 的高性能网络程序都是使用 `Reactor` 方案 -- **Windows** :在 `Windows` 下实现了一套完整的支持 `socket` 的异步编程接口,这套接口就是 `IOCP`,是由操作系统级别实现的 `异步I/O`,真正意义上 `异步I/O`,因此在 `Windows` 里实现高性能网络程序可以使用效率更高的 `Proactor` 方案 - - - -## select/poll/epoll - -select/poll/epoll对比: - -![select、poll、epoll对比](images/JAVA/select、poll、epoll对比.png) - -**注意**:**遍历**相当于查看所有的位置,**回调**相当于查看对应的位置。 - - - -### select - -![select工作流程](images/JAVA/select工作流程.jpg) - -POSIX所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理 - -**缺点** - -- 单个进程可监视的fd数量被限制,即能监听端口的数量有限,数值存在如下文件里:`cat /proc/sys/fs/file-max` -- 对socket是线性扫描,即采用轮询的方法,效率较低 -- select采取了内存拷贝方法来实现内核将FD消息通知给用户空间,这样一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大 - - - -select是第一版IO复用,提出后暴漏了很多问题。 - -- select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的 -- select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但不会告诉是那个sock上有数据,只能自己遍历查找 -- select 只能监视1024个链接 -- select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现这个sock不用,要收回,这个select 不支持的 - - - -### poll - -![poll工作流程](images/JAVA/poll工作流程.jpg) - -本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态 - -- 其没有最大连接数的限制,原因是它是基于链表来存储的 -- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义 -- poll特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd -- 边缘触发:只通知一次,epoll用的就是边缘触发 - - - -poll 修复了 select 的很多问题: - -- poll 去掉了1024个链接的限制 -- poll 从设计上来说不再修改传入数组 - -但是poll仍然不是线程安全的, 这就意味着不管服务器有多强悍,你也只能在一个线程里面处理一组 I/O 流。你当然可以拿多进程来配合了,不过然后你就有了多进程的各种问题。 - - - -### epoll - -![epoll工作流程](images/JAVA/epoll工作流程.jpg) - -在Linux2.6内核中提出的select和poll的增强版本 - -- 支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次 -- 使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知 - -**优点** - -- 没有最大并发连接的限制:能打开的FD的上限远大于1024(1G的内存能监听约10万个端口) -- 效率提升:非轮询的方式,不会随着FD数目的增加而效率下降;只有活跃可用的FD才会调用callback函数,即epoll最大的优点就在于它只管理“活跃”的连接,而跟连接总数无关 -- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销 -- 文件映射内存直接通过地址空间访问,效率更高,把文件映射到内存中 - - - -epoll 可以说是 I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如: - -- epoll 现在是线程安全的 -- epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了 -- epoll 内核态管理了各种IO文件描述符, 以前用户态发送所有文件描述符到内核态,然后内核态负责筛选返回可用数组,现在epoll模式下所有文件描述符在内核态有存,查询时不用传文件描述符进去了 - - - -## BIO(同步阻塞I/O) - -用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。 - -![同步阻塞IO](images/JAVA/同步阻塞IO.png) - -**特点:**I/O执行的两个阶段进程都是阻塞的。 - -**优点** - -- 能够及时的返回数据,无延迟 -- 程序简单,进程挂起基本不会消耗CPU时间 - -**缺点** - -- I/O等待对性能影响较大 -- 每个连接需要独立的一个进程/线程处理,当并发请求量较大时为了维护程序,内存、线程和CPU上下文切换开销较大,因此较少在开发环境中使用 - - - -## NIO(同步非阻塞I/O) - -用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求过程中,虽然用户线程每次发起IO请求后可以立即返回,但为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。 - -![同步非阻塞IO](images/JAVA/同步非阻塞IO.png) - -**特点:**non-blocking I/O模式需要不断的主动询问kernel数据是否已准备好。 - -**优点** - -- 进程在等待当前任务完成时,可以同时执行其他任务进程不会被阻塞在内核等待数据过程,每次发起的I/O请求会立即返回,具有较好的实时性 - -**缺点** - -- 不断轮询将占用大量CPU时间,系统资源利用率大打折扣,影响性能,整体数据吞吐量下降 -- 该模型不适用web服务器 - - - -## IO多路复用(异步阻塞I/O) - -通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。 - -![IO多路复用](images/JAVA/IO多路复用.png) - -**特点:**通过一种机制能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个变为可读就绪状态,select()/poll()函数就会返回。 - -**优点** - -- 可以基于一个阻塞对象,同时在多个描述符上可读就绪,而不是使用多个线程(每个描述符一个线程),即能处理更多的连接 -- 可以节省更多的系统资源 - -**缺点:** - -- 如果处理的连接数不是很多的话,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好 -- 可能延迟还更大,因为处理一个连接数需要发起两次system call - - - -## AIO(异步非阻塞I/O) - -AIO(异步非阻塞IO,即NIO.2)。异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。 - -![异步非阻塞IO](images/JAVA/异步非阻塞IO.png) - -**特点:**第一阶段和第二阶段都是有内核完成。 - -**优点** - -- 能充分利用DMA的特性,将I/O操作与计算重叠,提高性能、资源利用率与并发能力 - -**缺点** - -- 在程序的实现上比较困难 -- 要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 复用式I/O模型为主 - - - -## 信号驱动式I/O - -信号驱动式I/O是指进程预先告知内核,使得某个文件描述符上发生了变化时,内核使用信号通知该进程。在信号驱动式I/O模型,进程使用socket进行信号驱动I/O,并建立一个SIGIO信号处理函数,当进程通过该信号处理函数向内核发起I/O调用时,内核并没有准备好数据报,而是返回一个信号给进程,此时进程可以继续发起其他I/O调用。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。当数据报准备好之后,内核会递交SIGIO信号,通知用户空间的信号处理程序,数据已准备好;此时进程会发起recvfrom的系统调用,这一个阶段与阻塞式I/O无异。也就是说,在第二阶段内核复制数据到用户空间的过程中,进程同样是被阻塞的。 - -**信号驱动式I/O的整个过程图如下:** - -![信号驱动式IO](images/JAVA/信号驱动式IO.png) - -**第一阶段(非阻塞):** - -- ①:进程使用socket进行信号驱动I/O,建立SIGIO信号处理函数,向内核发起系统调用,内核在未准备好数据报的情况下返回一个信号给进程,此时进程可以继续做其他事情 -- ②:内核将磁盘中的数据加载至内核缓冲区完成后,会递交SIGIO信号给用户空间的信号处理程序 - -**第二阶段(阻塞):** - -- ③:进程在收到SIGIO信号程序之后,进程向内核发起系统调用(recvfrom) -- ④:内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成 -- ⑤:内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作 - - - -**特点:**借助socket进行信号驱动I/O并建立SIGIO信号处理函数 - -**优点** - -- 线程并没有在第一阶段(数据等待)时被阻塞,提高了资源利用率; - -**缺点** - -- 在程序的实现上比较困难 -- 信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失 - - - -**信号通知机制** - -- **水平触发:**指数据报到内核缓冲区准备好之后,内核通知进程后,进程因繁忙未发起recvfrom系统调用;内核会再次发送通知信号,循环往复,直到进程来请求recvfrom系统调用。很明显,这种方式会频繁消耗过多的系统资源 -- **边缘触发:**内核只会发送一次通知信号 - - - # Classloader ## JVM类加载机制 diff --git a/OS.md b/OS.md index 5911ef4..82ea90c 100644 --- a/OS.md +++ b/OS.md @@ -4,6 +4,439 @@ [TOC] +# I/O + +Linux/Unix常见IO模型:**阻塞(Blocking I/O)**、**非阻塞(Non-Blocking I/O)**、**IO多路复用(I/O Multiplexing)**、 **信号驱动 I/O(Signal Driven I/O)**(不常用)和**异步(Asynchronous I/O)**。网络IO操作主要涉及到**内核**和**进程**,其主要分为两个过程: + +- 内核等待数据可操作(可读或可写)——阻塞与非阻塞 +- 内核与进程之间数据的拷贝——同步与异步 + + + +## 基础概念 + +**① 阻塞(Blocking)和非阻塞(Non-blocking)** + +阻塞和非阻塞发生在内核等待数据可操作(可读或可写)时,指做事时是否需要等待应答。 + +- **阻塞:** 内核检查数据不可操作,则不立即返回 +- **非阻塞:** 内核检查数据不可操作,则立即返回 + +**② 同步(Synchronous)和异步(Asynchronous)** + +同步和异步发生在内核与进程交互时,进程触发IO操作后是否需要等待或轮询查看结果。 + +- **同步:** 触发IO操作 → 等待或轮询查看结果 +- **异步:** 触发IO操作 → 直接返回去做其它事,IO处理完后内核主动通知进程 + + + +### 阻塞I/O + +当用户程序执行 `read` ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,`read` 才会返回。阻塞等待的是 **内核数据准备好** 和 **数据从内核态拷贝到用户态** 两个过程。过程如下图: + +![阻塞IO](images/OS/阻塞IO.png) + + + +### 非阻塞I/O + +非阻塞的 `read` 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,`read` 调用才可以获取到结果。过程如下图: + +![非阻塞IO](images/OS/非阻塞IO.png) + +注意,**这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。** + + + +### 同步I/O + +无论 `read` 和 `send` 是 `阻塞I/O`,还是 `非阻塞I/O` 都是同步调用。因为在 `read` 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,即这个过程是同步的,如果内核实现的拷贝效率不高,`read` 调用就会在这个同步过程中等待比较长的时间。 + + + +### 异步I/O + +真正的异步 I/O 是`内核数据准备好` 和 `数据从内核态拷贝到用户态` 这两个过程都不用等待。 + +当我们发起 `aio_read` (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。过程如下图: + +![异步IO](images/OS/异步IO.png) + + + +## Reactor模式 + +`Reactor 模式` 即 I/O 多路复用监听事件,收到事件后根据事件类型分配(Dispatch)给某个进程/线程。其主要由 `Reactor` 和 `处理资源池` 两个核心部分组成: + +- **Reactor**:负责监听和分发事件。事件类型包含连接事件、读写事件 +- **处理资源池**:负责处理事件。如:read -> 业务逻辑 -> send + +Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于: + +- Reactor 的数量可以只有一个,也可以有多个 +- 处理资源池可以是单个进程/线程,也可以是多个进程/线程 + + + +将上面的两个因素排列组设一下,理论上就可以有 4 种方案选择: + +- **单 Reactor 单进程/线程** +- **单 Reactor 多进程/线程** +- **多 Reactor 单进程/线程**:相比 `单Reactor单进程/线程` 方案不仅复杂而且没有性能优势,因此可以忽略 +- **多 Reactor 多进程/线程** + + + +### 单Reactor单进程/单线程 + +一般来说,C 语言实现的是`单Reactor单进程`的方案,因为 C 语编写完的程序,运行后就是一个独立的进程,不需要在进程中再创建线程。而 Java 语言实现的是「单 Reactor 单线程」的方案,因为 Java 程序是跑在 Java 虚拟机这个进程上面的,虚拟机中有很多线程,我们写的 Java 程序只是其中的一个线程而已。以下是「`单 Reactor单进程`」的方案示意图: + + +![单Reactor单进程线程](images/OS/单Reactor单进程线程.png) + +可以看到进程里有 `Reactor`、`Acceptor`、`Handler` 这三个对象: + +- `Reactor` 对象的作用是监听和分发事件 +- `Acceptor` 对象的作用是获取连接 +- `Handler` 对象的作用是处理业务 + +对象里的 `select`、`accept`、`read`、`send` 是系统调用函数,`dispatch` 和 `业务处理` 是需要完成的操作,其中 `dispatch` 是分发事件操作。 + + + +**工作流程** + +- `Reactor` 对象通过 `select` (IO多路复用接口) 监听事件,收到事件后通过 `dispatch` 进行分发,具体分发给 `Acceptor` 对象还是 `Handler` 对象,还要看收到的事件类型 +- 如果是连接建立的事件,则交由 `Acceptor` 对象进行处理,`Acceptor` 对象会通过 `accept` 方法 获取连接,并创建一个 `Handler` 对象来处理后续的响应事件 +- 如果不是连接建立事件, 则交由当前连接对应的 `Handler` 对象来进行响应 +- `Handler` 对象通过 `read` -> 业务处理 -> `send` 的流程来完成完整的业务流程 + + + +**优缺点** + +- **优点** + - 因为全部工作都在同一个进程内完成,所以实现起来比较简单 + - 不需要考虑进程间通信,也不用担心多进程竞争 + +- **缺点** + - 因为只有一个进程,无法充分利用 多核 `CPU` 的性能 + - `Handler`对象在业务处理时,整个进程是无法处理其它连接事件,如果业务处理耗时比较长,那么就造成响应的延迟 + + + +**使用场景** + +单Reactor单进程的方案`不适用计算机密集型的场景`,`只适用于业务处理非常快速的场景`。如:Redis 是由 C 语言实现的,它采用的正是「单Reactor单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。 + + + +### 单Reactor多线程/多进程 + +如果要克服`单 Reactor 单线程/单进程`方案的缺点,那么就需要引入多线程/多进程,这样就产生了**单Reactor多线程/多进程**的方案。具体方案的示意图如下: + +![单Reactor多线程多进程](images/OS/单Reactor多线程多进程.png) + +**工作流程** + +- `Reactor` 对象通过 `select` (IO 多路复用接口) 监听事件,收到事件后通过 `dispatch` 进行分发,具体分发给 `Acceptor` 对象还是 `Handler` 对象,还要看收到的事件类型 +- 如果是连接建立的事件,则交由 `Acceptor` 对象进行处理,`Acceptor` 对象会通过 `accept` 方法获取连接,并创建一个 `Handler` 对象来处理后续的响应事件 +- 如果不是连接建立事件, 则交由当前连接对应的 `Handler` 对象来进行响应 +- `Handler` 对象不再负责业务处理,只负责数据的接收和发送,`Handler` 对象通过 `read` 读取到数据后,会将数据发给子线程里的 `Processor` 对象进行业务处理 +- 子线程里的 `Processor` 对象就进行业务处理,处理完后,将结果发给主线程中的 `Handler` 对象,接着由 `Handler` 通过 `send` 方法将响应结果发送给 `client` + + + +**单Reator多线程** + +- **优势**:能够充分利用多核 `CPU` 的能力 + +- **缺点**:带来了多线程竞争资源问题(如需加互斥锁解决) + +**单Reactor多进程** + +- **缺点** + - 需要考虑子进程和父进程的双向通信 + - 进程间通信远比线程间通信复杂 + + + +另外,`单Reactor` 的模式还有个问题,因为一个 `Reactor` 对象承担所有事件的 `监听` 和 `响应` ,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能瓶颈。 + + + +### 多Reactor多进程/多线程 + +要解决 `单Reactor` 的问题,就是将 `单Reactor` 实现成 `多Reactor`,这样就产生了 **多Reactor多进程/线程** 方案。其方案的示意图如下(以线程为例): + +![多Reactor多进程线程](images/OS/多Reactor多进程线程.png) + +**工作流程** + +- 主线程中的 `MainReactor` 对象通过 `select` 监控连接建立事件,收到事件后通过 `Acceptor` 对象中的 `accept` 获取连接,将新的连接分配给某个子线程 +- 子线程中的 `SubReactor` 对象将 `MainReactor` 对象分配的连接加入 `select` 继续进行监听,并创建一个 `Handler` 用于处理连接的响应事件 +- 如果有新的事件发生时,`SubReactor` 对象会调用当前连接对应的 `Handler` 对象来进行响应 +- `Handler` 对象通过 `read` -> 业务处理 -> `send` 的流程来完成完整的业务流程 + + + +**方案优势** + +`多Reactor多线程` 的方案虽然看起来复杂的,但是实际实现时比 `单Reactor多线程`的方案要简单的多,原因如下: + +- **分工明确**:主线程只负责接收新连接,子线程负责完成后续的业务处理 +- **主线程和子线程的交互很简单**:主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端 + + + +**应用场景** + +- `多Reactor多线程`:开源软件 `Netty`、`Memcache` + +- `多Reactor多进程`:开源软件 `Nginx`。不过 Nginx 方案与标准的多Reactor多进程有些差异,具体差异: + - 主进程仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而由子进程的 Reactor 来 accept 连接 + - 通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程 + + + +## Proactor模式 + +**Reactor 和 Proactor 的区别** + +- **Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件** + - 在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 `read` 方法来完成数据的读取,也就是要应用进程主动将 `socket` 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据 + - 简单理解:**来了事件**(有新连接、有数据可读、有数据可写)**操作系统通知应用进程,让应用进程来处理**(从驱动读取到内核以及从内核读取到用户空间) +- **Proactor 是异步网络模式, 感知的是已完成的读写事件** + - 在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据 + - 简单理解:**来了事件**(有新连接、有数据可读、有数据可写)**操作系统来处理**(从驱动读取到内核,从内核读取到用户空间),**处理完再通知应用进程** + +无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 **Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件**。 + + + +Proactor 模式的示意图如下: + +![Proactor模式](images/OS/Proactor模式.png) + +**工作流程** + +- Proactor Initiator 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 +- Asynchronous Operation Processor 注册到内核 +- Asynchronous Operation Processor 负责处理注册请求,并处理 I/O 操作; +- Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor +- Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理 +- Handler 完成业务处理 + + + +**平台支持** + +- **Linux**:在 `Linux` 下的 `异步I/O` 是不完善的,`aio` 系列函数是由 `POSIX` 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步。并且仅仅支持基于本地文件的 `aio` 异步操作,网络编程中的 `socket` 是不支持的,这也使得基于 `Linux` 的高性能网络程序都是使用 `Reactor` 方案 +- **Windows** :在 `Windows` 下实现了一套完整的支持 `socket` 的异步编程接口,这套接口就是 `IOCP`,是由操作系统级别实现的 `异步I/O`,真正意义上 `异步I/O`,因此在 `Windows` 里实现高性能网络程序可以使用效率更高的 `Proactor` 方案 + + + +## select/poll/epoll + +select/poll/epoll对比: + +![select、poll、epoll对比](images/OS/select、poll、epoll对比.png) + +**注意**:**遍历**相当于查看所有的位置,**回调**相当于查看对应的位置。 + + + +### select + +![select工作流程](images/OS/select工作流程.jpg) + +POSIX所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理 + +**缺点** + +- 单个进程可监视的fd数量被限制,即能监听端口的数量有限,数值存在如下文件里:`cat /proc/sys/fs/file-max` +- 对socket是线性扫描,即采用轮询的方法,效率较低 +- select采取了内存拷贝方法来实现内核将FD消息通知给用户空间,这样一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大 + + + +select是第一版IO复用,提出后暴漏了很多问题。 + +- select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的 +- select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但不会告诉是那个sock上有数据,只能自己遍历查找 +- select 只能监视1024个链接 +- select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现这个sock不用,要收回,这个select 不支持的 + + + +### poll + +![poll工作流程](images/OS/poll工作流程.jpg) + +本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态 + +- 其没有最大连接数的限制,原因是它是基于链表来存储的 +- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义 +- poll特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd +- 边缘触发:只通知一次,epoll用的就是边缘触发 + + + +poll 修复了 select 的很多问题: + +- poll 去掉了1024个链接的限制 +- poll 从设计上来说不再修改传入数组 + +但是poll仍然不是线程安全的, 这就意味着不管服务器有多强悍,你也只能在一个线程里面处理一组 I/O 流。你当然可以拿多进程来配合了,不过然后你就有了多进程的各种问题。 + + + +### epoll + +![epoll工作流程](images/OS/epoll工作流程.jpg) + +在Linux2.6内核中提出的select和poll的增强版本 + +- 支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次 +- 使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知 + +**优点** + +- 没有最大并发连接的限制:能打开的FD的上限远大于1024(1G的内存能监听约10万个端口) +- 效率提升:非轮询的方式,不会随着FD数目的增加而效率下降;只有活跃可用的FD才会调用callback函数,即epoll最大的优点就在于它只管理“活跃”的连接,而跟连接总数无关 +- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销 +- 文件映射内存直接通过地址空间访问,效率更高,把文件映射到内存中 + + + +epoll 可以说是 I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如: + +- epoll 现在是线程安全的 +- epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了 +- epoll 内核态管理了各种IO文件描述符, 以前用户态发送所有文件描述符到内核态,然后内核态负责筛选返回可用数组,现在epoll模式下所有文件描述符在内核态有存,查询时不用传文件描述符进去了 + + + +## BIO(同步阻塞I/O) + +用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。 + +![同步阻塞IO](images/OS/同步阻塞IO.png) + +**特点:**I/O执行的两个阶段进程都是阻塞的。 + +**优点** + +- 能够及时的返回数据,无延迟 +- 程序简单,进程挂起基本不会消耗CPU时间 + +**缺点** + +- I/O等待对性能影响较大 +- 每个连接需要独立的一个进程/线程处理,当并发请求量较大时为了维护程序,内存、线程和CPU上下文切换开销较大,因此较少在开发环境中使用 + + + +## NIO(同步非阻塞I/O) + +用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求过程中,虽然用户线程每次发起IO请求后可以立即返回,但为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。 + +![同步非阻塞IO](images/OS/同步非阻塞IO.png) + +**特点:**non-blocking I/O模式需要不断的主动询问kernel数据是否已准备好。 + +**优点** + +- 进程在等待当前任务完成时,可以同时执行其他任务进程不会被阻塞在内核等待数据过程,每次发起的I/O请求会立即返回,具有较好的实时性 + +**缺点** + +- 不断轮询将占用大量CPU时间,系统资源利用率大打折扣,影响性能,整体数据吞吐量下降 +- 该模型不适用web服务器 + + + +## IO多路复用(异步阻塞I/O) + +通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。 + +![IO多路复用](images/OS/IO多路复用.png) + +**特点:**通过一种机制能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个变为可读就绪状态,select()/poll()函数就会返回。 + +**优点** + +- 可以基于一个阻塞对象,同时在多个描述符上可读就绪,而不是使用多个线程(每个描述符一个线程),即能处理更多的连接 +- 可以节省更多的系统资源 + +**缺点:** + +- 如果处理的连接数不是很多的话,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好 +- 可能延迟还更大,因为处理一个连接数需要发起两次system call + + + +## AIO(异步非阻塞I/O) + +AIO(异步非阻塞IO,即NIO.2)。异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。 + +![异步非阻塞IO](images/OS/异步非阻塞IO.png) + +**特点:**第一阶段和第二阶段都是有内核完成。 + +**优点** + +- 能充分利用DMA的特性,将I/O操作与计算重叠,提高性能、资源利用率与并发能力 + +**缺点** + +- 在程序的实现上比较困难 +- 要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 复用式I/O模型为主 + + + +## 信号驱动式I/O + +信号驱动式I/O是指进程预先告知内核,使得某个文件描述符上发生了变化时,内核使用信号通知该进程。在信号驱动式I/O模型,进程使用socket进行信号驱动I/O,并建立一个SIGIO信号处理函数,当进程通过该信号处理函数向内核发起I/O调用时,内核并没有准备好数据报,而是返回一个信号给进程,此时进程可以继续发起其他I/O调用。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。当数据报准备好之后,内核会递交SIGIO信号,通知用户空间的信号处理程序,数据已准备好;此时进程会发起recvfrom的系统调用,这一个阶段与阻塞式I/O无异。也就是说,在第二阶段内核复制数据到用户空间的过程中,进程同样是被阻塞的。 + +**信号驱动式I/O的整个过程图如下:** + +![信号驱动式IO](images/OS/信号驱动式IO.png) + +**第一阶段(非阻塞):** + +- ①:进程使用socket进行信号驱动I/O,建立SIGIO信号处理函数,向内核发起系统调用,内核在未准备好数据报的情况下返回一个信号给进程,此时进程可以继续做其他事情 +- ②:内核将磁盘中的数据加载至内核缓冲区完成后,会递交SIGIO信号给用户空间的信号处理程序 + +**第二阶段(阻塞):** + +- ③:进程在收到SIGIO信号程序之后,进程向内核发起系统调用(recvfrom) +- ④:内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成 +- ⑤:内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作 + + + +**特点:**借助socket进行信号驱动I/O并建立SIGIO信号处理函数 + +**优点** + +- 线程并没有在第一阶段(数据等待)时被阻塞,提高了资源利用率; + +**缺点** + +- 在程序的实现上比较困难 +- 信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失 + + + +**信号通知机制** + +- **水平触发:**指数据报到内核缓冲区准备好之后,内核通知进程后,进程因繁忙未发起recvfrom系统调用;内核会再次发送通知信号,循环往复,直到进程来请求recvfrom系统调用。很明显,这种方式会频繁消耗过多的系统资源 +- **边缘触发:**内核只会发送一次通知信号 + + + # TCP TCP是**面向连接的、可靠的、基于字节流**的传输层通信协议: diff --git a/images/JAVA/IO多路复用.png b/images/OS/IO多路复用.png similarity index 100% rename from images/JAVA/IO多路复用.png rename to images/OS/IO多路复用.png diff --git a/images/JAVA/Proactor模式.png b/images/OS/Proactor模式.png similarity index 100% rename from images/JAVA/Proactor模式.png rename to images/OS/Proactor模式.png diff --git a/images/JAVA/epoll工作流程.jpg b/images/OS/epoll工作流程.jpg similarity index 100% rename from images/JAVA/epoll工作流程.jpg rename to images/OS/epoll工作流程.jpg diff --git a/images/JAVA/poll工作流程.jpg b/images/OS/poll工作流程.jpg similarity index 100% rename from images/JAVA/poll工作流程.jpg rename to images/OS/poll工作流程.jpg diff --git a/images/JAVA/select、poll、epoll对比.png b/images/OS/select、poll、epoll对比.png similarity index 100% rename from images/JAVA/select、poll、epoll对比.png rename to images/OS/select、poll、epoll对比.png diff --git a/images/JAVA/select工作流程.jpg b/images/OS/select工作流程.jpg similarity index 100% rename from images/JAVA/select工作流程.jpg rename to images/OS/select工作流程.jpg diff --git a/images/JAVA/信号驱动式IO.png b/images/OS/信号驱动式IO.png similarity index 100% rename from images/JAVA/信号驱动式IO.png rename to images/OS/信号驱动式IO.png diff --git a/images/JAVA/单Reactor单进程线程.png b/images/OS/单Reactor单进程线程.png similarity index 100% rename from images/JAVA/单Reactor单进程线程.png rename to images/OS/单Reactor单进程线程.png diff --git a/images/JAVA/单Reactor多线程多进程.png b/images/OS/单Reactor多线程多进程.png similarity index 100% rename from images/JAVA/单Reactor多线程多进程.png rename to images/OS/单Reactor多线程多进程.png diff --git a/images/JAVA/同步阻塞IO.png b/images/OS/同步阻塞IO.png similarity index 100% rename from images/JAVA/同步阻塞IO.png rename to images/OS/同步阻塞IO.png diff --git a/images/JAVA/同步非阻塞IO.png b/images/OS/同步非阻塞IO.png similarity index 100% rename from images/JAVA/同步非阻塞IO.png rename to images/OS/同步非阻塞IO.png diff --git a/images/JAVA/多Reactor多进程线程.png b/images/OS/多Reactor多进程线程.png similarity index 100% rename from images/JAVA/多Reactor多进程线程.png rename to images/OS/多Reactor多进程线程.png diff --git a/images/JAVA/异步IO.png b/images/OS/异步IO.png similarity index 100% rename from images/JAVA/异步IO.png rename to images/OS/异步IO.png diff --git a/images/JAVA/异步非阻塞IO.png b/images/OS/异步非阻塞IO.png similarity index 100% rename from images/JAVA/异步非阻塞IO.png rename to images/OS/异步非阻塞IO.png diff --git a/images/JAVA/阻塞IO.png b/images/OS/阻塞IO.png similarity index 100% rename from images/JAVA/阻塞IO.png rename to images/OS/阻塞IO.png diff --git a/images/JAVA/非阻塞IO.png b/images/OS/非阻塞IO.png similarity index 100% rename from images/JAVA/非阻塞IO.png rename to images/OS/非阻塞IO.png