写在开头:本文为Linux内存相关知识点简单总结。
1. 虚拟地址
Linux采用虚拟内存管理技术,利用虚拟内存技术让每个进程都有4GB 互不干涉的虚拟地址空间。
进程初始化分配和操作的都是基于这个「虚拟地址」,只有当进程需要实际访问内存资源的时候才会建立虚拟地址和物理地址的映射,调入物理内存页。
https://zhuanlan.zhihu.com/p/149581303
2. 物理地址
不管是用户空间还是内核空间,使用的地址都是虚拟地址,当需进程要实际访问内存的时候,会由内核的「请求分页机制」产生「缺页异常」调入物理内存页。
把虚拟地址转换成内存的物理地址,这中间涉及利用MMU 内存管理单元(Memory Management Unit ) 对虚拟地址分段和分页(段页式)地址转换。
image.png
3. 物理内存管理和虚拟内存分配。
3.1 物理内存管理
在Linux系统中通过分段和分页机制,把物理内存划分 4K 大小的内存页 Page(也称作页框Page Frame),物理内存的分配和回收都是基于内存页进行,把物理内存分页管理的好处大大的。
3.2 物理页管理面临问题
物理内存页分配会出现外部碎片和内部碎片问题,所谓的「内部」和「外部」是针对「页框内外」而言,一个页框内的内存碎片是内部碎片,多个页框间的碎片是外部碎片。
3.2.1 外部碎片
当需要分配大块内存的时候,要用好几页组合起来才够,而系统分配物理内存页的时候会尽量分配连续的内存页面,频繁的分配与回收物理页导致大量的小块内存夹杂在已分配页面中间,形成外部碎片,举个例子:
image.png
3.2.2 内部碎片
物理内存是按页来分配的,这样当实际只需要很小内存的时候,也会分配至少是 4K 大小的页面,而内核中有很多需要以字节为单位分配内存的场景,这样本来只想要几个字节而已却不得不分配一页内存,除去用掉的字节剩下的就形成了内部碎片。
image.png
3.2.3 页面管理算法
方法总比困难多,因为存在上面的这些问题,聪明的程序员灵机一动,引入了页面管理算法来解决上述的碎片问题。
3.2.3.1 Buddy(伙伴)分配算法
Linux 内核引入了伙伴系统算法(Buddy system),什么意思呢?就是把相同大小的页框块用链表串起来,页框块就像手拉手的好伙伴,也是这个算法名字的由来。
具体的,所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大可以申请1024个连续页框,对应4MB大小的连续内存。
image.png
因为任何正整数都可以由 2^n 的和组成,所以总能找到合适大小的内存块分配出去,减少了外部碎片产生 。
分配实例
比如:我需要申请4个页框,但是长度为4个连续页框块链表没有空闲的页框块,伙伴系统会从连续8个页框块的链表获取一个,并将其拆分为两个连续4个页框块,取其中一个,另外一个放入连续4个页框块的空闲链表中。释放的时候会检查,释放的这几个页框前后的页框是否空闲,能否组成下一级长度的块。
3.2.3.2 slab分配器
看到这里你可能会想,有了伙伴系统这下总可以管理好物理内存了吧?不,还不够,否则就没有slab分配器什么事了。
那什么是slab分配器呢?
一般来说,内核对象的生命周期是这样的:分配内存-初始化-释放内存,内核中有大量的小对象,比如文件描述结构对象、任务描述结构对象,如果按照伙伴系统按页分配和释放内存,对小对象频繁的执行「分配内存-初始化-释放内存」会非常消耗性能。
伙伴系统分配出去的内存还是以页框为单位,而对于内核的很多场景都是分配小片内存,远用不到一页内存大小的空间。slab分配器,「通过将内存按使用对象不同再划分成不同大小的空间」,应用于内核对象的缓存。
伙伴系统和slab不是二选一的关系,slab 内存分配器是对伙伴分配算法的补充。
主要优点
- slab 内存管理基于内核小对象,不用每次都分配一页内存,充分利用内存空间,避免内部碎片。
- slab 对内核中频繁创建和释放的小对象做缓存,重复利用一些相同的对象,减少内存分配次数。
3.3 虚拟内存分配
拟内存的分配,这里包括用户空间虚拟内存和内核空间虚拟内存。
3.3.1 用户空间内存分配
malloc
malloc 用于申请用户空间的虚拟内存,当申请小于 128KB 小内存的时,malloc使用 sbrk或brk 分配内存;当申请大于 128KB 的内存时,使用 mmap 函数申请内存;
存在问题
由于 brk/sbrk/mmap 属于系统调用,如果每次申请内存都要产生系统调用开销,cpu 在用户态和内核态之间频繁切换,非常影响性能。
而且,堆是从低地址往高地址增长,如果低地址的内存没有被释放,高地址的内存就不能被回收,容易产生内存碎片。
解决
因此,malloc采用的是内存池的实现方式,先申请一大块内存,然后将内存分成不同大小的内存块,然后用户申请内存时,直接从内存池中选择一块相近的内存块分配出去。
image.png
3.3.2 内核空间内存分配
kmalloc 和 vmalloc 分别用于分配不同映射区的虚拟内存
内核空间
kmalloc
kmalloc() 分配的虚拟地址范围在内核空间的「直接内存映射区」。
vmalloc
vmalloc 分配的虚拟地址区间,位于 vmalloc_start 与vmalloc_end 之间的「动态内存映射区」。
一般用分配大块内存,释放内存对应于 vfree,分配的虚拟内存地址连续,物理地址上不一定连续。函数原型在 <linux/vmalloc.h> 中声明。一般用在为活动的交换区分配数据结构,为某些 I/O 驱动程序分配缓冲区,或为内核模块分配空间。
下面的图总结了上述两种内核空间虚拟内存分配方式。
image.png4. MMU,页,页帧和页表
MMU:CPU 寻址时使用虚拟地址替代物理地址,然后再由 MMU(Memory Management Unit,内存管理单元)转换成物理地址。
内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小 4K(这个大小好像可以修改)分割成页(page)和页帧(page frame),并保证页与页帧的大小相同。
一块 4K 大小的虚拟地址范围称为页;
一块 4K 大小的物理地址范围称为页帧;
页表:页表就像一个函数,输入是页号,输出是页桢。操作系统给每一个进程维护一个页表。所以不同进程的虚拟地址可能一样。页表给出了进程中每一页所对应的页帧的位置。
https://www.jianshu.com/p/ca5c678c7237
5. Page Cache 和 Buffer Cache
Linux内核Page Cache和Buffer Cache关系及演化历史
Page Cache
以Page为单位,缓存文件内容。缓存在Page Cache中的文件数据,能够更快的被用户读取。同时对于带buffer的写入操作,数据在写入到Page Cache中即可立即返回,而不需等待数据被实际持久化到磁盘,进而提高了上层应用读写文件的整体性能。
Buffer Cache
磁盘的最小数据单位为sector,每次读写磁盘都是以sector为单位对磁盘进行操作。sector大小跟具体的磁盘类型有关,有的为512Byte, 有的为4K Bytes。无论用户是希望读取1个byte,还是10个byte,最终访问磁盘时,都必须以sector为单位读取,如果裸读磁盘,那意味着数据读取的效率会非常低。同样,如果用户希望向磁盘某个位置写入(更新)1个byte的数据,他也必须整个刷新一个sector,言下之意,则是在写入这1个byte之前,我们需要先将该1byte所在的磁盘sector数据全部读出来,在内存中,修改对应的这1个byte数据,然后再将整个修改后的sector数据,一口气写入磁盘。为了降低这类低效访问,尽可能的提升磁盘访问性能,内核会在磁盘sector上构建一层缓存,他以sector的整数倍力度单位(block),缓存部分sector数据在内存中,当有数据读取请求时,他能够直接从内存中将对应数据读出。当有数据写入时,他可以直接再内存中直接更新指定部分的数据,然后再通过异步方式,把更新后的数据写回到对应磁盘的sector中。这层缓存则是块缓存Buffer Cache。
深入理解Linux 的Page Cache
Page Cache 用于缓存文件的页数据,buffer cache 用于缓存块设备(如磁盘)的块数据。页是逻辑上的概念,因此 Page Cache 是与文件系统同级的;块是物理上的概念,因此 buffer cache 是与块设备驱动程序同级的。
在 Linux 2.4 版本的内核之前,Page Cache 与 buffer cache 是完全分离的。但是,块设备大多是磁盘,磁盘上的数据又大多通过文件系统来组织,这种设计导致很多数据被缓存了两次,浪费内存。所以在 2.4 版本内核之后,两块缓存近似融合在了一起:如果一个文件的页加载到了 Page Cache,那么同时 buffer cache 只需要维护块指向页的指针就可以了。只有那些没有文件表示的块,或者绕过了文件系统直接操作(如dd命令)的块,才会真正放到 buffer cache 里。因此,我们现在提起 Page Cache,基本上都同时指 Page Cache 和 buffer cache 两者,本文之后也不再区分,直接统称为 Page Cache。
Linux I/O 原理和 Zero-copy 技术全面揭秘
https://zhuanlan.zhihu.com/p/296207162
在 Linux 中,当程序调用各类文件操作函数后,用户数据(User Data)到达磁盘(Disk)的流程如上图所示。
图中描述了 Linux 中文件操作函数的层级关系和内存缓存层的存在位置,中间的黑色实线是用户态和内核态的分界线。
read(2)/write(2)
是 Linux 系统中最基本的 I/O 读写系统调用,我们开发操作 I/O 的程序时必定会接触到它们,而在这两个系统调用和真实的磁盘读写之间存在一层称为 Kernel buffer cache 的缓冲区缓存。在 Linux 中 I/O 缓存其实可以细分为两个:Page Cache 和 Buffer Cache,这两个其实是一体两面,共同组成了 Linux 的内核缓冲区(Kernel Buffer Cache):
1.读磁盘:内核会先检查 Page Cache 里是不是已经缓存了这个数据,若是,直接从这个内存缓冲区里读取返回,若否,则穿透到磁盘去读取,然后再缓存在 Page Cache 里,以备下次缓存命中;
2.写磁盘:内核直接把数据写入 Page Cache,并把对应的页标记为 dirty,添加到 dirty list 里,然后就直接返回,内核会定期把 dirty list 的页缓存 flush 到磁盘,保证页缓存和磁盘的最终一致性。
在 Linux 还不支持虚拟内存技术之前,还没有页的概念,因此 Buffer Cache 是基于操作系统读写磁盘的最小单位 -- 块(block)来进行的,所有的磁盘块操作都是通过 Buffer Cache 来加速,Linux 引入虚拟内存的机制来管理内存后,页成为虚拟内存管理的最小单位,因此也引入了 Page Cache 来缓存 Linux 文件内容,主要用来作为文件系统上的文件数据的缓存,提升读写性能,常见的是针对文件的 read()/write() 操作,另外也包括了通过 mmap() 映射之后的块设备,也就是说,事实上 Page Cache 负责了大部分的块设备文件的缓存工作。而 Buffer Cache 用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用,实际上负责所有对磁盘的 I/O 访问:
https://zhuanlan.zhihu.com/p/296207162
因为 Buffer Cache 是对粒度更细的设备块的缓存,而 Page Cache 是基于虚拟内存的页单元缓存,因此还是会基于 Buffer Cache,也就是说如果是缓存文件内容数据就会在内存里缓存两份相同的数据,这就会导致同一份文件保存了两份,冗余且低效。另外一个问题是,调用 write 后,有效数据是在 Buffer Cache 中,而非 Page Cache 中。这就导致 mmap 访问的文件数据可能存在不一致问题。为了规避这个问题,所有基于磁盘文件系统的 write,都需要调用 update_vm_cache() 函数,该操作会把调用 write 之后的 Buffer Cache 更新到 Page Cache 去。由于有这些设计上的弊端,因此在 Linux 2.4 版本之后,kernel 就将两者进行了统一,Buffer Cache 不再以独立的形式存在,而是以融合的方式存在于 Page Cache 中:
image.png
融合之后就可以统一操作 Page Cache 和 Buffer Cache:处理文件 I/O 缓存交给 Page Cache,而当底层 RAW device 刷新数据时以 Buffer Cache 的块单位来实际处理。
6. Linux I/O
Linux I/O 原理和 Zero-copy 技术全面揭秘
image.png
I/O 模式
在 Linux 或者其他 Unix-like 操作系统里,I/O 模式一般有三种:
程序控制 I/O
中断驱动 I/O
DMA I/O
1.程序控制 I/O
这是最简单的一种 I/O 模式,也叫忙等待或者轮询:用户通过发起一个系统调用,陷入内核态,内核将系统调用翻译成一个对应设备驱动程序的过程调用,接着设备驱动程序会启动 I/O 不断循环去检查该设备,看看是否已经就绪,一般通过返回码来表示,I/O 结束之后,设备驱动程序会把数据送到指定的地方并返回,切回用户态。
比如发起系统调用 read():
2.中断驱动 I/O
第二种 I/O 模式是利用中断来实现的:
image.png
3.DMA I/O
DMA 全称是 Direct Memory Access,也即直接存储器存取,是一种用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。整个过程无须 CPU 参与,数据直接通过 DMA 控制器进行快速地移动拷贝,节省 CPU 的资源去做其他工作。
目前,大部分的计算机都配备了 DMA 控制器,而 DMA 技术也支持大部分的外设和存储器。借助于 DMA 机制,计算机的 I/O 过程就能更加高效:
image.png
7.零拷贝
这篇文章关于Linux零拷贝理论性的东西说的比较清楚。
一文彻底揭秘linux操作系统之「零拷贝」!
零拷贝是什么?
"零拷贝"中的"拷贝"是指操作系统在I/O操作中,将数据从一个内存区域复制到另外一个内存区域,而"零"并不是指0次复制, 更多的是指在用户态和内核态之间的复制是0次。
8. 优化用户空间和内核空间数据传输的技术
如果要在实现在用户进程内处理数据(这种场景比直接转发数据更加常见)之后再发送出去的话,用户空间和内核空间的数据传输就是不可避免的,既然避无可避,那就只能选择优化了。
两种优化用户空间和内核空间数据传输的技术:
- 动态重映射与写时拷贝 (Copy-on-Write)
- 缓冲区共享 (Buffer Sharing)
8.1 动态重映射与写时拷贝 (Copy-on-Write)
写入时复制(Copy-on-write,COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
Linux的fork()采用copy on write技术
Copy On Write机制
fork创建出的子进程,与父进程共享内存空间,内存空间中的数据并不会马上复制给子进程。
当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
原理是:fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
局限性:
COW 这种零拷贝技术比较适用于那种多读少写从而使得 COW 事件发生较少的场景,因为 COW 事件所带来的系统开销要远远高于一次 CPU 拷贝所产生的。此外,在实际应用的过程中,为了避免频繁的内存映射,可以重复使用同一段内存缓冲区,因此,你不需要在只用过一次共享缓冲区之后就解除掉内存页的映射关系,而是重复循环使用,从而提升性能,不过这种内存页映射的持久化并不会减少由于页表往返移动和 TLB 冲刷所带来的系统开销,因为每次接收到 COW 事件之后对内存页而进行加锁或者解锁的时候,页面的只读标志 (read-ony) 都要被更改为 (write-only)。
8.2 缓冲区共享 (Buffer Sharing)
从前面的介绍可以看出,传统的 Linux I/O接口,都是基于复制/拷贝的:数据需要在操作系统内核空间和用户空间的缓冲区之间进行拷贝。在进行 I/O 操作之前,用户进程需要预先分配好一个内存缓冲区,使用 read() 系统调用时,内核会将从存储器或者网卡等设备读入的数据拷贝到这个用户缓冲区里;而使用 write() 系统调用时,则是把用户内存缓冲区的数据拷贝至内核缓冲区。
操作系统内核开发者们实现了一种叫 fbufs 的缓冲区共享的框架,也即快速缓冲区( Fast Buffers ),使用一个 fbuf 缓冲区作为数据传输的最小单位,使用这种技术需要调用新的操作系统 API,用户区和内核区、内核区之间的数据都必须严格地在 fbufs 这个体系下进行通信。fbufs 为每一个用户进程分配一个 buffer pool,里面会储存预分配 (也可以使用的时候再分配) 好的 buffers,这些 buffers 会被同时映射到用户内存空间和内核内存空间。fbufs 只需通过一次虚拟内存映射操作即可创建缓冲区,有效地消除那些由存储一致性维护所引发的大多数性能损耗。
用户进程/ IO 子系统通过发送一个个的 fbuf 写出数据到内核而非直接传递数据内容,相对应的,用户进程/ IO 子系统通过接收一个个的 fbuf 而从内核读入数据,这样就能减少传统的 read()/write() 系统调用带来的数据拷贝开销:
image.png
fbufs 的缺陷
共享缓冲区技术的实现需要依赖于用户进程、操作系统内核、以及 I/O 子系统 (设备驱动程序,文件系统等)之间协同工作。比如,设计得不好的用户进程容易就会修改已经发送出去的 fbuf 从而污染数据,更要命的是这种问题很难 debug。虽然这个技术的设计方案非常精彩,但是它的门槛和限制却不比前面介绍的其他技术少:首先会对操作系统 API 造成变动,需要使用新的一些 API 调用,其次还需要设备驱动程序配合改动,还有由于是内存共享,内核需要很小心谨慎地实现对这部分共享的内存进行数据保护和同步的机制,而这种并发的同步机制是非常容易出 bug 的从而又增加了内核的代码复杂度,等等。因此这一类的技术还远远没有到发展成熟和广泛应用的阶段,目前大多数的实现都还处于实验阶段。
更多关于Fast Buffers 的内容可参考这篇:
Linux 中的零拷贝技术
8.3 Buffer Sharing在Android中的使用
Android中 dma-buf 和 ION 。
https://blog.csdn.net/hexiaolong2009/article/details/102596744
dma-buf 最初的原型为 shrbuf,由 Marek Szyprowski (Samsung)于2011年8月2日首次提出,他实现了 “Buffer Sharing” 的概念验证(Proof-of-Concept),并在三星平台的 V4L2 驱动中实现了 camera 与 display 的 buffer 共享问题。该 patch 发表后,在内核社区引起了巨大反响,因为当时关于 buffer 共享问题很早就开始讨论了,但是由于内核没有现成的框架支持,导致各个厂商实现的驱动五花八门,此时急需要一个统一的框架来解决 buffer 共享问题。
于是 Sumit Semwal (Linaro) 基于 Marek Szyprowski 的 patch 重构了一套新的框架,也就是我们今天看到的 dma-buf 核心代码,它经历了社区开发人员给出的重重考验,并最终于 2012 年 2 月 merge 到了 Linux-3.3 主线版本中,这也是 dma-buf 的第一个正式版本。此后 dma-buf 被广泛应用于内核多媒体驱动开发中,尤其在 V4L2、DRM 子系统中得到了充分应用。
https://www.cnblogs.com/liuwanpeng/p/15827934.html
ION : 本质是dma-buf的再包装。
ION是基于dma-buf的。
内存管理 —— ION
http://kernel.meizu.com/memory%20management%20-%20ion.html
ION 是当前 Android 流行的内存分配管理机制,在多媒体部分中使用的最多,例如从 Camera 到 Display,从 Mediaserver 到 Surfaceflinger,都会利用 ION 进行内存分配管理。 ION 的前任是 PMEM,关于 PMEM 我在 M030/M04X 项目中有接触过,后来由于 PMEM 的一些局限性,Google 推出了 ION 来取代 PMEM,当前 ION 已经融合到 Linux 主线,被广泛使用。
https://blog.csdn.net/weixin_38328785/article/details/113649856
什么是ION?
ION 是当前 Android 流行的内存分配管理机制。
ION,最显著的特点是它可以被用户空间的进程之间或者内核空间的模块之间进行内存共享,而且这种共享可以是零拷贝的。在实际使用中,ION 和 VIDEOBUF2、DMA-BUF、V4L2 等结合的很紧密。
Non-ION
- CPU 可存取, 没特別要求, 通常会是这一类
- 此类对系统负担较轻, 因为各种技术成熟(virtual address space, CPU L1/L2/L3 cache, zram swap, .....)
ION
- 可给硬体存取 (GPU / ISP ...) , 可跨 process , 跨 kernel 进行內容分享 (zero copy )
- 因为相机资料量大, ION 用量会是 Camera memory footprint 的关注焦点
-
此类对系统的负担较重,因为往往需要一大块。
https://blog.csdn.net/weixin_38328785/article/details/113649856
ION内存分配说明文档:
https://lwn.net/Articles/480055/
这个文档说明了ION从分配到共享内存的API。
简单翻译版本:
Android ION内存分配
写的较好的版本:
Android系列-ION内存管理简介
https://www.jianshu.com/p/4f681f6ddc3b
https://www.jianshu.com/p/4f681f6ddc3b
ION是Google在Android 4.0 ICS中引入,用于改善对于当前不同的android设备,有着各种不同内存管理接口管理相应内存的状况。当前存在着各种不同的但是功能却类似的内存管理接口,例如在NVIDIA Tegra有一个“NVMAP”机制、在TI OMAP有一个“CMEM”机制、在Qualcomm MSM有一个“PMEM”机制,ION将其进行通用化,通过其接口,可集中分配各类不同内存(heap),同时上述三个芯片厂商也正将其内存管理策略切换至ION上。
另外,ION在内核空间和用户空间分别有一套接口,它不仅能管理内存,还可在其clients(来自内核的或者来自用户空间的)之间共享内存。
综上,ION主要功能:
1.内存管理器:提供通用的内存管理接口,通过heap管理各种类型的内存。
2.共享内存:可提供驱动之间、用户进程之间、内核空间和用户空间之间的共享内存。
每个heap中可分配若干个buffer,每个client通过handle管理对应的buffer。每个buffer只能有一个handle对应,每个用户进程只能有一个client,每个client可能有多个handle。两个client通过文件描述符fd(和handle有所对应,通过handle获取),通过映射方式,将相应内存映射,实现共享内存(获取到共享文件描述符fd后,共享进程可以通过mmap来操作共享内存。)。
在 Android 12 中,GKI 2.0 将 ION 分配器替换为 DMA-BUF 堆。
官方文档:
从 ION 转换到 DMA-BUF 堆
参考链接:
Linux 中的零拷贝技术
1. linux视频相关内存机制概述
Linux I/O 原理和 Zero-copy 技术全面揭秘
一文彻底揭秘linux操作系统之「零拷贝」!
linux内存管理(详解)
Linux内核Page Cache和Buffer Cache关系及演化历史
深入理解Linux 的Page Cache
深入理解零拷贝
网友评论