Linux内存是后台开发人员需要深入了解的计算机资源。合理使用内存有助于提高机器的性能和稳定性。主要介绍了Linux内存的组织结构和页面布局,内存碎片产生的原因和优化算法,Linux内核的几种内存管理方法,内存使用场景和那些内存使用的坑。

从内存的原理和结构,到内存的算法优化,再到使用场景,探索内存管理的机理和奥秘。

一、到Linux内存中

1、什么是内存?1)内存又称主存储器,是CPU可以直接寻址的存储空间,由半导体器件构成。2)内存的特点是存取速度快。

字体记忆的功能

1)临时存储cpu的运行数据。

2)通过硬盘等外部存储器交换的数据。

3)保证cpu计算的稳定性和高性能。

二、 Linux内存地址空间

1、Linux内存地址空间Linux内存管理全景

2、内存地址——用户模式内核模式

用户模式:Ring3在用户模式下运行的代码会受到很多处理器的影响。

内核状态:Ring0是处理器内存保护中的内核状态。

从用户态切换到内核态的三种方式:系统调用、异常和外设中断。

区别:每个进程都有自己的、独立的、不受干扰的内存空间;用户态程序不能随意操作内核地址空间,具有一定的安全保护功能;内核状态线程共享内核地址空间;

3、存储器地址——MMU地址转换

MMU是一种硬件电路,包含两个部分,一个是分段部分,一个是分页部分。

分段机制将逻辑地址转换成线性地址。

分页机制将线性地址转换成物理地址。

4、存储器地址——分段机制1)段选择器

为了快速检索段选择器,处理器提供了六个段寄存器来缓冲段选择器,它们是:cs、ss、ds、es、fs和gs。

段的基址:段在线性地址空间中的起始地址。

段限制:在虚拟地址空间的一个段中可以使用的最大偏移量。

2)分段实施

逻辑地址的段寄存器中的值提供段描述符,然后从段描述符中得到段基址和段边界,再加上逻辑地址的偏移量得到线性地址。

5、内存地址—— (32位)的分页机制

分页机制是在分段机制之后进行的,分段机制进一步将线性地址转化为物理地址。

10位页面目录、10位页面表条目、12位页面偏移地址

单页的大小是4KB。

6、用户模式地址空间

TEXT:代码段可执行代码,字符串文字,只读变量。

数据:数据段,映射器中已初始化的全局变量。

BSS段:保存程序中未初始化的全局变量。

heap:运行时的HEAP,它使用程序运行期间malloc请求的内存区域。

MMAP:共享库和匿名文件的映射区域。

堆栈:用户进程堆栈

7、内核地址空间

直接映射区:线性空间中距离3G最大间隔896M,为直接内存映射区。

动态内存映射区:这个区域由内核函数vmalloc分配。

永久内存映射区:这个区域可以访问高端内存。

固定映射区域:这个区域和4G的顶部只有一个4k的隔离区,每个地址项都服务于一个特定的用途,比如ACPI_BASE。

8、进程内存空间

通常,用户进程只能访问用户空间的虚拟地址,而不能访问内核空间的虚拟地址。

内核空间由内核映射,不会随进程而改变;内核空间地址有自己对应的页表,用户进程有不同的页表。

三、 Linux内存分配算法

内存管理算法:对于那些讨厌管理自己内存的人来说,这简直是天赐之物。1、内存碎片1)基础知识

原因:内存分配小,这些分配小的内存生命周期长,重复应用后会出现内存碎片。

优点:提高分配速度,方便内存管理,防止内存泄漏。

缺点:大量内存碎片会让系统变慢,内存利用率低,浪费大。

2)如何避免内存碎片

使用较少的动态内存分配函数(尽可能使用堆栈空间)

分配的内存和释放的内存应该尽可能在同一个函数中。

尽量一次性申请更大的内存,而不是反复申请更小的内存。

尽可能申请2的指数幂的大块内存空间。

外部碎片避免——伙伴系统算法

内部碎片避免——slab算法

管理自己的内存,设计内存池。

2、合伙人制度算法——组织架构1)概念

