c++常用知识点复盘

一、Linux内存管理机制

内存全貌图:
图片.png
 
 
Linux内存分为用户态和内核态两种,以32位4G的Linux内存为例进行说明, 其区别如下:
  • 用户态:Ring3 运行于用户态的代码则要受到处理器的诸多限制
  • 内核态:Ring0 在处理器的存储保护中,核心态
  • 用户态切换到内核态的 3 种方式:系统调用、异常、外设中断
  • 区别:每个进程都有完全属于自己的,独立的,不被干扰的内存空间;用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用;内核态线程共享内核地址空间;
图片.png
 
 

1、内核空间

内核态空间的结构示意图如图所示:
图片.png
 
 
  • 直接映射区:线性空间中层 3G 开始最大 896M 的区间,为直接内存映射区
  • 动态内存映射区:该区域由内核函数 vmalloc 来分配
  • 永久内存映射区:该区域可访问高端内存
  • 固定映射区:该区域和 4G 的顶端只有 4k 的隔离带,其每个地址项都服务于特定的用途,如:ACPI_BASE 等

页(page)是内核的内存管理基本单位。其与物理地址的映射方式如下:
图片.png
 
struct page {
 unsigned long flags;      // 页标志符  
 struct address_space *mapping; // 该页所在地址空间描述符结构指针 
 atomic_t _mapcount; // 页映射计数
} _struct_page_alignment;
  • flags:页标志包含是不是脏的,是否被锁定等等,每一位单独表示一种状态,可同时表示出32种不同状态,定义在<linux/page-flags.h>。
  • _mapcount:计数值为-1表示未被使用。
  • mapping:页在虚拟内存中的地址,对于不能永久映射到内核空间的内存(比如高端内存),该值为NULL;需要时必须动态映射这些内存。
尽管处理器的最小可寻址单位通常为字或字节,但内存管理单元(MMU,把虚拟地址转换为物理地址的硬件设备)通常以页为单位处理。内核用struct page结构体表示每个物理页,struct page结构体占40个字节,假定系统物理页大小为4KB,对于4GB物理内存,1M个页面,故所有的页面page结构体共占有内存大小为40MB,相对系统4G,这个代价并不高。

内核把页划分在不同的区(zone)总共3个区,具体如下:
 
   
描述
物理内存(MB)
ZONE_DMA
DMA使用的页
<16
ZONE_NORMAL
可正常寻址的页
16~896
ZONE_HIGHMEM
动态映射的页
>896

页面的分配与释放

页面的分配
图片.png
 
 
页面的释放
图片.png
 
 

字节分配与释放

kmalloc、vmalloc都是以字节位单位分配和释放内存

1、kmalloc

void * kmalloc(size_t size, gfp_t flags)
  • 该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存在物理内存中连续且保持原有的数据(不清零)
  • 其中部分flags取值说明:
  1. GFP_USER: 用于用户空间的分配内存,可能休眠;
  2. GFP_KERNEL:用于内核空间的内存分配,可能休眠;
  3. GFP_ATOMIC:用于原子性的内存分配,不会休眠;典型原子性场景有中断处理程序,软中断,tasklet等
  • kmalloc内存分配最终总是调用__get_free_pages 来进行实际的分配,故前缀都是GFP_开头。 kmalloc分最多只能分配32个page大小的内存,每个page=4k,也就是128K大小,其中16个字节用来记录页描述结构。
  • kmalloc分配的是常驻内存,不会被交换到文件中。最小分配单位是32或64字节。
  •  kzalloc() 等价于先用  kmalloc()  申请空间, 再用 memset() 来初始化,所有申请的元素都被初始化为0。 
static inline void *kzalloc(size_t size, gfp_t flags)
{
    return kmalloc(size, flags | __GFP_ZERO); //通过或标志位__GFP_ZERO,初始化元素为0
}
 

2、valloc

void * vmalloc(unsigned long size)

 

  • 该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存是逻辑上连续的。kmalloc不同,该函数乜有flags,默认是可以休眠的。
区别
图片.png

2、用户空间

用户空间中进程的内存,往往称为进程地址空间。Linux采用虚拟内存技术。

