10分钟!读懂虚拟内存 & I/O & 零拷贝

10分钟!读懂虚拟内存 & I/O & 零拷贝

10分钟!读懂虚拟内存 & I/O & 零拷贝
虚拟内存

(一)虚拟内存引进

咱们知道计算机由CPU、存储器、输入/输出设备三大中心部分组成,如下

10分钟!读懂虚拟内存 & I/O & 零拷贝

CPU运转速度很快,在彻底理想的状况下,存储器应该要一同具有以下三种特性:

  • 速度满意快:这样 CPU 的功率才不会受限于存储器;

  • 容量满意大:容量可以存储计算机所需的悉数数据;

  • 价格满意廉价:价格低廉,一切类型的计算机都能装备;

可是,出于成本考虑,当时计算机体系中,存储都是选用分层规划的,常见层次如下:

10分钟!读懂虚拟内存 & I/O & 零拷贝

上图分别为寄存器、高速缓存、主存和磁盘,它们的速度逐级递减、成本逐级递减,在计算机中的容量逐级递增。一般咱们所说的物理内存即上文中的主存,常作为操作体系或其他正在运转中的程序的暂时材料存储介质。在嵌入式以及一些老的操作体系中,体系直接经过物理寻址办法和主存打交道。可是,跟着科技开展,遇到如下窘境:

  • 一台机器或许一同运转多台大型运用程序;

  • 每个运用程序都需求在主存存储许多暂时数据;

  • 前期,单个CPU寻址才能2^32,导致内存最大4G。

主存成了计算机体系的瓶颈。此刻,科学家提出了一个概念:虚拟内存。

10分钟!读懂虚拟内存 & I/O & 零拷贝

以32位操作体系为例,虚拟内存的引进,使得操作体系可以为每个进程分配大小为 4GB的虚拟内存空间,而实践上物理内存在需求时才会被加载,有用处理了物理内存有限空间带来的瓶颈。在虚拟内存到物理内存转化的进程中,有很重要的一步便是进行地址翻译,下面介绍。

(二)地址翻译

进程在运转期间发生的内存地址都是虚拟地址,假如计算机没有引进虚拟内存这种存储器抽象技能的话,则 CPU 会把这些地址直接发送到内存地址总线上,然后拜访和虚拟地址相同值的物理地址;假如运用虚拟内存技能的话,CPU 则是把这些虚拟地址经过地址总线送到内存办理单元(Memory Management Unit,简称 MMU),MMU 将虚拟地址翻译成物理地址之后再经过内存总线去拜访物理内存:

10分钟!读懂虚拟内存 & I/O & 零拷贝

虚拟地址(比方 16 位地址 8196=0010 000000000100)分为两部分:虚拟页号(Virtual Page Number,简称 VPN,这儿是高 4 位部分)和偏移量(Virtual Page Offset,简称 VPO,这儿是低 12 位部分),虚拟地址转化成物理地址是经过页表(page table)来完结的。页表由多个页表项(Page Table Entry, 简称 PTE)组成,一般页表项中都会存储物理页框号、修正位、拜访位、保护位和 “在/不在” 位(有用位)等信息。这儿咱们依据一个例子来剖析当页面射中时,计算机各个硬件是怎么交互的:

10分钟!读懂虚拟内存 & I/O & 零拷贝

  • 第 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),需求写回磁盘更新该页面在磁盘上的副本,假如该页面是”干净”的,也便是没有被修正过,则直接用调入的新页面覆盖掉被换出的旧页面即可。缺页中止的详细流程如下:

10分钟!读懂虚拟内存 & I/O & 零拷贝

  • 第 1 步到第 3 步:和前面的页面射中的前 3 步是共同的;

  • 第 4 步:查看回来的页表项 PTE 发现其有用位是 0,则 MMU 触发一次缺页中止反常,然后 CPU 转入到操作体系内核中的缺页中止处理器;

  • 第 5 步:缺页中止处理程序查看所需的虚拟地址是否合法,承认合法后体系则查看是否有闲暇物理页框号 PPN 可以映射给该缺失的虚拟页面,假如没有闲暇页框,则履行页面置换算法寻觅一个现有的虚拟页面淘汰,假如该页面现已被修正过,则写回磁盘,更新该页面在磁盘上的副本;

  • 第 6 步:缺页中止处理程序从磁盘调入新的页面到内存,更新页表项 PTE;

  • 第 7 步:缺页中止程序回来到原先的进程,从头履行引起缺页中止的指令,CPU 将引起缺页中止的虚拟地址从头发送给 MMU,此刻该虚拟地址现已有了映射的物理页框号 PPN,因而会按照前面『Page Hit』的流程走一遍,最后主存把恳求的数据回来给处理器。

  • 高速缓存

