不知道有没有朋友和笔者一样,在没有了解零拷贝以前,便从字面意思理解为没有拷贝,单纯而又美好;其次笔者在刚接触工作的时候,老练的同事们在讨论性能优化的时候,拷贝这个词也会时常提及,在学习kafka时,也看到其在性能点中提到零拷贝技术。所以拷贝以及零拷贝的学习势在必得,基于此,花了一段时间学习了拷贝和零拷贝。本文文字较多,需要耐心的阅读,当然后期笔者会补上一些示意图,辅助理解。本文主要尝试搞明白的问题有如下几个:
- 什么是拷贝?
- 什么是零拷贝?
- 零拷贝有哪些实现方式?
- 以及其他的一些基本概念等等…
1. 拷贝是指将数据从一个内存区域复制到另一个内存区域,需要涉及到CPU的开销,就会影响系统的性能;
2. 零拷贝是指CPU不需要先将数据从一个内存取余拷贝到另一个内存区域。它的作用是在数据报从网络设备到用户程序空间传递的过程中, 减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载;
3. 主要实现技术是DMA数据传输技术和内存区域映射技术;
4. 作用: 可以减少内核缓冲区和用户进程缓冲区的反复IO拷贝;
减少内核进程地址空间和用户进程地址空间之间因为上下文切换带来的CPU开销。
内核空间和用户空间
为了避免用户进程直接 *** 作内核,保证内核安全, *** 作系统将虚拟内存划分为内核空间和用户空间
在linux系统中,内核模块运行在内核之中。用户程序运行在用户空间,对应的程序处于用户态。
- 内核空间总是驻留在内存中,它是为 *** 作系统内核保留的。应用程序不可以在该区域直接进行读写或者直接调用内核所定义的函数;
- 内核按照访问权限又分为进程私有和共享
1. 进程私有的虚拟内存:如每个进程的内核栈、页表、task结构以及mem_map结构等;
2. 共享的虚拟内存:物理存储、内核数据、内核代码区域。
用户空间
每个普通的进程都有一个单独的用户空间,处于用户态的进程不能访问内核中的数据,也不能调用内核中的函数,若需要调用需切换到内核态。
- 运行时栈:由编译器自动释放,存放函数的参数值,局部变量和方法返回值等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存储到栈顶,调用结束后调用信息会被d出d出并释放掉内存。栈区是从高地址位向低地址位增长的,是一块连续的内在区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
- 运行时堆:用于存放进程运行中被动态分配的内存段,位于 BSS 和栈中间的地址位。由卡发人员申请分配(malloc)和释放(free)。堆是从低地址位向高地址位增长,采用链式存储结构。频繁地 malloc/free 造成内存空间的不连续,产生大量碎片。当申请堆空间时,库函数按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
- 代码段:存放 CPU 可以执行的机器指令,该部分内存只能读不能写。通常代码区是共享的,即其它执行程序可调用它。假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。
- 未初始化的数据段:存放未初始化的全局变量,BSS 的数据在程序开始执行之前被初始化为 0 或 NULL。
- 已初始化的数据段:存放已初始化的全局变量,包括静态全局变量、静态局部变量以及常量。
- 内存映射区域:例如将动态库,共享内存等虚拟空间的内存映射到物理空间的内存,一般是 mmap 函数所分配的虚拟内存空间。
linux层级结构
- 内核态可以执行任意命令,调用系统的一切资源,而用户态只能执行简单的运算,不能直接调用系统资源。
- 用户态必须通过系统接口(System Call),才能向内核发出指令。
- 内核空间可以访问所有的 CPU 指令和所有的内存空间、I/O 空间和硬件设备。
- 用户空间只能访问受限的资源,如果需要特殊权限,可以通过系统调用获取相应的资源。
- 用户空间允许页面中断,而内核空间则不允许。
- 内核空间和用户空间是针对线性地址空间的。
- x86 CPU中用户空间是 0 - 3G 的地址范围,内核空间是 3G - 4G 的地址范围。x86_64 CPU 用户空间地址范围为0x0000000000000000 – 0x00007fffffffffff,内核地址空间为 0xffff880000000000 - 最大地址。
- 所有内核进程(线程)共用一个地址空间,而用户进程都有各自的地址空间。
从而,linux内部层级结构分为三部分,分别是硬件、内核空间和用户空间。
零拷贝来源
在了解零拷贝之前,先了解为什么会产生拷贝,以用户进程访问内存区域为例,看看整个过程需要哪些步骤:
用户进程申请并访问物理内存的过程- 用户进程向 *** 作系统发出内存申请请求;
- 系统检查,分配地址;
- 创建内存映射,并放到进程的页表;
- 返回地址给用户进程,开始访问虚拟地址;
- CPU根据页表找到内存映射,但是当前还未关联实际的物理地址,于是产生缺页中断;
- *** 作系统分配实际的物理内存,并且关联到页表相应内存映射,处理完后中断,CPU就可以访问内存;
- 一般情况, *** 作系统在第三步就将物理内存与内存映射相关联,因而很多时候都不会发生缺页中断。
- 用户进程向CPU发起read系统调用,用户态切换为内核态,堵塞等待数据返回;
- CPU接收到指令后对磁盘发起IO请求,将数据先放入磁盘缓冲区;
- 数据准备完成后,磁盘向CPU发起I/O中断;
- CPU收到中断,先从磁盘缓冲把数据拷贝到内核缓冲区,然后从内核缓冲区拷贝用户缓冲区;
- 用户进程切换为用户态,接触阻塞,等待下一个执行时间片。
当进程想从磁盘读数据到内存,又把数据送到另一个内存区域,便会涉及内存拷贝。而拷贝就会消耗CPU的性能,然后磁盘的读取速度有CPU的处理速度不在一个等级上。为了提升一个系统的性能,就需要减少拷贝的次数。
扩展知识:I/O中断
I/O中断属于硬件中断,需要硬件来接收中断信号,在IO *** 作中,又分为有中断和无中断:
- 无中断的IO *** 作:
- 当进程读取文件时,先检查页缓存中是否有数据,若没有数据则执行实际的IO *** 作。在执行IO *** 作时,用户线程将会阻塞等待硬盘将数据写到内存中,对用户来说线程是阻塞的;
- 在实际io *** 作中,若没有中断CPU会不断轮询IO *** 作是否完成,若没有完成就继续调度其他线程,若完成CPU会加入到线程就绪队列并恢复线程上下文信息;
- 线程处于就绪队列时,可以被 *** 作系统调度从而继续执行读 *** 作,此时数据会从 *** 作系统内核读取到用户缓存中。
- 有中断的IO *** 作:
- 当进程需要从硬盘读取一个文件时,会先检查内核缓存中是否有数据,若没有数据,则执行实际I/O *** 作。在I/O *** 作执行时,用户线程将阻塞等待数据从硬盘写到内存中。对于用户来说线程是被阻塞的。
- 在实际的IO *** 作中,CPU向IO模块(DMA)发送读指令,然后就去调度其他线程;
- 当DMA执行完成后,会产生中断信号再通知CPU,CPU将线程加入到线程就绪队列中,并恢复线程上下文信息;
- 线程处于就绪队列时,可以被 *** 作系统调度从而继续执行读 *** 作,此时数据会从 *** 作系统内核读取到用户缓存中。
备注: 对于用户线程来说,有无中断不影响,但是对于 *** 作系统内核来说,通过中断方式主动通知CPU的方式减少了线程轮询的判断,提升了内核线程的执行效率。
零拷贝实现基本技术 1. DMA实现技术- 用户进程向CPU发起read系统调用,用户态切换为内核态,堵塞等待数据返回;
- CPU接收到指令后对DMA发起IO请求,
- DMA控制器向磁盘发起IO请求,将数据先放入磁盘缓冲区,CPU全程不参与;
- 数据读取完成,DMA收到磁盘的通知,将数据从磁盘控制器拷贝到内核缓冲区;
- DMA控制器向CPU发起数据读取完成通知,CPU将数据从内核拷贝到用户缓冲区;
- 用户进程切换为用户态,接触阻塞,等待下一个执行时间片。
- 函数原型:
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
/*
* 参数:
* start : 为内存映射的起始地址,是一个建议的参数,通常为0或者NULL,此时由内核去决定真实地址。当flags为MAP_FIXED时,便是必填的参数。
* length :文件需要进行内存映射的字节长度;
* prot: 访问权限,读写或者执行以及无权限PROT_READ,PROT_WRITE,PROT_EXEC,PROT_NONE;
* flags:内存映射区域的修改是否被多个进程共享;MAP_PRICATE,MAP_SHARED,MAP_FIXED;
* fd: 文件描述符,每次map *** 作都会导致文件引用计数+1,umap便会减1;
* offset: 问津便宜领,从文件的起始位置向后的偏移量
优点:
-
对文件的读取 *** 作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。
-
实现了用户空间和内核空间的高效交互方式。两空间的各自修改 *** 作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
-
提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。
-
可用于实现高效的大规模数据传输。内存空间不足,是制约大数据 *** 作的一个方面,解决方案往往是借助硬盘空间协助 *** 作,补充内存的不足。但是进一步会造成大量的文件I/O *** 作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。
总的来说磁盘缓存出现的原因大概有两个:第一是访问磁盘的速度远慢于访问内存的速度,通过在内存中缓存磁盘内容可以提高访问速度;第二是根据程序的局部性原理,数据一旦被访问过,就很有可能在短时间内再次被访问,所以在内存中缓存磁盘内容可以提高程序运行速度。
- linux的缓存方式是利用物理内存缓存磁盘上的内容,称为页缓存。进而对磁盘的访问转换为对内存的访问,提升了程序运行的效率;
- 页缓存是基于页的、面向文件的缓存机制。
- 进程调用read()发起文件请求;
- 内核检查打开的文件列表,调用文件系统提供的read接口;
- 找到文件对应的inode,然后计算尧都区的具体的页;
- 通过inode查找对应的页缓存,如果页缓存命中则直接返回文件内容,如果没有则产生一个缺页异常,这时候系统会创建新的空的页缓存并从磁盘中读取文件更新页缓存;
- 返回读取的文件。
- 当一个进程调用write时,对文件的更新仅仅是将其写入文件对应的页缓存中,然后将对应的页缓存标记为dirty整个写入过程就结束了;
- linux内核会周期性的将脏页写回到磁盘,并清理dirty的标志。
回写线程: - 空闲空间不足,需要释放一部分页缓存;
- 脏页在内存中处理时间超过阈值;
- 当用户进程调用sync和fsync系统调用时,这是给用户进程提供强制回写的方法,满足回写严格要求的使用场景。
磁盘是数据块 的集合,内核会对磁盘上的数据块做缓冲。内核将磁盘上的数据块复制到内核缓冲区中,当一个用户空间中的进程要从磁盘上读数据时,内核一般不直接读磁盘,而 是将内核缓冲区中的数据复制到进程的缓冲区中。当进程所要求的数据块不在内核缓冲区时,内核会把相应的数据块加入到请求队列,然后把该进程挂起,接着为其 他进程服务。一段时间之后(其实很短的时间),内核把相应的数据块从磁盘读到内核缓冲区,然后再把数据复制到进程的缓冲区中,最后唤醒被挂起的进程。
传统磁盘IO到网络会经历4次上下文切换,2次CPU拷贝,2次DMA拷贝
上下文切换:当用户程序向内核发起系统调用时,CPU 将用户进程从用户态切换到内核态;当系统调用返回时,CPU 将用户进程从内核态切换回用户态。
CPU拷贝:由 CPU 直接处理数据的传送,数据拷贝时会一直占用 CPU 的资源。
DMA拷贝:由 CPU 向DMA磁盘控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,从而减轻了 CPU 资源的占有率。
零拷贝实现
思路
- 用户态直接IO:应用程序可以直接访问硬件存储, *** 作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
缺点:用户直接IO,由于磁盘的数据读取与CPU的执行中间有时间差距,会造成资源浪费。解决方案可参考异步IO - mmap+Write
核心思想如下: - 用户进程通过mmap()向内核发起系统调用,用户态切换为内核态;
- 将用户进程的内核空间的读缓冲区与用户空间的缓存区进行内存地址映射;
- CPU利用DMA控制器将数据从主存或磁盘拷贝到内核空间的读缓冲区???
- 上下文从内核切换回用户态,mmap系统调用执行返回;
- 用户进程通过write向内核发起系统调用,上下文从用户态切换为内核态;
- CPU将读缓冲区中的数据拷贝到网络缓冲区(socket buffer);
- CPU利用DMA控制器将数据从网络缓冲区拷贝到网卡进行数据传输;
- 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。
缺陷:
1. 内存映射对应的是页缓存,最小单位为4kb,一个5kb的文件会占用8kb的内存空间,进而浪费3kb的空间,因而对于小文件来说,空间浪费较为严重。
2. 。。。
- sendfile
sendfile系统调用,不仅减少了CPU的拷贝次数,还减少了上下文的切换。
sendfile(socket_fd, file_fd, len);
具体流程如下:
- 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space);
- CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer);
- CPU直接将内核缓冲区中的数据拷贝到网络缓冲区;
- CPU利用DMA控制器将数据从网络缓冲区拷贝到网卡进行数据传输;
- 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。
备注: 相比于mmap内存映射的方式,sendfile减少了2次上下文的切换,但仍然有1次CPU拷贝,但是sendfile的缺陷是用户程序不能对其做修改,只是单纯的完成一次数据的传输过程
- sendfile+DMA gather copy
1. 与sendfile的区别在于,不用CPU将数据从内核拷贝到网络缓冲区,只用将缓冲区的文件描述符合数据长度拷贝到网络缓冲区,
2. 基于已拷贝的文件描述符和数据长度,CPU利用DMA控制器的gather/scatter *** 作直接批量将数据从内核的读缓冲区拷贝到网卡进行数据传输。
…
网卡到内存
- 数据包从外面的网络进入物理网卡,如果目的地址不是该网卡,且网卡没有开启混杂模式,该包会被网卡丢弃;
- 网卡数据包通过DMA的方式写入到指定的内存地址,地址又网卡驱动分配并初始化;
- 网卡通过IRQ(硬中断)通知CPU,有数据到来;
- CPU根据中断表,调用注册的中断函数,这个中断函数会调用驱动程序中的相应函数;
- 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据,下次收到数据包直接写入内存就好,不用在通知CPU,提高CPU的效率;
- 启动软中断。由于硬中断不能相应其他硬件的中断,于是内核引入软中断
+-----+
| | Memroy
+--------+ 1 | | 2 DMA +--------+--------+--------+--------+
| Packet |-------->| NIC |------------>| Packet | Packet | Packet | ...... |
+--------+ | | +--------+--------+--------+--------+
| |<--------+
+-----+ |
| +----------------+
| |
3 | Raise IRQ | Disable IRQ
| 5 |
| |
↓ |
+-----+ +------------+
| | Run IRQ handler | |
| CPU |------------------>| NIC Driver |
| | 4 | |
+-----+ +------------+
|
6 | Raise soft IRQ
|
↓
未完。。。
总结待续。。。
参考资料
- https://zhuanlan.zhihu.com/p/83398714
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)