地址空间和内存区域

每个进程都有一个32位或64位的地址空间,取决于体系结构。 一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,也彼此互不相干,对于这种共享地址空间的进程称之为线程。一个进程可寻址4GB的虚拟内存(32位地址空间中),但不是所有虚拟地址都有权访问。对于进程可访问的地址空间称为内存区域。每个内存区域都具有对相关进程的可读、可写、可执行属性等相关权限设置。
  • 内存区域可包含的对象:
  1. text:代码段可执行代码、字符串字面值、只读变量
  2. data:数据段,映射程序中已经初始化的全局变量
  3. bss:存放程序中未初始化的全局变量
  4. heap:运行时的堆,在程序运行中使用 malloc 申请的内存区域
  5. mmap:共享库及匿名文件的映射区域
  6. stack:用户进程栈
这些内存区域不能相互覆盖,每一个进程都有不同的内存片段。
图片.png
 
 

进程的内存空间

  • 用户进程通常情况只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址
  • 内核空间是由内核负责映射,不会跟着进程变化;内核空间地址有自己对应的页表,用户进程各自有不同的页表
图片.png
 
 

内存管理技术

内存碎片

产生原因:内存分配较小,并且分配的这些小的内存生存周期又较长,反复申请后将产生内存碎片的出现
优点:提高分配速度,便于内存管理,防止内存泄露
缺点:大量的内存碎片会使系统缓慢,内存使用率低,浪费大

避免内存碎片的方式

  • 少用动态内存分配的函数(尽量使用栈空间)
  • 分配的内存和释放的内存尽量在同一个函数中
  • 尽量一次性申请较大的内存,而不要反复申请小内存
  • 尽可能申请大块的 2 的指数幂大小的内存空间
  • 外部碎片避免——伙伴系统算法
  • 内部碎片避免——slab 算法
  • 自己进行内存管理工作,设计内存池
 

内存池

基本原理

先申请分配一定数量的、大小相等(一般情况下) 的内存块留作备用
当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存
这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升

内核API

  • mempool_create 创建内存池对象
  • mempool_alloc 分配函数获得该对象
  • mempool_free 释放一个对象
  • mempool_destroy 销毁内存池
图片.png
 
 
 

共享内存

原理

它允许多个不相关的进程去访问同一部分逻辑内存
两个运行中的进程之间传输数据,共享内存将是一种效率极高的解决方案
两个运行中的进程共享数据,是进程间通信的高效方法,可有效减少数据拷贝的次数
图片.png
 
 

API

shm 接口
  • shmget 创建共享内存
  • shmat 启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间
  • shmdt 将共享内存从当前进程中分离

C++内存问题

内存泄漏的原因

  1. new和delete没有正确的成对使用,在类中的构造和析构中没有分别使用
  2. 如果对象内有嵌套的指针结构,没有依次释放嵌套内部的指针内存
  3. 基类的析构函数没有定义位virtual,导致子类资源不能有效释放
  4. 缺少拷贝构造函数,当按值传递的时候会出现隐形拷贝,造成数据未释放
  5. 使用对象的指针数组时,未依次释放每个对象的资源
 

野指针

“野指针”不是NULL指针,是指向“垃圾”内存的指针,即是指向不可用内存区域的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。
  1. 指针变量未初始化
  2. 指针被free或delete后没有置为null
  3. 指针指向超出变量的作用域范围,如返回指向栈内存指针
  4. 访问了空指针
 

资源访问冲突

  • 多线程共享变量没有使用volatile关键字:
 valotile的作用,它告诉编译器,不要把变量优化到寄存器中。在开发多线程并发的软件时,如果这些线程共享一些全局变量,这些全局变量最好用valotile修饰。这样可以避免因为编译器优化而引起的错误,这样的错误非常难查。
  • 全局变量仅对单进程有效,多线程访问全局变量未加锁
  • 多进程写共享内存数据,未做同步处理
  • mmap 内存映射,多进程不安全
 
 
 
 
posted @ 2023-02-07 17:20  AI小码过河  阅读(75)  评论(0)    收藏  举报
Live2D