为内核分配一组连续的页面提供了高效的分配策略,有效解决了外部碎片问题。

分配的内存区域是基于页帧的。

2)外部碎片

外部碎片(External fragmentation)是指尚未分配(不属于任何进程),但太小而无法分配给申请内存空间的新进程的内存空闲区域。3)组织结构。

所有空闲页被分组为11个块链表,每个块链表分别包含大小为1、2、4、8、16、32、64、128、256、512和1024个连续页帧的页块。最多可以申请1024连续页,对应4MB连续内存。

3、合作伙伴系统算法——应用和恢复1)应用算法

申请2 I页块存储空间,如果2 I对应的块链表中有空闲页块,就分配给应用。

如果没有空闲页块,则搜索2^(i 1)对应的块链表中是否有空闲页块,如果有,则将2 i块链表节点赋给应用,并将另外2 i块链表节点插入2 i对应的块链表中

如果在2^(i 1)块链表中没有空闲页块,重复步骤2,直到找到具有空闲页块的块链表。

如果仍然不是,则内存分配失败。

2)恢复算法

释放2 I页块的存储空间,搜索2 I页块对应的块链表,看是否有物理地址连续的页块。如果不是,就没有必要合并它们。

如果有,它将被合并到2^(i 1)页面块,以此类推,并继续寻找下一级块链接,直到它不能被合并。

3)条件

这两个块大小相同。

它们的物理地址是连续的。

页面块大小相同

4、 4M以上内存如何分配?1)为什么块内存分配受到限制?

分配的内存越大,失败的可能性就越大。

使用大块内存的情况很少。

2)内核获取4M以上大内存的方法。

修改MAX_ORDER,重新编译内核。

内核启动选择,传递' mem='参数,比如' mem=80M,预留一些内存;然后通过

Request_mem_region和ioremap_nocache将保留的内存映射到模块中。需要修改内核启动参数,而无需重新编译内核。但是这种方式不支持x86架构,只支持ARM ARM、PowerPC等非x86架构。

在start_kernel中mem_init函数之前调用alloc_boot_mem函数来预分配大块内存,需要重新编译内核。

Vmalloc函数,内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存。

5、合作伙伴系统——防碎片机制1)不可移动页面

这些页面在内存中有固定的位置,不能移动或回收。

内核代码段、数据段、来自内核kmalloc()的内存、内核线程占用的内存等。

2)可回收页面

这些页面不能移动,但可以删除。当回收的页面占用太多内存或内存不足时,内核会回收页面。3)可移动页面。

这些页面可以随意移动,用户空间应用程序使用的所有页面都属于这一类。它们通过页表进行映射。

当它们被移动到新的位置时,页表条目将相应地更新。

6、slab算法——基本原理1)基本概念

Linux中使用的Slab分配器是基于Jeff Bonwick首先为SunOS操作系统引入的算法。

其基本思想是将内核中经常使用的对象放入缓存中,并由系统保持在初始可用状态。例如,进程描述符,它经常在内核中被申请和释放。

2)内部碎片

分配的内存空间大于所需的内存空间

缓存经常使用的对象,以减少分配、初始化和释放对象的时间成本。

通过着色技术调整对象,更好地利用硬件缓存。

7、板坯分配器结构

由于对象是从板中分配和释放的,因此单个板可以在板列表之间移动。

slabs _ empty列表中的板是回收的主要候选对象。

Slab还支持公共对象的初始化,从而避免了为同一目的重复初始化一个对象。

8、实验室缓存1)普通缓存

片分配器提供的小块连续内存的分配是通过通用缓存实现的。

通用高速缓存提供的对象具有从32到131072字节的几何分布大小。

内核分别为内存申请和释放提供了kmalloc()和kfree()接口。

2)专用缓存

内核为私有缓存的申请和释放提供了完整的接口,并根据传递的参数为特定对象分配slab缓存。

Kmem_cache_create()用于为指定的对象创建缓存。它从cache_cache普通缓存中分配一个缓存描述符给新的专有缓存,并将这个描述符插入到缓存描述符形成的cache_chain链表中。