前面在剖析虚拟内存的作业原理之时,谈到页表的存储位置,为了简化处理,都是默许把主存和高速缓存放在一同,而实践上更详细的流程应该是如下的原理图:

10分钟!读懂虚拟内存 & I/O & 零拷贝

假如一台计算机一同装备了虚拟内存技能和 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 射中:

10分钟!读懂虚拟内存 & I/O & 零拷贝

第 1 步:CPU 发生一个虚拟地址 VA;

第 2 步和第 3 步:MMU 从 TLB 中取出对应的 PTE;

第 4 步:MMU 将这个虚拟地址 VA 翻译成一个实在的物理地址 PA,经过地址总线发送到高速缓存/主存中去;

第 5 步:高速缓存/主存将物理地址 PA 上的数据回来给 CPU。

  • TLB 不射中:

10分钟!读懂虚拟内存 & I/O & 零拷贝

第 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 可用虚拟内存地址空间绝大部分都会处于这样一种未分配的状况;更进一步,在程序运转进程中,只需求把一级页表放在主存中,虚拟内存体系可以在实践需求的时分才去创立、调入和调出二级页表,这样就可以保证只要那些最频频被运用的二级页表才会常驻在主存中,此举亦极大地缓解了主存的压力。

10分钟!读懂虚拟内存 & I/O & 零拷贝

10分钟!读懂虚拟内存 & I/O & 零拷贝
内核空间 & 用户空间

对 32 位操作体系而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也便是说一个进程的最大地址空间为 4G。操作体系的中心是内核(kernel),它独立于普通的运用程序,可以拜访受保护的内存空间,也有拜访底层硬件设备的一切权限。为了保证内核的安全,现在的操作体系一般都强制用户进程不能直接操作内核。详细的完结办法基本都是由操作体系将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。针对 Linux 操作体系而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核运用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程运用,称为用户空间。

10分钟!读懂虚拟内存 & I/O & 零拷贝

为什么需求区别内核空间与用户空间?

在 CPU 的一切指令中,有些指令是十分风险的,假如错用,将导致体系溃散,比方清内存、设置时钟等。假如允许一切的程序都可以运用这些指令,那么体系溃散的概率将大大增加。所以,CPU 将指令分为特权指令和非特权指令,关于那些风险的指令,只允许操作体系及其相关模块运用,普通运用程序只能运用那些不会形成灾祸的指令。区别内核空间和用户空间本质上是要进步操作体系的稳定性及可用性。

(一)内核态与用户态

当进程运转在内核空间时就处于内核态,而进程运转在用户空间时则处于用户态。在内核态下,进程运转在内核地址空间中,此刻 CPU 可以履行任何指令。运转的代码也不受任何的限制,可以自由地拜访任何有用地址,也可以直接进行端口的拜访。在用户态下,进程运转在用户地址空间中,被履行的代码要遭到 CPU 的诸多查看,它们只能拜访映射其地址空间的页表项中规则的在用户态下可拜访页面的虚拟地址,且只能对任务状况段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规则的可拜访端口进行直接拜访。

关于以前的 DOS 操作体系来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为一切的代码都是运转在内核态的,因而用户编写的运用程序代码可以很简单的让操作体系溃散掉。关于 Linux 来说,经过区别内核空间和用户空间的规划,隔离了操作体系代码(操作体系的代码要比运用程序的代码健壮许多)与运用程序代码。即便是单个运用程序呈现过错也不会影响到操作体系的稳定性,这样其它的程序还可以正常的运转。

怎么从用户空间进入内核空间?

其实一切的体系资源办理都是在内核空间中完结的。比方读写磁盘文件,分配回收内存,从网络接口读写数据等等。咱们的运用程序是无法直接进行这样的操作的。可是咱们可以经过内核供给的接口来完结这样的任务。比方运用程序要读取磁盘上的一个文件,它可以向内核建议一个 “体系调用” 告知内核:“我要读取磁盘上的某某文件”。其实便是经过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以履行任何的指令,当然也包括从磁盘上读取数据。详细进程是先把数据读取到内核空间中,然后再把数据仿制到用户空间并从内核态切换到用户态。此刻运用程序现已从体系调用中回来并且拿到了想要的数据,可以开开心心的往下履行了。简单说便是运用程序把高科技的事情(从磁盘读取文件)外包给了体系内核,体系内核做这些事情既专业又高效。

10分钟!读懂虚拟内存 & I/O & 零拷贝
IO

在进行IO操作时,一般需求经过如下两个阶段

  • 数据预备阶段:数据从硬件到内核空间

  • 数据仿制阶段:数据从内核空间到用户空间

10分钟!读懂虚拟内存 & I/O & 零拷贝

