虚拟内存和零拷贝

虚拟内存

操作系统提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。如果程序要访问虚拟地址的时候,进程持有的虚拟地址会通过CPU中的内存管理单元MMU的映射关系,来转换位物理地址,然后在通过物理地址访问内存地址,这样不同进程运行的时候对相同虚拟地址的写入实际上是写入不同的物理地址,这样就不会导致操作物理内存冲突了。

内存分段

分段机制下的内存地址是有两部分组成的,分别为段选择子段内偏移量

段选择子里面最重要的是段号(段表的索引),段表里面保存的是这个段的基地址、段的界限和特权等级等。

段内偏移量是位于0~段界限之间的值,段基地址加上段内偏移量就能得到物理内存地址。

段表是虚拟地址与物理地址的映射,分段机制会把程序的虚拟地址分为4个段,每个段在段表中有一个项,在一个项中找到段的基地址,在加上偏移量,于是就能找到物理内存中的地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2z9cpD7P-1642599396414)(C:\Users\clsld\Documents\note-document\linux\image-20211111211400458.png)]

分段的方法解决了程序本身不需要关心的物理内存地址访问的问题,但是它也有一些不足:

  1. 内存碎片问题
  2. 内存交换效率低的问题

内存问题可以分为外部内存碎片问题内部内存碎片问题

外部内存碎片问题也就是产生了多个不连续的小物理内存导致新的程序没有办法装载;

内部内存碎片问题就是程序所有的内存都被装载到了物理内存,但是这个程序有部分内存可能并不是很常用,这也会导致内存的浪费。

解决外部内存碎片问题的方法就是内存交换,将程序占用的内存写到硬盘上,然后在从硬盘上读取数据回内存里,在读取回的时候将数据写入的位置紧跟在其他程序占用的内存后面,这样就把空缺出来的内存间隙进行整理了。

这个内存交换的空间在linux系统中,就是swap空间,这块空间是从硬盘中划分出来的,用于内存与硬盘的空间交换。对于多进程的系统来说,内存碎片是很容易产生的,那有内存碎片就不得不进行内存交换,但是因为硬盘的访问速度比内存慢太多了,所以如果交换的内存数据量太过大时效率是非常低的。

内存分页

分页是把虚拟内存和物理内存空间且分层一段段固定尺寸的大小,这样一个固定尺寸大小的内存空间叫做页,在linux下每一页的大小为4KB。即页表保存的就是虚拟内存和物理内存你的对应的关系,当进程访问的虚拟内存地址在页表中查不到时,系统就会产生一个缺页异常,出现异常后,系统就会在内核空间中为进程分配物理内存,更新进程的页表,最后返回给用户进程,恢复进程的运行。

分页机制下内存地址分为两部分,页号页偏移量

页号是作为页表的索引,页表包含了物理页每页所在的物理内存基地址,通过物理内存基地址加上偏移量就能找到物理内存地址。

因为操作系统可以同时运行的进程非常的多,在32位环境下,虚拟内存地址有4GB,假设一个页的大小为4KB,那么就需要大约100万个页来对这些内存进行维护,如果每个页表4个字节,那么大概需要4MB内存来存储页表,这还只是一个进程的,要是有100个进程就需要400MB,这时页表将十分的庞大。

多级页表
二级分页

我们将一级页表进行分页,分成1024个二级页表,每个二级页表中存在1024个页表项

这样看来我们又额外引入了1024个页表,不是会使页表占用的内存更大吗,但是对于大多数程序来说,进程使用到的空间远远没有达到内存的限制,所以会存在部分对应的页表项是空的情况,如果一个一级页表的页表项没有被用到,就不需要创建这个一级页表对应的二级页表,可以在需要是再创建二级页表。并且对于不是空的页表项即已分配的页表项也存在最近一段时间未访问的页表,在这种情况下,操作系统会将页表换出到硬盘中,这样就不会占用物理内存。

多级分页

而在64位系统中,因为随着位数的增加每个页表的大小都变大了许多,所以二级分页是不够用的,因此就演变成了四级分页。全局页目录项PGD、上层页目录项PUD、中间层页目录项PMD、页表项PTE。

多级页表虽然解决了空间占用大的问题,但是由于其复杂了地址的转换,因此也带来了大量的时间开销,使得地址转换速度减慢。如果要解决这个问题,那么最简单的方式就是降低查询页表的频率。在CPU中有一个专门存放最常访问页表项的cache,这个cache就是TLB(Translation Lookaside Buffer),通常称为页表缓存、快表等。有了TLB后,在CPU寻址时会先查TLB,如果没有找到再查常规的页表。

段页式内存

内存分段和内存分页并不是对立的,它们可以组合起来使用,先将程序划分为多个有逻辑意义的段,也就是分段机制;接着把每个段划分为多个页,也就是把分段划分出来的连续空间在划分为固定大小的页。那么段页式内存的数据结构就是每一个程序有一张段表,段表中你的每个段又对应着一张页表,段表中的地址是页表的起始地址,而页表中的地址则对应着实际的物理内存地址。