Kmem_cache_alloc()在由其参数指定的缓存中分配一个片。相反,kmem_cache_free()在其参数指定的缓存中释放一个slab。

9、内核内存池1)基本原则

首先申请分配一定数量大小相等(一般情况下)的内存块备用。

当有新的内存需求时,会从内存池中分离出一部分内存块,如果内存块不够,我们会继续申请新的内存。

这样做的一个显著优点是尽可能避免内存碎片,提高了内存分配的效率。

2)内核API

创建一个内存池对象。

Mempool_alloc分配函数获取对象。

Mempool_free释放一个对象。

销毁内存池。

10、用户模式内存池1) C实例

11、DMA内存1)什么是DMA?

直接内存访问是一种硬件机制,它允许外围设备和主内存直接传输它们的I/O数据,而不需要系统处理器的参与。DMA控制器的功能。

可以向CPU发送系统保持信号,并提出总线接管请求。

当CPU发出允许接管的信号时,负责控制总线,进入DMA模式。

可以对内存进行寻址,修改地址指针,实现对内存的读写操作。

可以确定本次DMA传输的字节数,判断DMA传输是否结束。

发出DMA结束信号,使CPU恢复正常工作状态。

2) DMA信号

DREQ:DMA请求信号。它是外设对DMA控制器的请求,也是DMA操作的应用信号。

DACK:DMA响应信号。这是一个信号,DMA控制器向外设表明DMA请求已经收到并正在处理。

HRQ:由DMA控制器发送给CPU以接管总线的DMA请求信号。

hlda:CPU发送给DMA控制器的信号,以及允许接管总线的响应信号;

四、内存使用情况

内存不足的时代结束了吗?不行,不管你有多少内存,都不能随意使用。1、内存使用情况

页面管理

Slab(kmalloc,内存池)

用户模式内存使用(malloc、relloc文件映射、共享内存)

程序内存映射(堆栈、堆、代码、数据)

内核和用户模式下的数据传输(复制自用户、复制至用户)

存储器映射(硬件寄存器、保留存储器)

DMA存储器

2、用户模式内存分配功能

Alloca从堆栈中请求内存,所以不需要释放它。

malloc分配的内存空间没有初始化,使用malloc()函数的程序刚开始可以正常运行(内存空间没有被重新分配),但是过了一段时间(内存空间被重新分配)就可能出现问题。

Calloc将分配的内存空间中的每一位初始化为零。

Realloc扩展了现有的内存空间大小。

a)如果当前的连续内存块足够重分配,只需扩展P指向的空间并返回P的指针地址.这时Q和P指向同一个地址。

b)如果当前连续内存块不够长,找一个足够长的地方,分配一个新的内存q,将p指向的内容复制到q,返回q.并删除p指向的内存空间。

3、内核内存分配函数函数分配原理最大内存Others _get_free_pages直接对页框进行操作。4MB适合分配大量的连续物理内存。kmem_cache_alloc是基于slab机制实现的。128KB适合在频繁申请释放相同大小的内存块时使用kmalloc。使用kmem_cache_all。Oc实现了最常见的128KB的分配方式。当需要小于页帧大小的内存时,可以使用vmalloc来建立不连续物理内存到虚拟地址的映射。物理不连续适用于大内存。但如果对地址连续性没有要求,dma_alloc_coherent基于_alloc_pages实现4MB,适合dma操作。ioremap实现了从已知物理地址到虚拟地址的映射,适用于物理地址已知的情况。比如设备驱动alloc_bootmem启动内核的时候,预留了一段内存,内核看不出来它比物理内存大小小,所以内存管理要求高4、malloc申请内存。

当调用malloc函数时,它会沿着free_chuck_list连接表寻找一个足够大的内存块来满足用户的请求。

free_chuck_list连接表的主要工作是维护一个空闲堆空间缓冲区链表。

如果在空间缓冲区的链表中没有找到对应的节点,则需要通过系统调用sys_brk来扩展进程的堆栈空间。

5、页面缺失异常

通过get_free_pages申请一个或多个物理页面。

翻译进程pdg映射中addr所在的pte地址。