一般咱们所说的IO的堵塞/非堵塞以及同步/异步,和这两个阶段关系密切:

  • 堵塞IO和非堵塞 IO 断定规范:数据预备阶段,运用程序是否堵塞等待操作体系将数据从硬件加载到内核空间;

  • 同步 IO和异步 IO 断定规范:数据仿制阶段,数据是否备好后直接告诉运用程序运用,无需等待仿制;

(一)(同步)堵塞IO

堵塞IO :当用户发生了体系调用后,假如数据未从网卡抵达内核态,内核态数据未预备好,此刻会一向堵塞。直到数据安排妥当,然后从内核态仿制到用户态再回来。

10分钟!读懂虚拟内存 & I/O & 零拷贝

堵塞 IO 每个衔接一个单独的线程进行处理,一般调配多线程来应对大流量,可是,拓荒线程的开支比较大,一个程序可以拓荒的线程是有限的,面对百万衔接的状况,是无法处理。非堵塞IO可以一定程度上处理上述问题。

(二)(同步)非堵塞IO

非堵塞IO :在第一阶段(网卡-内核态)数据未抵达时不等待,然后直接回来。数据安排妥当后,从内核态仿制到用户态再回来。

10分钟!读懂虚拟内存 & I/O & 零拷贝

非堵塞 IO 处理了堵塞 IO每个衔接一个线程处理的问题,所以其最大的长处便是一个线程可以处理多个衔接。可是,非堵塞IO需求用户多次建议体系调用。频频的体系调用是比较耗费体系资源的。

(三)IO多路复用

为了处理非堵塞 IO 存在的频频的体系调用这个问题,跟着内核的开展,呈现了 IO 多路复用模型。

IO多路复用:经过一种机制一个进程能一同等待多个文件描绘符,而这些文件描绘符(套接字描绘符)其中的任意一个进入读安排妥当状况,就可以回来。

10分钟!读懂虚拟内存 & I/O & 零拷贝

IO多路复用本质上复用了体系调用,使多个文件的状况可以复用一个体系调用获取,有用减少了体系调用。select、poll、epoll均是依据IO多路复用思维完结的。


select poll epoll
函数 int select(int nfds, fd_setrestrict readfds, fd_setrestrict writefds, fd_setrestrict exceptfds, struct timevalrestrict timeout); int poll(struct pollfd *fds, nfds_t nfds, int timeout);
10分钟!读懂虚拟内存 & I/O & 零拷贝
10分钟!读懂虚拟内存 & I/O & 零拷贝
事情调集 经过3个参数分别传入感兴趣的可读、可写、反常事情,内核经过对这些参数的在线修正回来安排妥当事情 统一处理一切事情类型,用户经过pollfd中的events传入感兴趣的事情,内核经过修正pollfd中的revents反应安排妥当事情 内核经过事情表办理一切感兴趣的事情,经过调用epoll_wait获取安排妥当事情,epoll_wait的events仅回来安排妥当事情
内核完结&作业功率 选用轮询办法检测安排妥当事情,时刻复杂度o(n) 选用轮询办法检测安排妥当事情,时刻复杂度o(n) 回调回来安排妥当事情,时刻复杂度o(1)
最大衔接数 1024 无上限 无上限
作业形式 LT水平触发 LT水平触发 LT&ET
fd仿制 每次调用,每次仿制 每次调用,每次仿制 经过mmap技能,降低fd仿制的开支
官网 https://man7.org/linux/man-pages/man2/select.2.html https://man7.org/linux/man-pages/man2/poll.2.html https://man7.org/linux/man-pages/man7/epoll.7.html

10分钟!读懂虚拟内存 & I/O & 零拷贝

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 。

10分钟!读懂虚拟内存 & I/O & 零拷贝

  • 监督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形式:

10分钟!读懂虚拟内存 & I/O & 零拷贝

  • 单 reactor 单线程模型

此种模型一般只要一个 epoll 目标,一切的接纳客户端衔接、客户端读取、客户端写入操作都包含在一个线程内。

10分钟!读懂虚拟内存 & I/O & 零拷贝

  • 长处:模型简单,没有多线程、进程通讯、竞赛的问题,悉数都在一个线程中完结

  • 缺陷:单线程无法彻底发挥多核 CPU 的功能;I/O 操作和非 I/O 的事务操作在一个Reactor线程完结,这或许会大大延迟 I/O 恳求的响应;线程意外终止,或许进入死循环,会导致整个体系通讯模块不可用,不能接纳和处理外部音讯,形成节点毛病;

  • 运用场景:客户端的数量有限,事务处理十分快速,比方 Redis在事务处理的时刻复杂度 O(1) 的状况

  • 单 reactor 多线程模型

该模型将读写的事务逻辑交给详细的线程池来处理

