共计 11064 个字符,预计需要花费 28 分钟才能阅读完成。
虚拟内存
(一)虚拟内存引入
咱们晓得计算机由 CPU、存储器、输出 / 输出设备三大外围局部组成,如下
CPU 运行速度很快,在齐全现实的状态下,存储器应该要同时具备以下三种个性:
- 速度足够快:这样 CPU 的效率才不会受限于存储器;
- 容量足够大:容量可能存储计算机所需的全副数据;
- 价格足够便宜:价格低廉,所有类型的计算机都能装备;
然而,出于老本思考,以后计算机体系中,存储都是采纳分层设计的,常见档次如下:
上图别离为寄存器、高速缓存、主存和磁盘,它们的速度逐级递加、老本逐级递加,在计算机中的容量逐级递增。通常咱们所说的物理内存即上文中的主存,常作为操作系统或其余正在运行中的程序的长期材料存储介质。在嵌入式以及一些老的操作系统中,零碎间接通过物理寻址形式和主存打交道。然而,随着科技倒退,遇到如下困境:
- 一台机器可能同时运行多台大型应用程序;
- 每个应用程序都须要在主存存储大量长期数据;
- 晚期,单个 CPU 寻址能力 2^32,导致内存最大 4G。
主存成了计算机系统的瓶颈。此时,科学家提出了一个概念:虚拟内存。
以 32 位操作系统为例,虚拟内存的引入,使得操作系统能够为每个过程调配大小为 4GB 的虚拟内存空间,而实际上物理内存在须要时才会被加载,无效解决了物理内存无限空间带来的瓶颈。在虚拟内存到物理内存转换的过程中,有很重要的一步就是进行地址翻译,上面介绍。
(二)地址翻译
过程在运行期间产生的内存地址都是虚拟地址,如果计算机没有引入虚拟内存这种存储器形象技术的话,则 CPU 会把这些地址间接发送到内存地址总线上,而后拜访和虚拟地址雷同值的物理地址;如果应用虚拟内存技术的话,CPU 则是把这些虚拟地址通过地址总线送到内存治理单元(Memory Management Unit,简称 MMU),MMU 将虚拟地址翻译成物理地址之后再通过内存总线去拜访物理内存:
虚拟地址(比方 16 位地址 8196=0010 000000000100)分为两局部:虚构页号(Virtual Page Number,简称 VPN,这里是高 4 位局部)和偏移量(Virtual Page Offset,简称 VPO,这里是低 12 位局部),虚拟地址转换成物理地址是通过页表(page table)来实现的。页表由多个页表项(Page Table Entry, 简称 PTE)组成,个别页表项中都会存储物理页框号、批改位、拜访位、爱护位和 “ 在 / 不在 ” 位(无效位)等信息。这里咱们基于一个例子来剖析当页面命中时,计算机各个硬件是如何交互的:
- 第 1 步:处理器生成一个虚拟地址 VA,通过总线发送到 MMU;
- 第 2 步:MMU 通过虚构页号失去页表项的地址 PTEA,通过内存总线从 CPU 高速缓存 / 主存读取这个页表项 PTE;
- 第 3 步:CPU 高速缓存或者主存通过内存总线向 MMU 返回页表项 PTE;
- 第 4 步:MMU 先把页表项中的物理页框号 PPN 复制到寄存器的高三位中,接着把 12 位的偏移量 VPO 复制到寄存器的末 12 位形成 15 位的物理地址,即能够把该寄存器存储的物理内存地址 PA 发送到内存总线,拜访高速缓存 / 主存;
- 第 5 步:CPU 高速缓存 / 主存返回该物理地址对应的数据给处理器。
在 MMU 进行地址转换时,如果页表项的无效位是 0,则示意该页面并没有映射到实在的物理页框号 PPN,则会引发一个缺页中断,CPU 陷入操作系统内核,接着操作系统就会通过页面置换算法抉择一个页面将其换出 (swap),以便为行将调入的新页面腾出地位,如果要换出的页面的页表项里的批改位曾经被设置过,也就是被更新过,则这是一个脏页 (Dirty Page),须要写回磁盘更新该页面在磁盘上的正本,如果该页面是 ” 洁净 ” 的,也就是没有被批改过,则间接用调入的新页面笼罩掉被换出的旧页面即可。缺页中断的具体流程如下:
- 第 1 步到第 3 步:和后面的页面命中的前 3 步是统一的;
- 第 4 步:查看返回的页表项 PTE 发现其无效位是 0,则 MMU 触发一次缺页中断异样,而后 CPU 转入到操作系统内核中的缺页中断处理器;
- 第 5 步:缺页中断处理程序查看所需的虚拟地址是否非法,确认非法后零碎则查看是否有闲暇物理页框号 PPN 能够映射给该缺失的虚构页面,如果没有闲暇页框,则执行页面置换算法寻找一个现有的虚构页面淘汰,如果该页面曾经被批改过,则写回磁盘,更新该页面在磁盘上的正本;
- 第 6 步:缺页中断处理程序从磁盘调入新的页面到内存,更新页表项 PTE;
- 第 7 步:缺页中断程序返回到原先的过程,从新执行引起缺页中断的指令,CPU 将引起缺页中断的虚拟地址从新发送给 MMU,此时该虚拟地址曾经有了映射的物理页框号 PPN,因而会依照后面『Page Hit』的流程走一遍,最初主存把申请的数据返回给处理器。
高速缓存
后面在剖析虚拟内存的工作原理之时,谈到页表的存储地位,为了简化解决,都是默认把主存和高速缓存放在一起,而实际上更具体的流程应该是如下的原理图:
如果一台计算机同时装备了虚拟内存技术和 CPU 高速缓存,那么 MMU 每次都会优先尝试到高速缓存中进行寻址,如果缓存命中则会间接返回,只有当缓存不命中之后才去主存寻址。通常来说,大多数零碎都会抉择利用物理内存地址去拜访高速缓存,因为高速缓存相比于主存要小得多,所以应用物理寻址也不会太简单;另外也因为高速缓存容量很小,所以零碎须要尽量在多个过程之间共享数据块,而应用物理地址可能使得多过程同时在高速缓存中存储数据块以及共享来自雷同虚拟内存页的数据块变得更加直观。
减速翻译 & 优化页表
虚拟内存这项技术能不能真正地广泛应用到计算机中,还须要解决如下两个问题:
- 虚拟地址到物理地址的映射过程必须要十分快,地址翻译如何减速;
- 虚拟地址范畴的增大必然会导致页表的收缩,造成大页表。
“计算机科学畛域的任何问题都能够通过减少一个间接的中间层来解决”。尽管虚拟内存自身就曾经是一个中间层了,然而中间层里的问题同样能够通过再引入一个中间层来解决。减速地址翻译过程的计划目前是通过引入页表缓存模块 — TLB,而大页表则是通过实现多级页表或倒排页表来解决。
TLB 减速
翻译后备缓冲器(Translation Lookaside Buffer,TLB),也叫快表,是用来减速虚拟地址翻译的,因为虚拟内存的分页机制,页表个别是保留在内存中的一块固定的存储区,而 MMU 每次翻译虚拟地址的时候都须要从页表中匹配一个对应的 PTE,导致过程通过 MMU 拜访指定内存数据的时候比没有分页机制的零碎多了一次内存拜访,个别会多消耗几十到几百个 CPU 时钟周期,性能至多降落一半,如果 PTE 碰巧缓存在 CPU L1 高速缓存中,则开销能够升高到一两个周期,然而咱们不能寄希望于每次要匹配的 PTE 都刚好在 L1 中,因而须要引入减速机制,即 TLB 快表。TLB 能够简略地了解成页表的高速缓存,保留了最高频被拜访的页表项 PTE。因为 TLB 个别是硬件实现的,因而速度极快,MMU 收到虚拟地址时个别会先通过硬件 TLB 并行地在页表中匹配对应的 PTE,若命中且该 PTE 的拜访操作不违反爱护位(比方尝试写一个只读的内存地址),则间接从 TLB 取出对应的物理页框号 PPN 返回,若不命中则会穿透到主存页表里查问,并且会在查问到最新页表项之后存入 TLB,以备下次缓存命中,如果 TLB 以后的存储空间有余则会替换掉现有的其中一个 PTE。上面来具体分析一下 TLB 命中和不命中。
- TLB 命中:
第 1 步:CPU 产生一个虚拟地址 VA;
第 2 步和第 3 步:MMU 从 TLB 中取出对应的 PTE;
第 4 步:MMU 将这个虚拟地址 VA 翻译成一个实在的物理地址 PA,通过地址总线发送到高速缓存 / 主存中去;
第 5 步:高速缓存 / 主存将物理地址 PA 上的数据返回给 CPU。
- TLB 不命中:
第 1 步:CPU 产生一个虚拟地址 VA;
第 2 步至第 4 步:查问 TLB 失败,走失常的主存页表查问流程拿到 PTE,而后把它放入 TLB 缓存,以备下次查问,如果 TLB 此时的存储空间有余,则这个操作会汰换掉 TLB 中另一个已存在的 PTE;
第 5 步:MMU 将这个虚拟地址 VA 翻译成一个实在的物理地址 PA,通过地址总线发送到高速缓存 / 主存中去;
第 6 步:高速缓存 / 主存将物理地址 PA 上的数据返回给 CPU。
多级页表
TLB 的引入能够肯定水平上解决虚拟地址到物理地址翻译的开销问题,接下来还须要解决另一个问题:大页表。实践上一台 32 位的计算机的寻址空间是 4GB,也就是说每一个运行在该计算机上的过程实践上的虚构寻址范畴是 4GB。到目前为止,咱们始终在探讨的都是单页表的情景,如果每一个过程都把实践上可用的内存页都装载进一个页表里,然而实际上过程会真正应用到的内存其实可能只有很小的一部分,而咱们也晓得页表也是保留在计算机主存中的,那么势必会造成大量的内存节约,甚至有可能导致计算机物理内存不足从而无奈并行地运行更多过程。这个问题个别通过多级页表(Multi-Level Page Tables)来解决,通过把一个大页表进行拆分,造成多级的页表,咱们具体来看一个二级页表应该如何设计:假设一个虚拟地址是 32 位,由 10 位的一级页表索引、10 位的二级页表索引以及 12 位的地址偏移量,则 PTE 是 4 字节,页面 page 大小是 212 = 4KB,总共须要 220 个 PTE,一级页表中的每个 PTE 负责映射虚拟地址空间中的一个 4MB 的 chunk,每一个 chunk 都由 1024 个间断的页面 Page 组成,如果寻址空间是 4GB,那么一共只须要 1024 个 PTE 就足够笼罩整个过程地址空间。二级页表中的每一个 PTE 都负责映射到一个 4KB 的虚拟内存页面,和单页表的原理是一样的。多级页表的关键在于,咱们并不需要为一级页表中的每一个 PTE 都调配一个二级页表,而只须要为过程以后应用到的地址做相应的调配和映射。因而,对于大部分过程来说,它们的一级页表中有大量空置的 PTE,那么这部分 PTE 对应的二级页表也将无需存在,这是一个相当可观的内存节约,事实上对于一个典型的程序来说,实践上的 4GB 可用虚拟内存地址空间绝大部分都会处于这样一种未调配的状态;更进一步,在程序运行过程中,只须要把一级页表放在主存中,虚拟内存零碎能够在理论须要的时候才去创立、调入和调出二级页表,这样就能够确保只有那些最频繁被应用的二级页表才会常驻在主存中,此举亦极大地缓解了主存的压力。
内核空间 & 用户空间
对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2 的 32 次方)。也就是说一个过程的最大地址空间为 4G。操作系统的外围是内核 (kernel),它独立于一般的应用程序,能够拜访受爱护的内存空间,也有拜访底层硬件设施的所有权限。为了保障内核的平安,当初的操作系统个别都强制用户过程不能间接操作内核。具体的实现形式根本都是由操作系统将虚拟地址空间划分为两局部,一部分为内核空间,另一部分为用户空间。针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF) 由内核应用,称为内核空间。而较低的 3G 字节 (从虚拟地址 0x00000000 到 0xBFFFFFFF) 由各个过程应用,称为用户空间。
为什么须要辨别内核空间与用户空间?
在 CPU 的所有指令中,有些指令是十分危险的,如果错用,将导致系统解体,比方清内存、设置时钟等。如果容许所有的程序都能够应用这些指令,那么零碎解体的概率将大大增加。所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只容许操作系统及其相干模块应用,一般应用程序只能应用那些不会造成劫难的指令。辨别内核空间和用户空间实质上是要进步操作系统的稳定性及可用性。
(一)内核态与用户态
当过程运行在内核空间时就处于内核态,而过程运行在用户空间时则处于用户态。在内核态下,过程运行在内核地址空间中,此时 CPU 能够执行任何指令。运行的代码也不受任何的限度,能够自在地拜访任何无效地址,也能够间接进行端口的拜访。在用户态下,过程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多查看,它们只能拜访映射其地址空间的页表项中规定的在用户态下可拜访页面的虚拟地址,且只能对工作状态段 (TSS) 中 I/O 许可位图 (I/O Permission Bitmap) 中规定的可拜访端口进行间接拜访。
对于以前的 DOS 操作系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。能够认为所有的代码都是运行在内核态的,因此用户编写的利用程序代码能够很容易的让操作系统解体掉。对于 Linux 来说,通过辨别内核空间和用户空间的设计,隔离了操作系统代码 (操作系统的代码要比应用程序的代码强壮很多) 与利用程序代码。即使是单个应用程序呈现谬误也不会影响到操作系统的稳定性,这样其它的程序还能够失常的运行。
如何从用户空间进入内核空间?
其实所有的零碎资源管理都是在内核空间中实现的。比方读写磁盘文件,调配回收内存,从网络接口读写数据等等。咱们的应用程序是无奈间接进行这样的操作的。然而咱们能够通过内核提供的接口来实现这样的工作。比方应用程序要读取磁盘上的一个文件,它能够向内核发动一个“零碎调用”通知内核:“我要读取磁盘上的某某文件”。其实就是通过一个非凡的指令让过程从用户态进入到内核态 (到了内核空间),在内核空间中,CPU 能够执行任何的指令,当然也包含从磁盘上读取数据。具体过程是先把数据读取到内核空间中,而后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序曾经从零碎调用中返回并且拿到了想要的数据,能够开开心心的往下执行了。简略说就是应用程序把高科技的事件(从磁盘读取文件) 外包给了零碎内核,零碎内核做这些事件既业余又高效。
IO
在进行 IO 操作时,通常须要通过如下两个阶段
- 数据筹备阶段:数据从硬件到内核空间
- 数据拷贝阶段:数据从内核空间到用户空间
通常咱们所说的 IO 的阻塞 / 非阻塞以及同步 / 异步,和这两个阶段关系密切:
- 阻塞 IO 和非阻塞 IO 断定规范:数据筹备阶段,应用程序是否阻塞期待操作系统将数据从硬件加载到内核空间;
- 同步 IO 和异步 IO 断定规范:数据拷贝阶段,数据是否备好后间接告诉应用程序应用,无需期待拷贝;
(一)(同步)阻塞 IO
阻塞 IO:当用户产生了零碎调用后,如果数据未从网卡达到内核态,内核态数据未筹备好,此时会始终阻塞。直到数据就绪,而后从内核态拷贝到用户态再返回。
阻塞 IO 每个连贯一个独自的线程进行解决,通常搭配多线程来应答大流量,然而,开拓线程的开销比拟大,一个程序能够开拓的线程是无限的,面对百万连贯的状况,是无奈解决。非阻塞 IO 能够肯定水平上解决上述问题。
(二)(同步)非阻塞 IO
非阻塞 IO:在第一阶段 (网卡 - 内核态) 数据未达到时不期待,而后间接返回。数据就绪后,从内核态拷贝到用户态再返回。
非阻塞 IO 解决了阻塞 IO 每个连贯一个线程解决的问题,所以其最大的长处就是 一个线程能够解决多个连贯。然而,非阻塞 IO 须要用户屡次发动零碎调用。频繁的零碎调用是比拟耗费系统资源的。
(三)IO 多路复用
为了解决非阻塞 IO 存在的频繁的零碎调用这个问题,随着内核的倒退,呈现了 IO 多路复用模型。
IO 多路复用:通过一种机制一个过程能同时期待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,就能够返回。
IO 多路复用实质上复用了零碎调用,使多个文件的状态能够复用一个零碎调用获取,无效缩小了零碎调用。select、poll、epoll 均是基于 IO 多路复用思维实现的。
select 和 poll 的工作原理比拟类似,通过 select()或者 poll()将多个 socket fds 批量通过零碎调用传递给内核,由内核进行循环遍历判断哪些 fd 上数据就绪了,而后将就绪的 readyfds 返回给用户。再由用户进行挨个遍历就绪好的 fd,读取或者写入数据。所以通过 IO 多路复用 + 非阻塞 IO,一方面升高了零碎调用次数,另一方面能够用极少的线程来解决多个网络连接。select 和 poll 的最大区别是:select 默认能解决的最大连贯是 1024 个,能够通过批改配置来扭转,但究竟是无限个;而 poll 实践上能够反对有限个。而 select 和 poll 则面临类似的问题在治理海量的连贯时,会频繁地从用户态拷贝到内核态,比拟耗费资源。
epoll 无效躲避了将 fd 频繁的从用户态拷贝到内核态,通过应用红黑树 (RB-tree) 搜寻被监督的文件描述符(file descriptor)。在 epoll 实例上注册事件时,epoll 会将该事件增加到 epoll 实例的红黑树上并注册一个回调函数,当事件产生时会将事件增加到就绪链表中。
- epoll 数据结构 + 算法
epoll 的外围数据结构是:1 个 红黑树 和 1 个 双向链表,还有 3 个外围 API。
- 监督 socket 索引 - 红黑树
为什么采纳红黑树呢?因为和 epoll 的工作机制无关。epoll 在增加一个 socket 或者删除一个 socket 或者批改一个 socket 的时候,它须要查问速度更快,操作效率最高,因而须要一个更加优良的数据结构可能治理这些 socket。咱们想到的比方链表,数组,二叉搜寻树,B+ 树等都无奈满足要求。
- 因为链表在查问,删除的时候毫无疑问工夫复杂度是 O(n);
- 数组查问很快,然而删除和新增工夫复杂度是 O(n);
- 二叉搜寻树尽管查问效率是 lgn,然而如果不是均衡的,那么就会进化为线性查找,复杂度间接来到 O(n);
- B+ 树是均衡多路查找树,次要是通过升高树的高度来存储上亿级别的数据,然而它的利用场景是内存放不下的时候可能用起码的 IO 拜访次数从磁盘获取数据。比方数据库聚簇索引,成千盈百万的数据内存无奈满足查找就须要到内存查找,而因为 B + 树层高很低,只须要几次磁盘 IO 就能获取数据到内存,所以在这种磁盘到内存拜访上 B + 树更适宜。
因为咱们解决上万级的 fd,它们自身的存储空间并不会很大,所以偏向于在内存中去实现治理,而红黑树是一种十分优良的均衡树,它齐全是在内存中操作,而且查找,删除和新增工夫复杂度都是 lgn,效率十分高,因而抉择用红黑树实现 epoll 是最佳的抉择。当然不抉择用 AVL 树是因为红黑树是不合乎 AVL 树的平衡条件的,红黑是用非严格的均衡来换取增删节点时候旋转次数的升高,任何不均衡都会在三次旋转之内解决;而 AVL 树是严格均衡树,在减少或者删除节点的时候,依据不同状况,旋转的次数比红黑树要多。所以红黑树的插入效率更高。
- 就绪 socket 列表 - 双向链表
就绪列表存储的是就绪的 socket,所以它应可能疾速的插入数据。程序可能随时调用 epoll\_ctl 增加监督 socket,也可能随时删除。当删除时,若该 socket 曾经寄存在就绪列表中,它也应该被移除。(事实上,每个 epoll\_item 既是红黑树节点,也是链表节点,删除红黑树节点,天然删除了链表节点)所以就绪列表应是一种可能疾速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll 应用 双向链表来实现就绪队列(rdllist)
- 三个 API
- int epoll\_create(int size)
性能:内核会产生一个 epoll 实例数据结构并返回一个文件描述符 epfd,这个非凡的描述符就是 epoll 实例的句柄,前面的两个接口都以它为核心。同时也会创立红黑树和就绪列表,红黑树来治理注册 fd,就绪列表来收集所有就绪 fd。size 参数示意所要监督文件描述符的最大值,不过在起初的 Linux 版本中曾经被弃用(同时,size 不要传 0,会报 invalid argument 谬误)
- int epoll\_ctl(int epfd,int op,int fd,struct epoll\_event *event)
性能:将被监听的 socket 文件描述符增加到红黑树或从红黑树中删除或者对监听事件进行批改;同时向内核中断处理程序注册一个回调函数,内核在检测到某文件描述符可读 / 可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。
- int epoll\_wait(int epfd,struct epoll\_event *events,int maxevents,int timeout);
性能:阻塞期待注册的事件产生,返回事件的数目,并将触发的事件写入 events 数组中。events: 用来记录被触发的 events,其大小应该和 maxevents 统一 maxevents: 返回的 events 的最大个数处于 ready 状态的那些文件描述符会被复制进 ready list 中,epoll\_wait 用于向用户过程返回 ready list(就绪列表)。events 和 maxevents 两个参数形容一个由用户调配的 struct epoll event 数组,调用返回时,内核将就绪列表 (双向链表) 复制到这个数组中,并将理论复制的个数作为返回值。留神,如果就绪列表比 maxevents 长,则只能复制前 maxevents 个成员;反之,则可能齐全复制就绪列表。另外,struct epoll event 构造中的 events 域在这里的解释是:在被监测的文件描述符上理论产生的事件。
- 工作模式
epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)。
- LT 模式
LT(level triggered)是缺省的工作形式,并且同时反对 block 和 no-block socket. 在这种做法中,内核通知你一个文件描述符是否就绪了,而后你能够对这个就绪的 fd 进行 IO 操作。如果你不做任何操作,内核还是会持续告诉你。
- ET 模式
ET(edge-triggered)是高速工作形式,只反对 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 通知你。而后它会假如你晓得文件描述符曾经就绪,并且不会再为那个文件描述符发送更多的就绪告诉,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比方,你在发送,接管或者接管申请,或者发送接管的数据少于一定量时导致了一个 EWOULDBLOCK 谬误)。留神,如果始终不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的告诉(only once) ET 模式在很大水平上缩小了 epoll 事件被反复触发的次数,因而效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须应用非阻塞套接口,以防止因为一个文件句柄的阻塞读 / 阻塞写操作把解决多个文件描述符的工作饿死。
(四)网络 IO 模型
理论的网络模型常联合 I / O 复用和线程池实现,如 Reactor 模式:
- 单 reactor 单线程模型
此种模型通常只有一个 epoll 对象,所有的接管客户端连贯、客户端读取、客户端写入操作都蕴含在一个线程内。
- 长处:模型简略,没有多线程、过程通信、竞争的问题,全副都在一个线程中实现
- 毛病:单线程无奈齐全施展多核 CPU 的性能;I/O 操作和非 I/O 的业务操作在一个 Reactor 线程实现,这可能会大大提早 I/O 申请的响应;线程意外终止,或者进入死循环,会导致整个零碎通信模块不可用,不能接管和解决内部音讯,造成节点故障;
- 应用场景:客户端的数量无限,业务解决十分疾速,比方 Redis 在业务解决的工夫复杂度 O(1) 的状况
- 单 reactor 多线程模型
该模型将读写的业务逻辑交给具体的线程池来解决
- 长处:充分利用多核 cpu 的解决能力,晋升 I / O 响应速度;
- 毛病:在该模式中,尽管非 I/O 操作交给了线程池来解决,然而所有的 I/O 操作仍然由 Reactor 单线程执行,在高负载、高并发或大数据量的利用场景,仍然容易成为瓶颈。
- multi-reactor 多线程模型
在这种模型中,次要分为两个局部:mainReactor、subReactors。mainReactor 次要负责接管客户端的连贯,而后将建设的客户端连贯通过负载平衡的形式分发给 subReactors,subReactors 来负责具体的每个连贯的读写 对于非 IO 的操作,仍然交给工作线程池去做。
- 长处:父线程与子线程的数据交互简略职责明确,父线程只须要接管新连贯,子线程实现后续的业务解决。Reactor 主线程只须要把新连贯传给子线程,子线程无需返回数据。
- 毛病:编程复杂度较高。
支流的中间件所采纳的网络模型
(五)异步 IO
后面介绍的所有网络 IO 都是同步 IO,因为当数据在内核态就绪时,在内核态拷贝用户态的过程中,依然会有短暂工夫的阻塞期待。而异步 IO 指:内核态拷贝数据到用户态这种形式也是交给零碎线程来实现,不禁用户线程实现,如 windows 的 IOCP,Linux 的 AIO。
零拷贝
(一)传统 IO 流程
传统 IO 流程会通过如下两个过程:
- 数据筹备阶段:数据从硬件到内核空间
- 数据拷贝阶段:数据从内核空间到用户空间
零拷贝:指数据无需从硬件到内核空间或从内核空间到用户空间。上面介绍常见的零拷贝实现
(二)mmap + write
mmap 将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程,整个拷贝过程会产生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。
(三)sendfile
通过 sendfile 零碎调用,数据能够间接在内核空间外部进行 I /O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝,sendfile 调用中 I /O 数据对用户空间是齐全不可见的,整个拷贝过程会产生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。
(四)Sendfile + DMA gather copy
Linux2.4 引入,将内核空间的读缓冲区(read buffer)中对应的数据形容信息(内存地址、地址偏移量)记录到相应的网络缓冲区(socketbuffer)中,由 DMA 依据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设施中,这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作,产生 2 次上下文切换、0 次 CPU 拷贝以及 2 次 DMA 拷贝;
(五)splice
Linux2.6.17 版本引入,在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建设管道(pipeline),从而防止了两者之间的 CPU 拷贝操作,2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝。
(六)写时复制
通过尽量提早产生公有对象中的正本,写时复制最充沛地利用了罕见的物理资源。
(七)Java 中零拷贝
MappedByteBuffer:基于内存映射(mmap)这种零拷贝形式的提供的一种实现。
FileChannel 基于 sendfile 定义了 transferFrom() 和 transferTo() 两个形象办法,它通过在通道和通道之间建设连贯实现数据传输的。
参考
https://mp.weixin.qq.com/s/c81Fvws0J2tHjcdTgxvv6g
https://blog.csdn.net/Chasing\_\_Dreams/article/details/106297351
https://mp.weixin.qq.com/s/EDzFOo3gcivOe\_RgipkTkQ
https://mp.weixin.qq.com/s/G6TfGbc4U8Zhv30wnN0HIg
https://www.modb.pro/db/189656
https://mp.weixin.qq.com/s/r9RU4RoE-qrzXPAwiej5sw