将addr对应的pte设置为物理页面的第一个地址。

系统调用:Brk—-应用内存小于等于128kb,do _ map—-应用内存大于128kb。

6、用户进程访问内存分析

用户态进程独占虚拟地址空间,两个进程的虚拟地址可以相同。

当访问用户模式虚拟地址空间时,如果没有映射物理地址,则通过系统调用发出缺页异常。

内核捕获缺页异常,分配物理地址空间,用用户态虚拟地址建立映射。

7、共享内存1)原理

它允许多个不相关的进程访问逻辑内存的同一部分。

共享内存将是在两个正在运行的进程之间传输数据的有效解决方案。

在两个正在运行的进程间共享数据是进程间通信的一种高效方法,可以有效减少数据副本的数量。

2) Shm接口

Shmget创建共享内存

Shmat启动对共享内存的访问,并将共享内存连接到当前进程的地址空间。

Shmdt将共享内存与当前进程分开

五、内存用什么坑?

1、C内存泄漏

调用new和delete函数时,类的构造函数和析构函数不匹配。

嵌套对象指针未被正确清除。

基类的析构函数没有定义为虚函数。

当基类的指针指向子类对象时,如果基类的析构函数不是虚的,那么子类的析构函数就不会被调用,子类的资源就不会被正确释放,从而造成内存泄漏。

缺少复制构造函数,按值传递会调用(复制)构造函数,按引用传递不会调用。

指向对象的指针数组不同于对象数组。数组包含指向对象的指针,不仅释放了每个对象的空间,还释放了每个指针的空间。

没有重载的赋值操作符,对象是一个一个复制的。如果这个类的大小是可变的,结果就是内存泄漏。

2、C字段指针

指针变量未初始化。

释放或删除后,指针未设置为空。

指针操作超出了变量的范围,比如返回一个指向堆栈内存的指针就是一个野指针。

访问空指针(需要短期判断)

Sizeof无法获取数组的大小。

试图修改常数,如char p=' 1234p=' 1

3、C资源访问冲突

多线程共享变量没有用valotile修饰。

对全局变量的多线程访问没有被锁定。

全局变量仅对单个进程有效。

多进程在不同步的情况下写入共享内存数据。

Mmap存储器映射,多

添加元素(插入/推回等。)和删除元素会导致顺序容器迭代器失败。

错误示例:删除当前迭代器,迭代器将失效。

正确的例子:当迭代器擦除时,需要保存下一个迭代器。

5、C 11智能指针

Auto_ptr替换为unique_ptr。

使用make_shared初始化shared_ptr。

weak_ptr智能指针助手原理分析(1);

(2)数据结构:

(3)用法:a. lock()获取被管对象的强引用指针b. expired()检测被管对象是否释放c. get()访问智能指针对象。

6、C 11更小、更快、更安全

原子数据类型多线程安全性

std:array定长数组的开销小于array。与std:vector不同,数组的长度是固定的,不能动态扩展。

Std:vector vector瘦身shrink_to_fit():将容量减少到与size()相同的大小。

td:forward_list

Forward_list是单链表(std:list是双链表)。当只需要顺序遍历时,forward_list可以节省更多内存,插入和删除的性能高于list。

Std:unordered_map,std:unordered_set一个由hash实现的无序容器,插入、删除、搜索的时间复杂度为O(1)。如果不注意容器中元素的顺序,使用无序容器可以获得更高的性能六、如何检查内存?

系统中的内存使用情况:/proc/meminfo

进程的内存使用情况:/proc/28040/status

总查询内存使用量:空闲

查询过程中cpu和内存使用的比例:top

虚拟内存统计信息:vmstat

内存消耗率和进程排序:psauxsort-RSS

释放系统内存缓存:/proc/sys/vm/drop_caches。

要释放pagecache,请使用echo 1/proc/sys/VM/drop _ cache

要释放dentries和inodes,请使用echo 2/proc/sys/VM/drop _ cache

要释放pagecache、dentries和inodes,请使用echo 3/proc/sys/VM/drop _ cache

审核编辑:郭婷