10分钟!读懂虚拟内存 & I/O & 零拷贝

  • 长处:充分利用多核cpu 的处理才能,提高I/O响应速度;

  • 缺陷:在该形式中,尽管非 I/O 操作交给了线程池来处理,可是一切的 I/O 操作仍然由 Reactor 单线程履行,在高负载、高并发或大数据量的运用场景,仍然简单成为瓶颈。

  • multi-reactor 多线程模型

在这种模型中,首要分为两个部分:mainReactor、subReactors。mainReactor 首要担任接纳客户端的衔接,然后将树立的客户端衔接经过负载均衡的办法分发给 subReactors,subReactors 来担任详细的每个衔接的读写 关于非 IO 的操作,仍然交给作业线程池去做。

10分钟!读懂虚拟内存 & I/O & 零拷贝

  • 长处:父线程与子线程的数据交互简单责任明确,父线程只需求接纳新衔接,子线程完结后续的事务处理。Reactor 主线程只需求把新衔接传给子线程,子线程无需回来数据。

  • 缺陷:编程复杂度较高。

  • 主流的中间件所选用的网络模型

框架 epll触发办法 reactor形式
redis 水平触发 单reactor
memcached 水平触发 多线程多reactor
kafka 水平触发 多线程多reactor
nginx 边缘触发 多线程多reactor

(五)异步 IO

10分钟!读懂虚拟内存 & I/O & 零拷贝

前面介绍的一切网络 IO 都是同步 IO,由于当数据在内核态安排妥当时,在内核态仿制用户态的进程中,仍然会有时间短时刻的堵塞等待。而异步 IO 指:内核态仿制数据到用户态这种办法也是交给体系线程来完结,不由用户线程完结,如 windows 的 IOCP ,Linux的AIO。

10分钟!读懂虚拟内存 & I/O & 零拷贝
零仿制

(一)传统IO流程

传统IO流程会经过如下两个进程:

  • 数据预备阶段:数据从硬件到内核空间

  • 数据仿制阶段:数据从内核空间到用户空间

10分钟!读懂虚拟内存 & I/O & 零拷贝

零仿制:指数据无需从硬件到内核空间或从内核空间到用户空间。下面介绍常见的零仿制完结

(二)mmap + write

mmap 将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,然后完结内核缓冲区与运用程序内存的同享,省去了将数据从内核读缓冲区(read buffer)仿制到用户缓冲区(user buffer)的进程,整个仿制进程会发生 4 次上下文切换,1 次CPU 仿制和 2次 DMA 仿制。

10分钟!读懂虚拟内存 & I/O & 零拷贝

(三)sendfile

经过sendfile 体系调用,数据可以直接在内核空间内部进行I/O 传输,然后省去了数据在用户空间和内核空间之间的来回仿制,sendfile 调用中I/O 数据对用户空间是彻底不可见的,整个仿制进程会发生 2 次上下文切换,1 次CPU 仿制和 2次 DMA 仿制。

10分钟!读懂虚拟内存 & I/O & 零拷贝

(四)Sendfile + DMA gather copy

Linux2.4引进 ,将内核空间的读缓冲区(read buffer)中对应的数据描绘信息(内存地址、地址偏移量)记录到相应的网络缓冲区(socketbuffer)中,由 DMA依据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)仿制到网卡设备中,这样就省去了内核空间中仅剩的 1 次 CPU 仿制操作,发生2次上下文切换、0 次CPU 仿制以及 2次 DMA 仿制;

10分钟!读懂虚拟内存 & I/O & 零拷贝

(五)splice

Linux2.6.17版别引进,在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间树立管道(pipeline),然后防止了两者之间的 CPU 仿制操作,2 次上下文切换,0 次CPU 仿制以及 2次 DMA 仿制。

10分钟!读懂虚拟内存 & I/O & 零拷贝

(六)写时仿制

10分钟!读懂虚拟内存 & I/O & 零拷贝

经过尽量延迟发生私有目标中的副本,写时仿制最充分有利地势用了稀有的物理资源。

(七)Java中零仿制

MappedByteBuffer:依据内存映射(mmap)这种零仿制办法的供给的一种完结。

10分钟!读懂虚拟内存 & I/O & 零拷贝

FileChannel 依据sendfile定义了 transferFrom() 和 transferTo() 两个抽象办法,它经过在通道和通道之间树立衔接完结数据传输的。

10分钟!读懂虚拟内存 & I/O & 零拷贝

参考

mp.weixin.qq.com/s/c81Fvws0J…

blog.csdn.net/Chasing\_\_…

mp.weixin.qq.com/s/EDzFOo3gc…

mp.weixin.qq.com/s/G6TfGbc4U…

www.modb.pro/db/189656

mp.weixin.qq.com/s/r9RU4RoE-…

阅读原文