访问过程为访问段表,得到页表起始地址,访问页表,得到物理页号,将物理页号与页内偏移量进行组合,得到物理地址。

linux内存

由于Intel x86CPU在实现段式内存管理的同时也实现了页式内存管理,Intel中每个段加上偏移的逻辑地址经过段式内存管理器映射成线性地址即虚拟地址,而线性地址在由页式内存管理映射成物理地址。

而linux主要采用的是页式内存管理,但是出于对Intel cpu的考虑不可避免的涉及了段式内存管理。但是linux把Intel中段式内存管理的逻辑地址给屏蔽了,在linux中每个段的起始地址都是一样的,这就意味着不需要经过逻辑地址到线性地址即虚拟地址的转换,而intel CPU中的段内存机制只用于访问控制和内存保护。

linux系统把虚拟地址空间分为内核空间和用户空间,不同的位数的操作系统的地址空间范围大小不一样。虽然每个进程都有各自的独立的虚拟内存,但是每个虚拟内存中的内核地址其实关联的都是相同的物理内存,这样进程切换到内核态后,就可以很方便地访问内核空间内存。

用户空间从高到低分别是7种不同的内存端,程序文件端(包括二进制可执行代码)、已初始化数据段(包括静态常量)、未初始化数据段(包括未初始化的静态变量)、堆段(包括动态分配的内存)、文件映射段(包括动态库、共享内存等)、栈段(包括局部变量和函数调用的上下文等,栈的大小是固定的,一般是8MB)

零拷贝

磁盘对比内存来说是相当慢的硬件了,所以很多优化方案都是减少对磁盘的访问,比如零拷贝、直接IO、异步IO等,这些优化的目的都是为了提供系统的吞吐量。

用户态和内核态

对于操作系统来说,创建一个进程是核心功能,创建进程需要做很多工作,比如分配物理内存,父子进程拷贝学习,拷贝设置页目录页表等等,这些最关键的工作得由特定的进程去做,这样可以做到集中管理,减少有限资源的访问和使用冲突。用户运行一个程序,该程序创建的进程开始运行自己的代码,如果要执行文件操作、网络数据发送等就必须通过write、read、send等系统调用,这些系统调用会调用内核的代码,进程会切换到特权级为0,然后进入内核地址空间去执行内核代码。

当一个进程在执行用户自己代码时出于用户态,此时该进程的特权等级最低为3级,是普通用户进程运行的特权级,特权状态为3的进程不能访问特权级为0的地址空间,包括代码和数据;当一个进程因为系统调用陷入到内核代码中执行时处于内核态,此时特权级最高,为0。每个进程都有自己的内核栈,执行的内核代码会使用当前进程的内核栈。

用户态切换到内核态的三种方式:

系统调用,这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。

异常,当CPU在执行运行在用户态下的程序时,发生了一些没有预知的异常这时会触发由当前运行进程切换到处理此异常的内核相关代码中

外围设备中断,当外围设备完成用户请求的操作后,会向CPU发出相应的中断信息,这时CPU会暂停执行下一条即将要执行的指令而转到与中断信号对应的处理程序去执行,如果前面执行的指令是用户态下的进程,那么转换的过程自然就是有用户态到内核态的切换,如磁盘读写操作完成,系统会切换回磁盘读写的中断处理程序中执行后面的操作。

DMA技术

DMA(Direct Memory Access)直接内存访问技术,简单理解就是在进行IO设备和内存的数据传输的时候,数据搬运的工作全部交给DMA控制器,而CPU不再参与任何与数据搬运相关的事情,这样CPU就可以去处理别的事务。

在没有DMA技术前,IO操作的过程是这样的:

进程调用read()系统调用,进程从用户态切换到内核态,CPU发出指令给磁盘控制器然后返回;

磁盘控制器收到指令后,于是就开始准备数据,把数据放入磁盘控制器的缓冲区中,然后产生一个CPU中断;

CPU收到中断信号后就会暂停执行下一条即将要执行的指令而转到中断信号对应的处理程序中去执行;

CPU把磁盘控制器缓冲区的数据一个一个字节的读进自己的寄存器中的PageCache,

然后将Pagecache里面的数据写拷贝到用户缓冲区,随后该进程从内核态切换为用户态。

使用DMA控制器进行数据传输的过程:

用户进程调用read()方法向操作系统发出IO请求,请求读取数据到自己的内存缓冲区,进程进入阻塞状态;

操作系统收到请求后,进一步将IO请求发送给DMA然后让CPU执行其他任务;

DMA进一步将IO请求发送给磁盘,磁盘收到DMA的IO请求后,把数据从磁盘读取到磁盘控制器的缓冲区中;

当磁盘控制器的缓冲区被读满后,向DMA发起中断信号,告知自己缓冲区已满;

DMA收到磁盘信号后,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用CPU,CPU可以执行其他任务‘

当DMA读取了足够多的数据,就会发送中断信号给CPU;

CPU收到DMA的信号知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回。

