【IO】IO 模型
【IO】IO 模型
首先需要区分几个概念
非阻塞 I/O,字符转换,缓冲以及通道
从 JDK 7 版本开始,Java 新加入的文件和网络 io 特性称为 nio2 (new io 2, 因为 jdk1.4 中已经有过一个 nio 了),包含了众多性能和功能上的改进,其中最重要的部分,就是对异步 io 的支持,称为 Java AIO (asynchronous IO)。
因为 AIO 的实施需充分调用 OS 参与,IO 需要操作系统支持、并发也同样需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。所以本文也附带介绍了 Linux 2.6 及以后版本新增的 AIO 特性(因为这跟 Java AIO 是对应关系)
- IO 分为内存 IO / 网络 IO / 磁盘 IO,磁盘 IO 都是阻塞的
- 阻塞与非阻塞是通过代码来实现的,区别在于是在于发过来操作请求,数据准备好才返回(阻塞)还是直接返回(非阻塞)
- IO 读取顺序:磁盘(磁盘 IO)/ 网卡(网络 IO)—> 内核缓冲区 —> 用户内存,重点在于后面的过程是否是需要进程阻塞等待的
IO 模型#
-
同步阻塞(blocking IO)
A 去钓鱼了,你一直在那等,鱼上钩了,然后把鱼钓上来(全程阻塞)普遍使用的 IO 模型,linux 默认的 IO 模型。
进程调用 recvfrom 一直到 recvfrom 返回 -
同步非阻塞(noblocking IO)
B 去钓鱼了,放好钩后,然后开始看书,刷抖音去了,过一会看看是不是鱼上钩了。然后等鱼上钩了,把鱼钓上来
这个询问其实询问的是操作系统内核,即文件描述缓冲区是否就绪,准备好了,就进行拷贝数据包的操作。没有数据报准备好时,也不阻塞程序,内核直接返回为准备就绪的信号。
但是这个轮询其实是对 CPU 来说是一个比较大的消耗很少使用,因为他浪费了大量的 CPU 资源
进程 recvfrom,如果没有准备就绪的话,直接返回 EWOULDBLOCK,过段时间再次调用 recvfrom,直到正常返回。这个操作其实就是 epolling(轮询),epolling 内核是一个很占用 CPU 资源的操作
但是对管道的操作,最好使用非阻塞的方式
- nginx 和 node 对于本地文件的 IO 使用的是,以线程的方法模拟非阻塞的效果
- 对于静态文件的 IO 使用的是 zero copy(例如 sendfile)的效率是非常高的
-
信号驱动 IO(signal blocking IO)
C 去钓鱼了,放好钩,但是这个时候他在鱼竿上放了一个小铃铛,然后去看书刷抖音了,等鱼上钩了,铃铛就响了,她就可以把鱼钓上来了
进程告诉内核,数据报准备好的时候,你告诉我一个信号,我对 sinal 信号进行捕捉,然后调用信号处理函数来他要求套接字一定允许使用异步 IO,设置简单,但是困难的是判定 SIGIO 信号产生的时候程序处于什么状态,UDP 的话也就是接收到数据报,或者发生异步错误,但是 TCP 的情况就太多了。
NTP 服务器,它使用 UDP, -
IO 多路复用(IO multiplexing)
D 也去钓鱼了,但是 D 带了很多鱼竿,D 用的方法和 B 差不多,他来回走着看是否有鱼上钩了,上钩了就收杆
这样增加了效率,减少了等待的时间,IO 多路转接多了一个 select 函数,其中一个参数是文件描述符的集合,他循环对这些文件描述符进行监听,当某个文件描述符状态改为就绪,就处理这个,select 只负责等,recvfrom 只负责拷贝。
IO 多路复用用到了 select () 和 poll () 或者 epoll ()(2.6 开始)函数,主要在这些函数上面阻塞了,调用 select 的时候,只有准备就绪了才会返回,在单个 IO 操作下,和同步阻塞是没有任何区别的,高级之处在于对于多个 IO 的监听,所以他用在多个 IO / 多类型 / 多协议的场景下
-
异步 IO (asynchronous IO)
E 也想钓鱼,但是 E 有事情,他让 F 来帮他钓鱼,F 掉到鱼了,就通知 E,E 来收杆。
应用程序调用 aio_read,内核一方面去取数据报内容返回,另一方面将控制权还给应用程序。应用程序继续处理其他事情,是一种非阻塞的。
之前的都只是不管怎么说,最多也就是等待过程非阻塞,现在读都是非阻塞的,这才是异步。
IO 的过程其实就两部,一部分等待读或者写就绪(也就是等),一部分就是读或者(数据搬迁)
以上五种模型的效率。A<B<D<C<E
select poll epoll 区别#
F 同样去钓鱼,但是他想钓的鱼是金枪鱼,他告诉 G 帮他看着,接下来看三种方式下,G 的区别
select:G 一直去看有鱼上钩了没有,有鱼上钩了,他就去看看这个鱼是不是金枪鱼,是的话他就通知 F,但是 G 最多只能管理 1024 个杆。无差别轮询,处理的流越多,轮询时间越长,时间复杂度 O(n)
poll:poll 的方式和 select 一样,但是不限制数量,它是用链表存储的。时间复杂度 O(n)
epoll:epoll 可以理解为 event poll,epoll 的做法相当于给每条鱼都打上了标签,这样鱼上钩了,自动就知道什么鱼了。epoll 基于内核的反射机制。在有活跃的 socket 的时候,系统会调用我们提前设置好的回掉函数。这样就比 select/poll 轮询方便很多
以上最重要的区别就是 epoll 把轮询的操作托让给了内核去做,因为内核更高效。但是托让给内核,我们调用了系统调用,如果轮询结果为空,也没有 wakeup 或新消息处理,这样就会发生空轮询,CPU 使用率就会 100%
selector.select()
操作是阻塞的,只有被监听的 fd 有读写操作时,才被唤醒
netty 对于此问题解决方案是对 Selector 的 select 操作周期进行统计,没完成一次空的 select 操作就进行一次计数。当在某个周期内发生了 N 次空轮询,就触发 epoll 死循环 BUG。然后重建 Selector,判断是否是其他线程发起的重建请求,若不是讲原来的 SocketChannel 从旧的 Selector 上去除注册。注册到新的 Selector,并把之前的给关闭
windows 上面的 iocp 模型是 aio 模型,他会在哪只操作完 IO 以后,通过 get 函数获取一个完成事件的通知。
NIO 场景#
NIO 可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现 NIO 的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如 P2P 网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如下图所示:
如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的 IO 服务器实现可能非常契合。
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
C10K/M 问题#
OS 内核中的两个基本问题:
连接数 = 线程数 / 进程数:当一个数据包进来,内核会遍历其所有进程以决定由哪个进程来处理这个数据包。
连接数 = 选择数 / 轮询次数(单线程):同样的可扩展性问题,每个包都要走一遭列表上所有的 socket。
通过上述针对 Apache 所表现出的问题,实际上彻底解决并发性能问题的解决方法的根本就是改进 OS 内核使其在常数时间内查找,使线程切换时间与线程数量无关,使用一个新的可扩展 epoll ()/IOCompletionPort 常数时间去做 socket 查询。
因为线程调度并没有得到扩展,所以服务器大规模对 socket 使用 epoll 方法,这样就导致需要使用异步编程模式,而这些编程模式正是 Nginx 和 Node 类型服务器具有的。所以当从 Apache 迁移到 Nginx 和 Node 类型服务器时,即使在一个配置较低的服务器上增加连接数,性能也不会突降。所以在处理 C10K 连接时,一台笔记本电脑的速度甚至超过了 16 核的服务器。这也是前一个 10 年解决 C10K 问题的普遍方法。
1 千万的并发连接数;
100 万个连接 / 秒:每个连接以这个速率持续约 10 秒;
10GB / 秒的连接:快速连接到互联网;
1 千万个数据包 / 秒:据估计目前的服务器每秒处理 50K 数据包,以后会更多;
10 微秒的延迟:可扩展服务器也许可以处理这个规模(但延迟可能会飙升);
10 微秒的抖动:限制最大延迟;
并发 10 核技术:软件应支持更多核的服务器(通常情况下,软件能轻松扩展到四核,服务器可以扩展到更多核,因此需要重写软件,以支持更多核的服务器)
硬件不是 C10M 问题的性能瓶颈所在处,真正的问题出在软件上,内核不做重的业务逻辑
参考#
https://blog.csdn.net/ccj2020/article/details/7739880
https://blog.csdn.net/ZWE7616175/article/details/80591587
https://www.cnblogs.com/wt645631686/p/8528912.html
https://www.cnblogs.com/aspirant/p/9166944.html
五种 IO 模式 —— 阻塞 (默认 IO 模式),非阻塞 (常用语管道),IO 多路复用 (IO 多路复用的应用场景),信号 IO,异步 IO
五种 I/O 模式:
【1】阻塞 I/O(Linux 下的 I/O 操作默认是阻塞 I/O,即 open 和 socket 创建的 I/O 都是阻塞 I/O)
【2】非阻塞 I/O(可以通过 fcntl 或者 open 时使用 O_NONBLOCK 参数,将 fd 设置为非阻塞的 I/O)
【3】I/O 多路复用 (I/O 多路复用,通常需要非阻塞 I/O 配合使用)
【4】信号驱动 I/O(SIGIO)
【5】异步 I/O
一般来说,程序进行输入操作有两步:
1.等待有数据可以读
2.将数据从系统内核中拷贝到程序的数据区。
对于 sock 编程来说:
第一步: 一般来说是等待数据从网络上传到本地。当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中;
第二步: 是从内核中把数据拷贝到程序的数据区中。
阻塞 I/O 模式 // 进程处于阻塞模式时,让出 CPU,进入休眠状态
阻塞 I/O 模式是最普遍使用的 I/O 模式。是 Linux 系统下缺省的 IO 模式。
大部分程序使用的都是阻塞模式的 I/O 。
一个套接字建立后所处于的模式就是阻塞 I/O 模式。(因为 Linux 系统默认的 IO 模式是阻塞模式)
对于一个 UDP 套接字来说,数据就绪的标志比较简单:
(1)已经收到了一整个数据报
(2)没有收到。
而 TCP 这个概念就比较复杂,需要附加一些其他的变量。
一个进程调用 recvfrom ,然后系统调用并不返回知道有数据报到达本地系统,然后系统将数据拷贝到进程的缓存中。 (如果系统调用收到一个中断信号,则它的调用会被中断)
我们称这个进程在调用 recvfrom 一直到从 recvfrom 返回这段时间是阻塞的。当 recvfrom 正常返回时,我们的进程继续它的操作。
非阻塞模式 I/O // 非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的 CPU 资源。
当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核: “当我请求的 I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
我们开始对 recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返回一个 EWOULDBLOCK 的错误。
第四次我们调用 recvfrom 函数,一个数据报已经到达了,内核将它拷贝到我们的应用程序的缓冲区中,然后 recvfrom 正常返回,我们就可以对接收到的数据进行处理了。
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读 (称做 polling(轮询))。应用程序不停的 polling 内核来检查是否 I/O 操作已经就绪。这将是一个极浪费 CPU 资源的操作。这种模式使用中不是很普遍。
例如:
对管道的操作,最好使用非阻塞方式!
I/O 多路复用 // 针对批量 IP 操作时,使用 I/O 多路复用,非常有好。
在使用 I/O 多路技术的时候,我们调用 select() 函数和 poll() 函数或 epoll 函数 (2.6 内核开始支持),在调用它们的时候阻塞,而不是我们来调用 recvfrom(或 recv)的时候阻塞。
当我们调用 select 函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当 select 函数返回的时候, 也就是套接字可以读取数据的时候。 这时候我们就可以调用 recvfrom 函数来将数据拷贝到我们的程序缓冲区中。
对于单个 I/O 操作,和阻塞模式相比较,select() 和 poll() 或 epoll 并没有什么高级的地方。
而且,在阻塞模式下只需要调用一个函数:读取或发送函数。
在使用了多路复用技术后,我们需要调用两个函数了:
先调用 select() 函数或 poll() 函数,然后才能进行真正的读写。
多路复用的高级之处在于::
它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select() 函数就可以返回。
IO 多路技术一般在下面这些情况中被使用:
1、当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字),I/O 多路复用技术将会有机会得到使用。
2、当程序需要同时进行多个套接字的操作的时候。
3、如果一个 TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。
4、如果一个服务器程序同时使用 TCP 和 UDP 协议。
5、如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如 inetd 就是这样的)。
异步 IO 模式有::
1、信号驱动 I/O 模式
2、异步 I/O 模式
信号驱动 I/O 模式 // 自己没有用过。
我们可以使用信号,让内核在文件描述符就绪的时候使用 SIGIO 信号来通知我们。我们将这种模式称为信号驱动 I/O 模式。
为了在一个套接字上使用信号驱动 I/O 操作,下面这三步是所必须的。
(1)一个和 SIGIO 信号的处理函数必须设定。
(2)套接字的拥有者必须被设定。一般来说是使用 fcntl 函数的 F_SETOWN 参数来
进行设定拥有者。
(3)套接字必须被允许使用异步 I/O。一般是通过调用 fcntl 函数的 F_SETFL 命令,O_ASYNC 为参数来实现。
虽然设定套接字为异步 I/O 非常简单,但是使用起来困难的部分是怎样在程序中断定产生 SIGIO 信号发送给套接字属主的时候,程序处在什么状态。
1.UDP 套接字的 SIGIO 信号 (比较简单)
在 UDP 协议上使用异步 I/O 非常简单.这个信号将会在这个时候产生:
1、套接字收到了一个数据报的数据包。
2、套接字发生了异步错误。
当我们在使用 UDP 套接字异步 I/O 的时候,我们使用 recvfrom() 函数来读取数据报数据或是异步 I/O 错误信息。
2.TCP 套接字的 SIGIO 信号 (不会使用)
不幸的是,异步 I/O 几乎对 TCP 套接字而言没有什么作用。因为对于一个 TCP 套接字来说,SIGIO 信号发生的几率太高了,所以 SIGIO 信号并不能告诉我们究竟发生了什么事情。
在 TCP 连接中, SIGIO 信号将会在这个时候产生:
l 在一个监听某个端口的套接字上成功的建立了一个新连接。
l 一个断线的请求被成功的初始化。
l 一个断线的请求成功的结束。
l 套接字的某一个通道(发送通道或是接收通道)被关闭。
l 套接字接收到新数据。
l 套接字将数据发送出去。
l 发生了一个异步 I/O 的错误。
一个对信号驱动 I/O 比较实用的方面是 NTP(网络时间协议 Network Time Protocol)服务器,它使用 UDP。这个服务器的主循环用来接收从客户端发送过来的数据报数据包,然后再发送请求。对于这个服务器来说,记录下收到每一个数据包的具体时间是很重要的。
因为那将是返回给客户端的值,客户端要使用这个数据来计算数据报在网络上来回所花费的时间。图 6-8 表示了怎样建立这样的一个 UDP 服务器。
异步 I/O 模式 // 比如写操作,只需用写,不一定写入磁盘 (这就是异步 I/O) 的好处。异步 IO 的好处效率高。
当我们运行在异步 I/O 模式下时,我们如果想进行 I/O 操作,只需要告诉内核我们要进行 I/O 操作,然后内核会马上返回。具体的 I/O 和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的 I/O 操作和数据拷贝后,内核将通知我们的程序。
异步 I/O 和 信号驱动 I/O 的区别是:
1、信号驱动 I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送 SIGIO 消息。
2、异步 I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。
在网络环境下,通俗的讲,将 IO 分为两步:
1. 等;
2. 数据搬迁。
如果要想提高 IO 效率,需要将等的时间降低。
五种 IO 模型包括:阻塞 IO、非阻塞 IO、信号驱动 IO、IO 多路转接、异步 IO。其中,前四个被称为同步 IO。
在介绍五种 IO 模型时,我会举生活中钓鱼的例子,加深理解。
1. 阻塞 IO(blocking I/O)
A 拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。
在内核将数据准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。
其实,我们例子中所说的鱼竿就是这一个文件描述符。这个模型是我们最常见的,程序调用和我们编写的基本程序是一致的。
fd=connect();
write(fd);
read(fd);
close(fd);
程序的 read 必须在 write 之后执行,当 write 阻塞住了,read 就不能执行下去,一直处于等待状态。
2. 非阻塞 IO(noblocking I/O)
B 也在河边钓鱼,但是 B 不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B 也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但 B 在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。
其实,B 在检查鱼竿是否有鱼,是一个轮询的过程。
每次客户询问内核是否有数据准备好,即文件描述符缓冲区是否就绪。当有数据报准备好时,就进行拷贝数据报的操作。当没有数据报准备好时,也不阻塞程序,内核直接返回未准备就绪的信号,等待用户程序的下一个轮寻。
但是,轮寻对于 CPU 来说是较大的浪费,一般只有在特定的场景下才使用。
3. 信号驱动 IO(signal blocking I/O)
C 也在河边钓鱼,但与 A、B 不同的是,C 比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,C 就会将鱼钓上来。
信号驱动 IO 模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对 SIGIO 信号进行捕捉,并且调用我的信号处理函数来获取数据报。
4.IO 多路转接(I/O multiplexing)
D 同样也在河边钓鱼,但是 D 生活水平比较好,D 拿了很多的鱼竿,一次性有很多鱼竿在等,D 不断的查看每个鱼竿是否有鱼上钩。增加了效率,减少了等待的时间。
IO 多路转接是多了一个 select 函数,select 函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。
其中,select 只负责等,recvfrom 只负责拷贝。
IO 多路转接是属于阻塞 IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞 IO 的高。
5. 异步 IO(asynchronous I/O)
E 也想钓鱼,但 E 有事情,于是他雇来了 F,让 F 帮他等待鱼上钩,一旦有鱼上钩,F 就打电话给 E,E 就会将鱼钓上去。
当应用程序调用 aio_read 时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。
当内核中有数据报就绪时,由内核将数据报拷贝到应用程序中,返回 aio_read 中定义好的函数处理程序。
很少有 Linux 系统支持,Windows 的 IOCP 就是该模型。
可以看出,阻塞程度:阻塞 IO > 非阻塞 IO > 多路转接 IO > 信号驱动 IO > 异步 IO,效率是由低到高的。
什么是 IO 多路复用?Nginx 的处理机制
先来说一下什么是 IO 复用?
IO 复用解决的就是并发行的问题,比如多个用户并发访问一个 WEB 网站,对于服务端后台而言就会产生多个请求,处理多个请求对于中间件就会产生多个 IO 流对于系统的读写。那么对于 IO 流请求操作系统内核有并行处理和串行处理的概念,串行处理的方式是一个个处理,前面的发生阻塞,就没办法完成后面的请求。这个时候我们必须考虑并行的方式完成整个 IO 流的请求来实现最大的并发和吞吐,这时候就是用到 IO 复用技术。IO 复用就是让一个 Socket 来作为复用完成整个 IO 流的请求。当然实现整个 IO 流的请求多线程的方式就是其中一种。
*** 下面是一些举例,让你更清楚的了解什么是 IO 多路复用 ***
举例:在教室里面有一个老师同时给学生出一道题目,检查每个学生做的是否正确,这时候老师可以选择一个一个学生的去问学生是否做完。如果 A 学生没做完,那么再问 B 学生,B 学生没做完再问 C 学生,挨个问下去,如果发现问道某一个学生,某一个学生说做完的时候,这时候再给当下学生解答。那么这时候会发现,如果一个学生发生了阻塞,阻塞在一个学生下,其他学生就会耽误了,这时候对整个课堂效率就底下。这就是串行请求类处理。
那么多线程呢?
再举例:也是这个场景,给学生出题让学生解答。这个老师学会了分身术,每个老师对每个学生进行监听,看学生是否答题完并作出解答,这样效率就高了。这就是多线程进行 IO 流处理,那么多线程 IO 流就会产生一定的消耗,资源问题的存在。
什么是 IO 多路复用呢?
再再举例:同样也是答题场景,其实就是改为了由学生来主动跟老师汇报,比如学生 B 答完了,这时候老师跟 B 进行解答,因为 B 学习效率高,其他学生还在做题,这时候给学生 B 解答完后,老师再给第二个学生解答,这时候学生主动汇报,效率就高了。这种方式就是 IO 多路复用的方式。特点:学生主动汇报。
对于操作系统而言,IO 多路复用就是要完成操作系统 IO 的请求。对于 IO 文件的请求,当一个 IO 流要进行文件处理的时候,要获取一组文件的描述符,当文件描述符还没有就绪时,那么它就在等待,直到描述符一旦就绪,马上上报系统通知的机制,告诉应用程序我准备就绪,你可以来操作了。这就是 IO 多路复用的方式。
这种机制处理起来就很高效,多路复用就是在一个线程里,交替并发的完成。复用的就是一个线程。
------------------------ 下面来看一下知乎帖子几个前辈给出的比较粗俗易懂的解答 ------------------------
李遥:
你有 N 个不知道什么时候来水的水龙头需要接水,你根据某种信号一会儿拧这个龙头,一会儿拧那个龙头把水都接了就是多路复用(一个线程)。使用残像拳在每个水龙头前派一个你的分身蹲守就是 Threaded IO。其实后者也没啥不好,因为未来的内核会消除上下文切换的软硬件性能损耗。
Leslie:
要弄清问题先要知道问题的出现原因
原因:
由于进程的执行过程是线性的 (也就是顺序执行), 当我们调用低速系统 I/O (read,write,accept 等等), 进程可能阻塞,此时进程就阻塞在这个调用上,不能执行其他操作。阻塞很正常。接下来考虑这么一个问题:一个服务器进程和一个客户端进程通信,服务器端 read (sockfd1,bud,bufsize), 此时客户端进程没有发送数据,那么 read (阻塞调用) 将阻塞直到客户端调用 write (sockfd,but,size) 发来数据。在一个客户和服务器通信时这没什么问题,当多个客户与服务器通信时,若服务器阻塞于其中一个客户 sockfd1, 当另一个 客户的数据到达套接字 sockfd2 时,服务器不能处理,仍然阻塞在 read (sockfd1,...) 上;此时问题就出现了,不能及时处理另一个客户的服务,咋么办?I/O 多路复用来解决!
I/O 多路复用:
继续上面的问题,有多个客户连接,sockfd1,sockfd2,sockfd3..sockfdn 同时监听这 n 个客户,当其中有一个发来消息 时就从 select 的阻塞中返回,然后就调用 read 读取收到消息的 sockfd, 然后又循环回 select 阻塞;这样就不会因为阻塞在其中一个上而不能处理另一个客户的消息
Q:
那这样子,在读取 socket1 的数据时,如果其它 socket 有数据来,那么也要等到 socket1 读取完了才能继续读取其它 socket 的数据吧。那不是也阻塞住了吗?而且读取到的数据也要开启线程处理吧,那这和多线程 IO 有什么区别呢?
A:
1.CPU 本来就是线性的不论什么都需要顺序处理并行只能是多核 CPUhttp:
2.io 多路复用本来就是用来解决对多个 I/O 监听时,一个 I/O 阻塞影响其他 I/O 的问题,跟多线程没关系.
3. 跟多线程相比较,线程切换需要切换到内核进行线程切换,需要消耗时间和资源。而 I/O 多路复用不需要切换线 / 进程,效率相对较高,特别是对高并发的应用 nginx 就是用 I/O 多路复用,故而性能极佳。但多线程编程逻辑和处理上比 I/O 多路复用简单。而 I/O 多路复用处理起来较为复杂.
某匿名用户:
这些名词比较绕口,理解涵义就好。一个 epoll 场景:一个酒吧服务员(一个线程),前面趴了一群醉汉,突然一个吼一声 “倒酒”(事件),你小跑过去给他倒一杯,然后随他去吧,突然又一个要倒酒,你又过去倒上,就这样一个服务员服务好多人,有时没人喝酒,服务员处于空闲状态,可以干点别的玩玩手机。至于 epoll 与 select,poll 的区别在于后两者的场景中醉汉不说话,你要挨个问要不要酒,没时间玩手机了。io 多路复用大概就是指这几个醉汉共用一个服务员。
某匿名用户:
1 人赞同了该回答
IO 模式一般分为同步 IO 和异步 IO. 同步 IO 会阻塞进程,异步 IO 不会阻塞进程。目前 linux 上大部分用的是同步 IO, 异步 IO 在 linux 上目前还不成熟,不过 windows 的 iocp 算是真正的异步 IO。
同步 IO 又分为阻塞 IO, 非阻塞 IO, IO 多路复用. What? 同步 IO 明明会阻塞进程,为什么也包括非阻塞 IO? 因为非阻塞 IO 虽然在请求数据时不阻塞,但真正数据来临时,也就是内核数据拷贝到用户数据时,此时进程是阻塞的.
那么这些 IO 模式的区别分别是什么?接下来举个小例子来说明。假设你现在去女生宿舍楼找自己的女神,但是你只知道女神的手机号,并不知道女神的具体房间
先说同步 IO 的情况,
1. 阻塞 IO, 给女神发一条短信,说我来找你了,然后就默默的一直等着女神下楼,这个期间除了等待你不会做其他事情,属于备胎做法.
2. 非阻塞 IO, 给女神发短信,如果不回,接着再发,一直发到女神下楼,这个期间你除了发短信等待不会做其他事情,属于专一做法.
3. IO 多路复用,是找一个宿管大妈来帮你监视下楼的女生,这个期间你可以些其他的事情。例如可以顺便看看其他妹子,玩玩王者荣耀,上个厕所等等。IO 复用又包括 select, poll, epoll 模式。那么它们的区别是什么?
3.1 select 大妈每一个女生下楼,select 大妈都不知道这个是不是你的女神,她需要一个一个询问,并且 select 大妈能力还有限,最多一次帮你监视 1024 个妹子
3.2 poll 大妈不限制盯着女生的数量,只要是经过宿舍楼门口的女生,都会帮你去问是不是你女神
3.3 epoll 大妈不限制盯着女生的数量,并且也不需要一个一个去问。那么如何做呢?epoll 大妈会为每个进宿舍楼的女生脸上贴上一个大字条,上面写上女生自己的名字,只要女生下楼了,epoll 大妈就知道这个是不是你女神了,然后大妈再通知你.
上面这些同步 IO 有一个共同点就是,当女神走出宿舍门口的时候,你已经站在宿舍门口等着女神的,此时你属于阻塞状态
接下来是异步 IO 的情况
你告诉女神我来了,然后你就去王者荣耀了,一直到女神下楼了,发现找不见你了,女神再给你打电话通知你,说我下楼了,你在哪呢?这时候你才来到宿舍门口。此时属于逆袭做法。
总结:
有多个客户连接,1、2、3、4、5、N 同时监听这 M 个客户,当其中有一个发来消息时就从阻塞中返回,然后就进行 请求处理,然后又循环回阻塞;这样就不会因为阻塞在其中一个上而不能处理另一个客户的消息
具体帖子地址:https://www.zhihu.com/question/32163005
【IO】线程模型
线程模型#
-
传统线程模型
采用阻塞 IO 模型,一个链接一个线程问题:
-
Reactor 模型(反应堆模型)/Dispatcher 模型(分发模式)
Reactor 模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。针对第一个问题,我们使用线程池,不是每个连接一个线程,而是在线程建立以后,把业务处理的任务交给线程去处理,这样一个线程就可以处理多个链接的业务了
针对第二个问题,我们使用 IO 复用模型,多个连接公用一个阻塞对象,IO 复用模型,当某个连接有数据可以处理,操作系统会通知应用程序
-
单 Reactor 单线程
就是一个接待员,一个服务员
优点:简单,没有多线程,没有进程通信
缺点:性能,无法发挥多核的极致,一个 handler 卡死,导致当前进程无法使用,IO 和 CPU 不匹配
场景:客户端有限,业务处理快,比如 redis -
单 Reactor 多线程
就是一个接待员,多个服务员,但是真正的多线程是在 Worker 中,你可以认为是多个厨师
优点:充分利用的 CPU
缺点:进程通信,复杂,Reactor 承放了太多业务,高并发下可能成为性能瓶颈 -
主从 Reactor 多线程
就是多个接待员,多个服务员,多个厨师
主 Reactor 负责建立连接,建立连接后的句柄丢给子 Reactor,子 Reactor 负责监听所有事件进行处理
优点:职责明确,分摊压力
Nginx/netty/memcached 都是使用的这
-
-
Proactor 模型(前摄器)
reactor 中等待时间发生,然后让实现准备好的 handler 去处理,后者来实际读写,它是同步非阻塞的线程模型
如果 IO 改为异步,交给操作系统来完成,则可以进一步提高效率,这就是异步网络模型,Proactor
Proactor 读写在内核中完成,Reactor 读写在 Handler 里面完成
1)编程复杂性,由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的,因此开发异步应用程序更加复杂。应用程序还可能因为反向的流控而变得更加难以 Debug;
2)内存使用,缓冲区在读或写操作的时间段内必须保持住,可能造成持续的不确定性,并且每个并发操作都要求有独立的缓存,相比 Reactor 模式,在 Socket 已经准备好读或写前,是不要求开辟缓存的;
3)操作系统支持,Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下,Linux 2.6 才引入,目前异步 I/O 还不完善。
【IO】ByteBuf 和 Channel 和 Pipeline
BytetBuf#
ByteBuf 就是 JDK nio 中 Buffer 的新轮子
buffer 的主要目的进行流量整形,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O,以减少响应次数
ByteBuffer:
- 长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的 POJO 对象大于 ByteBuffer 的容量时,会发生索引越界异常;
- ByteBuffer 只有一个标识位控的指针 position, 读写的时候需要手工调用 flip () 和 rewind () 等,使用者必须小心谨慎地处理这些 API, 否则很容易导致程序处理失败;
- ByteBuffer 的 API 功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。
- 需要的话,可以自定义 buffer 类型;
- 通过组合 buffer 类型,可实现透明的 zero-copy;
- 提供动态的 buffer 类型,如 StringBuffer 一样,容量是按需扩展;
- 如果 c<t,则 n 从阈值 t (4MB) 开始,以每次增加 2 倍的方式扩容,直到双倍后的大小小于 c;
- 如果 c>t,则 n=c/t*t+t
- 无需调用 flip () 方法;方法反转(讲 Buffer 从读模式变成写模式)
- 常常比 ByteBuffer(JDK 的)快
- 使用了读写两个指针,分别记录读写的位置,复杂操作更简单
堆内存和直接内存#
NIO 的 Buffer 提供了一个可以不经过 JVM 内存直接访问系统物理内存的类 ——DirectBuffer。 DirectBuffer 类继承自 ByteBuffer,但和普通的 ByteBuffer 不同,普通的 ByteBuffer 仍在 JVM 堆上分配内存,其最大内存受到最大堆内存的限制;而 DirectBuffer 直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。
直接内存的读写操作比普通 Buffer 快,但它的创建、销毁比普通 Buffer 慢(猜测原因是 DirectBuffer 需向 OS 申请内存涉及到用户态内核态切换,而后者则直接从堆内存划内存即可)。
因此直接内存使用于需要大内存空间且频繁访问的场合,不适用于频繁申请释放内存的场合。
Note:DirectBuffer 并没有真正向 OS 申请分配内存,其最终还是通过调用 Unsafe 的 allocateMemory () 来进行内存分配。不过 JVM 对 Direct Memory 可申请的大小也有限制,可用 - XX:MaxDirectMemorySize=1M 设置,这部分内存不受 JVM 垃圾回收管理。
最佳实践:在 I/O 通信线程的读写缓冲区使用 DirectByteBuf,后端业务消息的编解码模块使用 HeapByteBuf。
从内存回收角度看,ByteBuf 分 2 类:
基于对象池的 ByteBuf 和普通 ByteBuf。
两者的主要区别就是基于对象池的 ByteBuf 可以重用 ByteBuf 对象,它自己维护一个内存池,可以循环利用创建的 ByteBuf,提高内存使用效率,降低由于高负载导致的频繁 GC。测试表明使用内存池后的 Netty 在高负载、大并发的冲击下内存和 GC 更加平稳。
内存池化#
池化的简单实现思路,是基于 JVM 堆内存之上,构建更高一层内存池,通过调用内存池 allocate 方法获取内存空间,调用 release 方法将内存区域归还内存池。内存池面临的首要问题是碎片回收,内存池在频繁申请和释放空间后,还能有尽可能连续的内存空间用于大块内存空间的分配。基于这个需求,有两种算法用于优化这一块的内存分配:伙伴系统和 slab 系统。
netty4 相对于 netty3 的一大改进就是引入了内存池化技术,用以解决高速网络通信过程中,netty 造成的应用内存锯齿状消费和大量 gc 的问题。
Netty 中的零拷贝#
其实 netty 中并没有实现真正的零拷贝,netty 中的零拷贝更多的应该理解为少拷贝或者说复用(reuse),操作系统的零拷贝是避免了 CPU 将数据从一个内存区域拷贝到另一个内存区域,而 netty 中的数据操作全部是在用户态。当真正要通过 netty 将数据发送到网络时,仍然需要将数据从用户态拷贝到内核态,此时就无法做到真正的零拷贝了。
Netty 的零拷贝(或者说 ByteBuf 的复用)主要体现在以下几个方面:
- DirectByteBuf 通过直接在堆外分配内存的方式,避免了数据从堆内拷贝到堆外的过程
- 通过组合 ByteBuf 类:即 CompositeByteBuf,将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 而不需要进行数据拷贝
- 通过各种包装方法,将 byte []、ByteBuf、ByteBuffer 等包装成一个 ByteBuf 对象,而不需要进行数据的拷贝
- 通过 slice 方法,将一个 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝,这在需要进行拆包操作时非常管用
- 通过 FileRegion 包装的 FileChannel.tranferTo 方法进行文件传输时,可以直接将文件缓冲区的数据发送到目标 Channel, 减少了通过循环 write 方式导致的内存拷贝。但是这种方式是需要得到操作系统的零拷贝的支持的,如果 netty 所运行的操作系统不支持零拷贝的特性,则 netty 仍然无法做到零拷贝
Channel#
-
Channel:封装了 jdk 原生的 channel,提供统一的 API,作为其它各个功能组件的容器。
-
ChannelPipeline:责任链模式的核心组件,ChannelHandler 的容器,按顺序组织各个 ChannelHandler,并在它们之间转发事件。
-
ChannelHandlerContext:封装一个具体的 ChannelHandler,并为 ChannelHandler 的执行提供一个线程环境(ChannelHandlerInvoker)可以理解为 ChannelPipeline 链路上的一个节点,节点里面包含有指向前后节点的指针,事件在各个 ChannelHandler 之间传递,靠的就是 ChannelHandlerContext。
-
ChannelHandlerInvoker:顾名思义,是 ChannelHandler 的一个 Invoker,它存在的意义是为 ChannelHandler 提供一个运行的线程环境,默认的实现 DefaultChannelHandlerInvoker 有一个 EventExecutor 类型的成员,就是 Netty 的 EventLoop 线程,所以默认 ChannelHandler 的处理逻辑在 EventLoop 线程内。当然也可以提供不同的实现,替换默认的线程模型。
从 Netty 内部 IO 线程接读到 IO 数据,依次经过 N 个 Handler 到达最内部的逻辑处理单元,这种称之为 Inbound Handler;从 Channel 发出 IO 请求,依次经过 M 个 Handler 到达 Netty 内部 IO 线程,这种称之为 Outbound Handler。内部代码实现流程则是:Head -> Tail (Inbound),Tail -> Head (Outbound)。
网关#
网关类产品的主要功能就是消息的预处理和转发,请求和响应对象都是 “朝生夕灭” 类型的,在高并发场景下,一定要防止不合理的内存申请,具体措施如下。
(1)内存按需分配。不要一次性申请较大的内存来保存较小的消息,造成内存空间浪费,引发频繁 GC 问题。
(2)不要频繁地创建和释放对象。这会增加 GC 的负担,降低系统的吞吐量,可以采用内存池等机制优化内存的申请和释放。
(3)减少对象拷贝。对于透传类的消息,尽量把涉及业务逻辑处理的字段放入 Header,不要对 Body 做解码,直接透传到后端服务即可。这样可以大幅减少内存的申请和对象拷贝,降低内存占用,提升性能。
(4)流控机制必不可少。除了客户端并发连接数流控、QPS 流控,还需要针对内存占用等指标做流控,防止业务高峰期的 OOM。
(1)Netty 作为一个通用的 NIO 框架,不能对用户的应用场景进行假设,可以使用它做流式计算,也可以用它做 RCP 框架,不同的应用场景,传输的码流大小千差万别,无论初始化时分配的是 32KB 还是 1MB,都会随着应用场景的变化而变得不合适。因此,Netty 根据上次实际读取的码流大小对下次的接收 Buffer 缓冲区进行预测和调整,能够最大限度地满足不同行业的应用场景的需要。
(2)综合性能更高。分配容量过大会导致内存占用开销增加,后续的 Buffer 处理性能下降;容量过小需要频繁地内存扩张来接收大的请求消息,同样会导致性能下降。
(3)更节约内存。假如通常情况请求消息大小平均值为 1MB 左右,接收缓冲区大小为 1.2MB,突然某个客户发送了一个 10MB 的附件,接收缓冲区扩张为 10MB 以读取该附件,如果缓冲区不能收缩,每次缓冲区创建都会分配 10MB 的内存,但是后续所有的消息都是 1MB 左右的,这样会导致内存的浪费,如果并发客户端过多,可能会导致内存溢出并宕机。
参考#
https://www.jianshu.com/p/f7c668cd05cd
https://www.itcodemonkey.com/article/4655.html
https://blog.csdn.net/qq157538651/article/details/93537187
https://www.cnblogs.com/z-sm/p/6235157.html
https://sq.163yun.com/blog/article/213832853624152064
https://www.cnblogs.com/xys1228/p/6088805.html
http://www.52im.net/thread-99-1-1.html
https://blog.csdn.net/zjuclh/article/details/51002491
http://docs.52im.net/extend/docs/src/netty4/