湖南网站建设磐石网络,wordpress 物流插件,百度商家版下载,免费下载微信2023目录
一、Linux环境下的内存管理
二、栈的管理
三、堆内存管理
四、mmap映射区
五、内存泄漏与防范
六、常见的内存错误及检测 C程序中定义的函数、全局变量、静态变量经过编译链接后#xff0c;分别以section的形式存储在可执行文件的代码段、数据段和BSS段中。当程序运…目录
一、Linux环境下的内存管理
二、栈的管理
三、堆内存管理
四、mmap映射区
五、内存泄漏与防范
六、常见的内存错误及检测 C程序中定义的函数、全局变量、静态变量经过编译链接后分别以section的形式存储在可执行文件的代码段、数据段和BSS段中。当程序运行时可执行文件首先被加载到内存中各个section分别加载到内存中对应的代码段、数据段和BSS段中。需要动态链接的动态库也被加载到内存中完成代码的链接和重定位操作以保证程序的正常运行。一个可执行文件被加载到内存中运行时它在内存空间的分布如下图所示。 一、Linux环境下的内存管理 在Linux环境下运行的程序在编译时链接的起始地址都是相同的而且是一个虚拟地址。Linux操作系统需要CPU内存管理单元的支持才能运行Linux内核通过页表和MMU硬件来管理内存完成虚拟地址到物理地址的转换、内存读写权限管理等功能。 可执行文件在运行时加载器将可执行文件中的不同section加载到内存中读写权限不同的区域如代码段、数据段、.bss段、.rodata段等。 计算机上运行的程序主要分为两种操作系统和应用程序。每一个应用程序进程都有4GB大小的虚拟地址空间。为了系统的安全稳定04GB的虚拟地址空间一般分为两部分用户空间和内核空间。03GB地址空间给应用程序使用而操作系统一般运行在34GB内核空间。通过内存权限管理应用程序没有权限访问内核空间只能通过中断或系统调用来访问内核空间这在一定程度上保障了操作系统核心代码的稳定运行。 在Linux环境下虽然所有的程序编译时使用相同的链接地址但在程序运行时相同的虚拟地址会通过MMU转换映射到不同的物理内存区域各个可执行文件被加载到内存不同的物理页上。如下图所示每个进程都有各自的页表用来记录各自进程中虚拟地址到物理地址的映射关系。 二、栈的管理 栈有两种基本操作入栈push和出栈pop。入栈是把一个栈元素压入栈中而出栈则是从栈中弹出一个栈元素。入栈和出栈都靠栈指针Stack PointerSP来维护SP会随着入栈和出栈在栈顶上下移动。如下图所示根据栈指针SP指向栈顶元素的不同栈可分为满栈和空栈根据栈的生长方向不同栈又分为递增栈和递减栈。 满栈的栈指针SP总是指向栈顶元素而空栈的栈指针则指向栈顶元素上方的可用空间一个栈元素入栈时递增栈的栈指针从低地址往高地址增长而递减栈的栈指针则从高地址往低地址增长。 在栈的初始化过程中栈在内存中的起始地址还是有点讲究的。ARM处理器使用的是满递减栈在Linux环境下栈的起始地址一般就是进程用户空间的最高地址紧挨着内核空间栈指针从高地址往低地址增长。为了防止黑客栈溢出攻击新版本的Linux内核一般会将栈的起始地址设置成随机的如下图所示每次程序运行栈的初始化起始地址都会基于用户空间的最高地址有一个随机的偏移每次栈的起始地址都不一样。 Linux默认给每一个用户进程栈分配8MB大小的空间。栈的容量如果设置得过大则会增加内存开销和启动时间如果设置得过小则程序超出栈设置的内存空间又容易发生栈溢出Stack Overflow产生段错误。 在设置栈大小时我们要根据程序中的变量、数组对栈空间的实际需求设置合理的栈大小。用户在编写程序时为了防止栈溢出可以参考下面的一些原则。 ● 尽量不要在函数内使用大数组如果确实需要大块内存则可以使用malloc申请动态内存。 ● 函数的嵌套层数不宜过深。 ● 递归的层数不宜太深。 全局变量定义在函数体外其作用域范围为从声明处到文件结束。其他文件如果想使用这个全局变量则在自己的文件内使用extern声明后即可使用。全局变量的生命周期在整个程序运行期间都是有效的。 局部变量定义在函数内其作用域只能在函数体内使用。函数只有在被调用的时候才会在内存中开辟一个栈帧空间在这个栈空间里存储局部变量及传进来的函数实参等。函数调用结束这个栈帧空间就被销毁释放了变量也就随之消失因此局部变量的生命周期仅仅存在于函数运行期间。每一次函数被调用临时开辟的栈帧空间可能不相同局部变量的地址也不相同。 编译器在编译程序时其实是根据一对大括号{}来限定一个变量的作用域的示例
#include iostreamvoid functionWithStatic() {static int staticVar 0; // 带有 static 修饰的局部变量int normalVar 0; // 普通局部变量staticVar;normalVar;std::cout Static Variable: staticVar std::endl;std::cout Normal Variable: normalVar std::endl;
}int main() {for (int i 0; i 3; i) {functionWithStatic();}return 0;
}运行结果 最后我们对变量的作用域做下小结。全局变量的作用域如下。 ● 全局变量的作用域由文件来限定。 ● 可使用extern进行扩展被其他文件引用。 ● 也可以使用static进行限制只能在本文件中被引用。局部变量的作用域如下。 ● 局部变量的作用域由{}限定。 ● 可以使用static修饰局部变量来改变它们的存储属性生命周期但不能改变其作用域。 三、堆内存管理 我们使用malloc()/free()函数申请/释放的动态内存就属于堆内存如下图所示堆是Linux进程空间中一片可动态扩展或缩减的内存区域一般位于BSS段的后面。 堆内存与栈相比有相同点也有区别。 ● 堆内存是匿名的不能像变量那样使用名字直接访问一般通过指针间接访问。 ● 在函数运行期间对函数栈帧内的内存访问也不能像变量那样通过变量名直接访问一般通过栈指针FP或SP相对寻址访问。 ● 堆内存由程序员自己申请和释放函数退出时如果程序员没有主动释放就会造成内存泄漏。 ● 栈内存由编译器维护函数运行时开辟一个栈帧空间函数运行结束栈帧空间随之销毁释放。 malloc()/free()函数的底层实现其实就是通过系统调用brk向内核的内存管理系统申请内存。内核批准后就会在BSS段的后面留出一片内存空间允许用户进行读写操作。申请的内存使用完毕后要通过free()函数释放free()函数的底层实现也是通过系统调用来归还这块内存的。 当用户要申请的内存比较大时如大于128KB一般会通过mmap系统调用直接映射一片内存使用结束后再通过ummap系统调用归还这块内存。mmap区域是Linux进程中比较特殊的一块区域主要用于程序运行时动态共享库的加载和mmap文件映射。早期的Linux内核将该区域设置在0x40000000附近Linux 2.6以后的内核将该区域移到了栈附近打印mmap映射区域的地址你会发现大部分地址都在0xBxxxxxxx范围内紧挨着进程的用户栈。
示例代码 运行结果 根据程序的打印结果我们可以看到对于用户申请的小块内存Linux内存管理子系统会在BSS段的后面批准一块内存给用户使用。当用户申请的内存大于128KB时Linux系统则通过mmap系统调用映射一片内存给用户使用映射区域在用户进程栈附近。两次申请的不同大小 的内存其地址分别位于内存中两个不同的区域heap区和mmap区。 让a.out进程先不退出一直死循环运行以方便我们通过cat命令查看a.out进程的内存布局。 在32位X86平台下我们可以看到heap区域在.bss段的后面而mmap区域则紧挨着stackmmap区域包括进程动态链接时加载到内存的动态链接器ld-2.23.so、动态共享库、使用mmap申请的动态内存。 使用kill命令杀掉a.out进程再重新运行你会发现mem_100和mem_256K的地址打印值发生了变化每次程序运行的地址可能都不相同。这是因为heap区和mmap区的起始地址和stack一样也不是固定不变的。为了防止黑客攻击每次程序运行时它们都会以一个随机偏移作为起始地址。 通过a.out进程的内存布局我们看到栈的起始地址并不紧挨着内核空间0xc0000000而是从0xbf9a2000作为起始地址中间有一个大约6MB的偏移。heap区也不紧挨着.bss段它们之间也有一个offsetmmap区也是如此它和stack区之间也有一个offset。 大量的系统调用会让处理器和操作系统在不同的工作模式之间来回切换操作系统要在用户态和内核态之间来回切换CPU要在普通模式和特权模式之间来回切换每一次切换都意味着各种上下文环境的保存和恢复频繁地系统调用会降低系统的性能。系统调用还有一个不人性化的地方是不支持任意大小的内存分配有的平台甚至只支持一个或数倍物理页大小的内存申请这在一定程度上会造成内存的浪费。举个通俗的例子内存申请有点类似你去银行存取款。当你需要用钱时如果每次都是用多少取多少用1块取1块用10块取10块那么估计你天天都得往银行跑花费在交通、排队上的时间开销无疑是巨大的。同样的道理当你往银行存钱时如果只要口袋里有钱就往银行里存有1块存1块有10块存10块那么估计你也得天天往银行跑。正确的做法应该是每次取钱时多取一些放到自己的钱包里可以多次使用存钱时也是如此先存放到钱包里等攒够了一定数额再存到银行里这样就可以大大减少去银行的次数。 为了提高内存申请效率减少系统调用带来的开销我们可以参考上面的钱包模式在用户空间层面对堆内存介入管理。如在glibc中实现的内存分配器allocator可以直接对堆内存进行维护和管理。如图5-27所示内存分配器通过系统调用brk()/mmap()向Linux内存管理子系统“批发”内存同时实现了malloc()/free()等API函数给用户使用满足用户动态内存的申请与释放请求。 当用户使用free()释放内存时释放的内存并不会立即返回给内核而是被内存分配器接收缓存在用户空间。内存分配器将这些内存块通过链表收集起来等下次有用户再去申请内存时可以直接从链表上查找合适大小的内存块给用户使用如果缓存的内存不够用再通过brk()系统调用去内核“批发”内存。内存分配器相当于一个内存池缓存通过这种操作方式大大减少了系统调用的次数从而提升了程序申请内存的效率提高了系统的整体性能。
四、mmap映射区 当用户使用malloc申请大于128KB的堆内存时内存分配器会通过mmap系统调用在Linux进程虚拟空间中直接映射一片内存给用户使用。 当我们运行一个程序时需要从磁盘上将该可执行文件加载到内存。将文件加载到内存有两种常用的操作方法一种是通过常规的文件I/O操作如read/write等系统调用接口一种是使用mmap系统调用将文件映射到进程的虚拟空间然后直接对这片映射区域读写即可。 文件I/O操作使用文件的API函数open、read、write、close对文件进行打开和读写操作。文件存储于磁盘中我们通过指定的文件名打开一个文件就会得到一个文件描述符通过该文件描述符就可以找到该文件的索引节点inode根据inode就可以找到该文件在磁盘上的存储位置。然后我们就可以直接调用read()/write()函数到磁盘指定的位置读写数据了。文件的读写流程如图5-34左侧图所示。 磁盘属于机械设备程序每次读写磁盘都要经过转动磁盘、磁头定位等操作读写速度较慢。为了提高读写效率减少I/O读盘次数以保护磁盘Linux内核基于程序的局部原理提供了一种磁盘缓冲机制。如图5-34所示在内存中以物理页为单位缓存磁盘上的普通文件或块设备文件。当应用程序读磁盘文件时会先到缓存中看数据是否存在若数据存在就直接读取并复制到用户空间若不存在则先将磁盘数据读取到页缓存page cache中然后从页缓存中复制数据到用户空间的buf中。当应用程序写数据到磁盘文件时会先将用户空间buf中的数据写入page cache当page cache中缓存的数据达到设定的阈值或者刷新时间超时Linux内核会将这些数据回写到磁盘中。 当动态库第一次被链接器加载到内存参与动态链接时如图5-43所示动态库映射到了当前进程虚拟空间的mmap区域动态链接和重定位结束后程序就开始运行。当程序访问mmap映射区域去调用动态库的一些函数时发现此时还没有为这片虚拟空间分配物理内存就会产生一个请页异常。内核接着会为这片映射内存区域分配物理内存将动态库文件libtest.so加载到物理内存并将虚拟地址和物理地址之间的映射关系更新到进程的页表项此时动态库才真正加载到物理内存程序才可以正常运行。 对于已经加载到物理内存的文件Linux内核会通过一个radix tree的树结构来管理这些页缓存对象。在图5-44中当进程B运行也需要加载动态库libtest.so时动态链接器会将库文件libtest.so映射到进程B的一片虚拟内存空间上链接重定位完成后进程B开始运行。当通过映射内存地址访问libtest.so时也会触发一个请页异常Linux内核在分配物理内存之前会先从radix tree树中查询libtest.so是否已经加载到物理内存当内核发现libtest.so库文件已经加载到内存后就不会给进程B分配新的物理内存而是直接修改进程B的页表项将进程B中的这片映射区域直接映射到libtest.so所在的物理内存上。 通过上面的分析我们可以看到动态库libtest.so只加载到物理内存一次后面的进程如果需要链接这个动态库直接将该库文件映射到自身进程的虚拟空间即可同一个动态库虽然被映射到了多个进程的不同虚拟地址空间但是通过MMU地址转换都指向了物理内存中的同一块区域。此时动态库libtest.so也被多个进程共享使用因此动态库也被称作动态共享库。
五、内存泄漏与防范 什么是内存泄漏 在一个C函数中如果我们使用malloc()申请的内存在使用结束后没有及时被释放则C标准库中的内存分配器ptmalloc和内核中的内存管理子系统都失去了对这块内存的追踪和管理。失去管理和追踪的这块内存一直孤零零地躺在内存的某片区域用户、内存分配器和内存管理子系统都不知道它的存在它就像内存中的一块漏洞我们称这种现象为内存泄漏。
预防内存泄漏 预防内存泄漏最好的方法就是内存申请后及时地释放两者要配对使用内存释放后要及时将指针设置为NULL使用内存指针前要进行非空判断。
内存泄漏检测工具 MTrace是Linux系统自带的一个工具它通过跟踪内存的使用记录来动态定位用户代码中内存泄漏的位置。使用MTrace很简单在代码中添加下面的接口函数就可以了。 mtrace()函数用来开启内存使用的记录跟踪功能muntrace()函数用来关闭内存使用的记录跟踪功能。如果想检测一段代码是否有内存泄漏则可以把这两个函数添加到要检测的程序代码中。 开启跟踪功能后MTrace会跟踪程序代码中使用动态内存的记录并把跟踪记录保存在一个文件里这个文件可以由用户通过MALLOC_TRACE来指定。接下来我们编译、运行这个程序并使用MTrace来定位内存泄漏的位置。 通过生成的日志文件mtrace.log来定位内存泄漏在程序中的位置。 根据动态内存的使用记录我们可以很快定位到内存泄漏发生在mcheck.c文件中的第11行代码。
六、常见的内存错误及检测 如图5-47所示内存管理子系统将一个进程的虚拟空间划分为不同的区域如代码段、数据段、BSS段、堆、栈、mmap映射区域、内核空间等每个区域都有不同的读、写、执行权限。 通过内存管理每个区域都有具体的访问权限如只读、读写、禁止访问等。数据段、BSS段、堆栈区域都属于读写区而代码段则属于只读区如果你往代码段的地址空间上写数据就会发生一个段错误。在Linux用户进程的4GB虚拟空间上除了上面我们熟悉的区域还剩下很多区域如代码段之前的区域、堆和mmap区域之间的进程空间、内核空间等。这部分内存空间是禁止用户程序访问的。当一个用户进程试图访问这部分空间时就会被系统检测到在Linux下系统会向当前进程发送一个信号SIGSEGV终止该进程的运行。 对于应用程序来说常见的内存错误一般主要分为以下几种类型内存越界、内存踩踏、多次释放、非法指针。
内存越界导致段错误
内存踩踏如果一个进程中有多个线程多个线程都申请堆内存这些堆内存就可能彼此相邻使用时需要谨慎提防越界。在内核驱动开发中驱动代码运行在特权状态对内存访问比较自由多个驱动程序申请的物理内存也可能彼此相邻。如果你的程序代码经常莫名其妙地崩溃而且每次出错的地方也不一样在确保自己的代码没问题后也可以大胆地去怀疑一下是不是内存踩踏的问题。
内存践踏检测APImprotect()页page是Linux内存管理的基本单元在32位系统中一个页通常是4096字节mprotect()要保护的内存单元通常要以页地址对齐我们可以使用memalign()函数申请一个以页地址对齐的一片内存。
内存检测神器Valgrind包含一套工具集其中一个内存检测工具Memcheck可以对我们的内存进行内存覆盖、内存泄漏、内存越界检测。