整个数据的传输过程中,CPU不再参与数据的搬运工作,全程由DMA来完成,但是CPU在整个过程中也是必不可少的

文件传输的改进

文件传输的问题

服务端要想提供文件传输的功能,最简单的方式就是将磁盘上的文件读取出来,然后通过网络协议发送给客户端。而在进程层面看的话其实会有4次用户态与内核态上下文切换的过程,

即进程调用read()读取磁盘文件时,进程会从用户态切换成内核态,通过DMA将磁盘上的文件拷贝到内核缓冲区

把内核缓冲区中的数据拷贝到用户缓冲区里后会将内核态切换为用户态,这个搬运数据的过程是需要CPU完成的

把用户缓冲区的数据拷贝到内核的socket缓冲区里,此时需要进程从用户态到内核态的切换并且这个过程也是需要CPU完成的。

把内核的socket缓冲区里的数据拷贝到网卡的缓冲区里,这个过程是由DMA完成的。

我们可以看到这个文件传输的过程中,数据搬运了四次,过多的数据拷贝会降低系统的性能。要提高文件传输的性能就要减少用户态与内核态上下文的切换内存拷贝的次数

优化文件传输性能

读取磁盘数据的时候之所以要进行上下文切换是因为用户空间没有权限操作磁盘和网卡,内核的权限最高,这些操作设备的过程需要交给操作系统内核来完成,而一次系统调用必然会发生两次上下文切换:首先是从用户态到内核态,当内核态执行完任务后会切换会用户态。要想减少上下文切换的次数就要减少系统调用的次数。

文件传输方式会经历4次数据拷贝,而这里面中从内核的缓冲区拷贝到用户缓冲区再从用户缓冲区拷贝到socket缓冲区里,这个过程是没有必要的,我们应该想办法减少数据拷贝的次数。

零拷贝技术实现减少数据拷贝次数

mmap+write

我们知道read()系统调用会把内核缓冲区中的数据拷贝到用户缓冲区,于是为了减少这一开销我们可以用mmap()替换read()系统调用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U2KsGi5l-1642599455351)(C:\Users\clsld\Documents\note-document\linux\image-20211114152019001.png)]

进程调用mmap()后,DMA会把磁盘的数据拷贝到内核缓冲区,进程用户和系统内核共享这个缓冲区;

进程再调用write(),操作系统直接将内核缓冲区的数据拷贝到socket缓冲区中,由CPU来搬运数据;

最后把内核socket缓冲区里的数据宝贝到网卡缓冲区里,这是由DMA来搬运的。

mmap()系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样内核就不需要与用户空间进行数据拷贝。

sendfile
sendfile(int out_fd,int in_fd,off_t *offest,size_t count);
//前两个参数是目的端和源端文件描述符,
//后两个参数是源端的偏移量和复制数据的长度,
//返回值是实际复制数据的长度。

该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态。但这不是真正的零拷贝技术,如果网卡支持SG-DMA技术,sendfile()系统调用可以通过DMA将磁盘上的数据拷贝到内核缓冲区,缓冲区描述符和数据长度传到socket缓冲区,此过程不需要将数据从操作系统内核缓冲区拷贝到socket缓冲区中。这里所谓的零拷贝技术是因为我们没有在内存层面去拷贝数据,也就是说全程没有CPU来搬运数据,所有数据都是通过DMA来进行传输的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JLOqnINW-1642599455352)(C:\Users\clsld\Documents\note-document\linux\image-20211114171126624.png)]

零拷贝技术的文件传输方式相比传统的文件传输方式减少了2次的上下文切换和数据拷贝次数,只需要2次上下文切换和数据拷贝次数就可以完成文件的传输,而且2次的数据拷贝过程都不需要CPU,2次都是由DMA进行搬运的。

PageCache

文件传输的过程中,第一步就是需要先把初盘文件数据拷贝到内核缓冲区中,这个内核缓冲区实际上就是磁盘的高速缓存(PageCache)。读写磁盘相比读写内存的速度慢很多,所以我们要想办法把读写磁盘替换成读写内存,于是我们通过DMA把磁盘的数据搬运到内存中,这样就可以用读内存代替读磁盘。但是内存的空间比磁盘空间要小很多,内存注定只能拷贝磁盘里的一小部分数据。所以通常刚被访问的数据在短时间内再次被访问的概率很高,于是PageCache来缓存最近被访问的数据,当空间不足时淘汰最久未被访问的缓存。

所以读磁盘数据的时候优先是在PageCache中找,如果数据在PageCache则直接返回,如果没有,则在磁盘中读取,然后缓存到PageCache中。读取磁盘数据的时候,需要找到数据所在的位置,但是对于机械磁盘需要通过磁头旋转到数据所在的扇区,再开始顺序读取数据,这个旋转磁头的都做是比较耗时的。所以PageCache还有预读功能,假设read()方法每次只会读32KB字节,虽然read刚开始的时候只会读取0~32KB的字节,但是内核会把后面的32—64KB的数据也读取到PageCache中,这样读取后面32—64KB数据的成本就很低。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值