一、简介MMAP mmap技术 是一种文件或其他对象映射到内存的技术。这种技术,让用户程序(用户空间)直接访问设备内存(内核空间),相比于在用户空间和内核空间互相拷贝数据,效率更高。 系统调用命令:mmap()。使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write() 等操作。 什么是零拷贝(Zero-copy)? 零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。 二、DMADMA的全称叫直接内存存取(Direct Memory Access),是一种允许外围设备直接访问系统主内存的机制,原来需要依赖cpu进行的数据传输现在可以交给DMA控制器来做,DMA控制传输的过程中cpu可以去做其他的工作,使效率大大提升,目前大多数的硬件设备,包括磁盘控制器、网卡、显卡以及声卡等都支持 DMA 技术 在DMA技术出现之前,应用程序与磁盘之间的I/O操作都是通过cpu的中断完成的,如图: 有了DMA技术以后: DMA控制器接过了将数据从磁盘控制器缓冲区拷贝到内核缓冲区的工作,解放了cpu。 为什么要有 DMA 技术? 在没有 DMA 技术前,I/O 的过程是这样的:
为了方便你理解,我画了一副图: 可以看到,整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。 简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来。 计算机科学家们发现了事情的严重性后,于是就发明了 DMA 技术,也就是直接内存访问(Direct Memory Access) 技术。 【文章福利】小编推荐自己的Linux内核技术交流群:【865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!! 什么是 DMA 技术?简单理解就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。 那使用 DMA 控制器进行数据传输的过程究竟是什么样的呢?下面我们来具体看看。 具体过程:
可以看到, 整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。 早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。 传统的文件传输有多糟糕? 如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。 传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。 代码通常如下,一般会需要两个系统调用:
代码很简单,虽然就两行代码,但是这里面发生了不少的事情。 首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。 其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:
我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。 这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。 所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。 如何优化文件传输的性能? 先来看看,如何减少「用户态与内核态的上下文切换」的次数呢? 读取磁盘数据的时候,之所以要发生上下文切换,这是因为用户空间没有权限操作磁盘或网卡,内核的权限最高,这些操作设备的过程都需要交由操作系统内核来完成,所以一般要通过内核去完成某些任务的时候,就需要使用操作系统提供的系统调用函数。 而一次系统调用必然会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。 所以,要想减少上下文切换到次数,就要减少系统调用的次数。 再来看看,如何减少「数据拷贝」的次数? 在前面我们知道了,传统的文件传输方式会历经 4 次数据拷贝,而且这里面,「从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到 socket 的缓冲区里」,这个过程是没有必要的。 因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的。 三、零拷贝零拷贝技术是另一个系统调用,Linux 中如 sendfile 命令。它减少了内存中用户空间与内核空间数据的拷贝过程,使得 CPU 处理效率更高。 如何实现零拷贝? 零拷贝技术实现的方式通常有 2 种:
下面就谈一谈,它们是如何减少「上下文切换」和「数据拷贝」的次数。 3.1mmap + write在前面我们知道,
具体过程如下: 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区; 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据; 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。 我们可以得知,通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。 但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。 3.2sendfile相比
sendfile是Linux2.1内核版本后引入的一个系统调用函数,通过使用sendfile数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,同时由于使用sendfile替代了read+write从而节省了一次系统调用,也就是2次上下文切换。 整个过程发生了2次用户态和内核态的上下文切换和3次拷贝,具体流程如下:
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。 零拷贝技术的文件传输方式相比传统文件传输的方式,减少了2次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。 所以,总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上。 3.3使用零拷贝技术的项目事实上,Kafka 这个开源项目,就利用了「零拷贝」技术,从而大幅提升了 I/O 的吞吐率,这也是 Kafka 在处理海量数据为什么这么快的原因之一。 如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的
如果 Linux 系统支持 曾经有大佬专门写过程序测试过,在同样的硬件条件下,传统文件传输和零拷拷贝文件传输的性能差异,你可以看到下面这张测试数据图,使用了零拷贝能够缩短 另外,Nginx 也支持零拷贝技术,一般默认是开启零拷贝技术,这样有利于提高文件传输的效率,是否开启零拷贝技术的配置如下:
sendfile 配置的具体意思:
当然,要使用 sendfile,Linux 内核版本必须要 2.1 以上的版本。 3.4sendfile+DMA Scatter/GatherLinux2.4内核版本之后对 它将读缓冲区中的数据描述信息--内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程 整个过程发生了2次用户态和内核态的上下文切换和2次拷贝,其中更重要的是完全没有CPU拷贝,具体流程如下:
3.5应用场景对于文章开头说的两个场景:RocketMQ和Kafka都使用到了零拷贝的技术。 对于MQ而言,无非就是生产者发送数据到MQ然后持久化到磁盘,之后消费者从MQ读取数据。 对于RocketMQ来说这两个步骤使用的是mmap+write,而Kafka则是使用mmap+write持久化数据,发送数据使用sendfile。 Kafka Kafka 是一个分布式发布订阅消息系统,它巧妙用到了这两种技术。 数据的输入(从网卡到磁盘) 用了 MMAP 打通用户空间和内核空间,并将一部分内存映射到磁盘上的一段空间。 数据的输出(从磁盘到网卡) 若没有零拷贝,用户空间先调内核的 read 去读磁盘中的文件,将磁盘数据存入用户空间(data 从磁盘->内核空间->用户空间);然后再调用内核的 write 方法,将数据发到网卡(data 从用户空间->内核空间->网卡)。由于数据没有在用户态改变数据,所以造成了数据的流转浪费。 四、共享内存mmap内核和用户空间,共享内存。数据copy到内核区后,只需要把地址共享给应用程序即可,无需再copy一次数据到用户空间。 优点:
缺点:
应用: kafka生产者发送消息到broker的时候,broker的网络接收到数据后,copy到broker的内核空间。然后通过mmap技术,broker会修改消息头,添加一些元数据。所以,写入数据很快。当然顺序IO也是关键技术。 函数原型:
mmap的内存即不在堆也不在栈上,是一块独立的空间。 4.1mmap()mmap()在调用进程的虚拟地址空间中创建一个新的映射。新映射的起始地址在addr中指定。length参数指定映射的长度。 如果addr为空,则内核选择创建映射的地址;这是创建新映射的最可移植方法。如果addr不为空,则内核将其作为一个提示,提示将映射放置在何处;在Linux上,映射将在附近的页面边界处创建。新映射的地址作为调用的结果返回。 文件映射的内容(与匿名映射相反;参见下面的MAP_MAP_ANONYMOUS)使用文件描述符fd所引用的文件(或其他对象)中从偏移量offset开始的length字节进行初始化。offset必须是sysconf(_SC_PAGE_SIZE)返回的页面大小的倍数。 prot参数描述了映射所需的内存保护(不得与文件的打开模式冲突)。它是PROT_NONE或以下一个或多个标志的位OR: flags参数确定映射的更新是否对映射相同区域的其他进程可见,以及更新是否传递到基础文件。通过在标志中包含以下值中的一个来确定此行为: 此外,以下值中的零个或多个可以在flag中进行“或”运算: 返回值: 成功后,mmap()返回指向映射区域的指针。错误时,返回值MAP_FAILED(即,(void*)-1),并设置errno以指示错误原因。 4.2munmap()munmap()系统调用删除指定地址范围的映射,并导致对该范围内陆址的进一步引用生成无效内存引用。当进程终止时,区域也会自动取消映射。另一方面,关闭文件描述符不会取消区域映射。 地址addr必须是页面大小的倍数(但长度不必是)。包含指定范围一部分的所有页面均未映射,对这些页面的后续引用将生成SIGSEGV。如果指示的范围不包含任何映射页,则不是错误。 返回值: 成功时,munmap()返回0。失败时,它返回-1,errno被设置为指示错误原因(可能是EINVAL)。 错误代码 使用映射区域可产生以下信号: 流程 (1)打开文件 示例代码:
shm*接口 共享内存就是允许两个不相关的进程访问同一个内存块。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。 共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以,通常需要用其他的机制来同步对共享内存的访问,例如信号量。 shmget() 创建共享内存。函数原型:
描述: shmget()返回与参数key的值关联的System V共享内存段的标识符。如果key的值为IPC_PRIVATE或key不是IPC_PRIVATE,不存在与key对应的共享内存段,并且在shmflg中指定了IPC_CREAT,则会创建一个大小等于size值的新共享内存段(向上舍入为PAGE_SIZE的倍数)。 如果shmflg同时指定IPC_CREAT和IPC_ EXCL,并且key已经存在共享内存段,则shmget()将失败,错误号设置为EEXIST。【这类似于open()的组合O_CREAT|O_EXCL的效果。】 值shmflg由以下组成: 除上述标志外,shmflg的最低有效9位指定授予所有者、组和其他人的权限。这些位的格式和含义与open()的模式参数相同。目前,系统不使用执行权限。 返回值: 成功后,将返回有效的共享内存标识符。出现错误时,返回-1,并设置errno以指示错误。 错误: 失败时,错误号设置为以下之一: hmat() 启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间,函数原型:
描述: shmat()将由shmid标识的System V共享内存段附加到调用进程的地址空间。附加地址由shmaddr根据以下标准之一指定:
除了SHM_RND,还可以在shmflg位掩码参数中指定以下标志: 呼叫进程的brk()值不被附加改变。该段将在进程退出时自动分离。同一段可以作为读写段附加在进程的地址空间中,并且可以多次附加。 成功的shmat()调用更新与共享内存段相关联的shmid_ds结构的成员【参见shmctl()】,如下所示:
返回值: 成功时,shmat()返回附加共享内存段的地址;错误时,返回(void*)-1,并设置errno以指示错误原因。 错误: 当shmat()失败时,errno设置为以下之一: shmdt() 将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。函数原型:
描述: shmdt()将位于shmaddr指定地址的共享内存段从调用进程的地址空间中分离。要分离的段当前附加的shmaddr必须等于附加的shmat()调用返回的值。 参数shmaddr是shmat()函数返回的地址指针。 在成功调用shmdt()时,系统更新与共享内存段关联的shmid_ds结构的成员,如下所示:
返回值: 成功时,shmdt()返回0;在出现错误时,返回-1,并设置errno以指示错误原因。 错误: 当shmdt()失败时,errno设置如下: shmctl() 控制共享内存。函数原型:
描述: shmctl()对系统V共享内存段执行cmd指定的控制操作,该段的标识符在shmid中给出。 buf参数是指向shmid_ds结构的指针,如下:
ipc_perm结构定义如下:
返回值: 成功的IPC_INFO或SHM_INFO操作将返回内核内部数组中记录所有共享内存段信息的最高使用项的索引。(此信息可与重复的SHM_STAT操作一起使用,以获得有关系统上所有共享内存段的信息。)成功的SHM_STAT操作返回其索引在shmid中给出的共享内存段标识符。其他操作成功时返回0。 出现错误时,返回-1,并适当设置errno。 流程 共享内存,可以大大加快对文件或设备的读写操作。共享内存的方式有mmap和shmget 、 shmat。 所谓的零拷贝,就是不需要CPU的参与,而不是其他的意思,mmap内部其实是一个DMA技术。 |
|