C程序的内存结构
C语言之所以复杂是因为它的内存模型是必不可少的. 与某些高级语言不同,使用对象时只需要使用new进行创建. 之后,您无需担心所有事情. 对于C语言,与内存相关的所有事物都需要熟悉,否则,很长一段时间以来,它总是会步履维艰. 下图是C程序的典型存储器结构. 当然,有一个重要前提. 这样的布局在虚拟内存中:
关于虚拟内存内核维护一个页表(page table),该页表用于表示虚拟内存与物理内存地址或磁盘(交换区域)之间的映射关系. 并非所有虚拟地址都需要映射到物理内存上,否则,无论计算机的存储库有多大,如果再打开几个进程,内存将被消耗. 当您需要使用内存时,请应用到操作系统. 如果请求合法,则内核将条目添加到页表中,以在虚拟地址和物理地址之间建立映射. 同样,当需要释放时,将释放映射并返回资源.
关于虚拟内存的优势,“ Linux编程接口”给出了一个相对完整的答案:
进程与内核之间是相互隔离的,因此一个进程无法读取或修改另一个进程或内核的内存. 通过使每个进程的页表条目指向RAM(或交换区域)中物理页的不同集,可以实现这一点.
在适当情况下,两个或多个进程可以共享内存. 内核通过使不同进程中的页表条目引用RAM的相同页来实现这一点. 内存共享发生在两种常见情况下:
–执行同一程序的多个进程可以共享该程序代码的单个(只读)副本. 当多个程序执行相同的程序文件(或加载相同的共享库)时,会隐式执行这种共享.
–进程可以使用shmget()和mmap()系统调用来显式请求与其他进程共享内存区域. 这是出于进程间通信的目的.
便于实施存储器保护方案;也就是说,可以标记页表项,以指示相应页的内容是可读,可写,可执行或这些保护的某种组合. 在多个进程共享RAM页的情况下,可以指定每个进程在内存上具有不同的保护例如,一个进程可能具有对页面的只读访问权限,而另一个进程具有对读写的访问权限.
程序员和诸如编译器和链接器之类的工具无需关心RAM中程序的物理布局.
由于仅一部分程序需要驻留在内存中,因此程序加载和运行更快. 此外,一个进程的内存占用量(即虚拟大小)可能会超过RAM的容量.
虚拟内存管理的最后一个优势是,由于每个进程都使用较少的RAM,因此可以同时在RAM中保留更多的进程. 这通常可以提高CPU利用率,因为它增加了在任何时候,至少有一个CPU可以执行的进程的可能性.
本文主要讨论堆区域中内存分配的一些细节.
使用系统调用
通常,我们将堆的当前边界称为“程序中断”. 在堆区域中分配内存是将程序中断移到更高地址的过程. UNIX系统中的两个系统调用与此程序中断关系最密切:
int brk(void * addr);
无效* sbrk(intptr_t增量);
sbrk()用于通过将程序中断移动增量长度来增加和释放内存. 由于虚拟内存是按页分配的,因此增量值不是实际分配的结果. 只要它不是页面大小的整数倍,就会多提供一个页面,而不是四舍五入,除非内存不足,否则总会提供更多页面. sbrk(0)显示当前程序中断. 当尝试访问除程序中断以外的存储器时,将生成SIGSEV信号并发生段故障. brk()和sbrk()的用法类似.
1 intmain()2 {3 int* p = sbrk(100);4 *(p+1023) =4;5 printf("**\n");6 *(p+1024) =4;7 }
适用于内核的一段代码,用于100个字节的内存. 实际上,它映射一个内存页面. 第4行访问内存页面的最后4个字节,并将其重写. 第6行访问映射关系之外的内存. 非法,该程序的运行结果如下:
$ a.out
**
分段错误
使用brk()/ sbrk()释放内存时,映射关系可能不会立即释放. 当程序中断掉落超过一页时,可以将请求的物理内存返回给内核. 当然,释放后对该内存的所有操作都是不确定的,就像玩火一样. 同时,应该注意的是c语言内存分配对应,程序中断的移动不能移到堆区域之外,例如bss区域,数据区域等,这种行为基本上是致命的行为.
使用C标准库函数
malloc()/ free()绝对是C语言中使用最广泛的函数之一. 与brk()/ sbrk()相比,另一个接口更简单,并且还允许释放内存. (当程序中断向下移动以释放内存时,Brk()/ sbrk()无法自由释放,并且顶部的“ innocent”元素也将被释放. )例如,这种情况(此处的内存映射为已发布): <
free()释放没有这样的“坑”,因为自由释放内存不一定会导致程序中断. 如果在free()释放的内存上方(在高内存地址处)仍然有可用内存,则程序中断将不会移动,因此不会释放映射关系,这意味着该内存尚未返回到内核. 相反,它将作为免费内存移交给免费维护. 当下一个malloc应用程序执行时,它将将该内存(如果足够的话)返回给malloc. 那么,free如何知道空闲内存的大小?这是因为malloc返回的内存具有特殊的结构:
此存储器的大小记录在该存储器的前面. 回收该内存时,将记录其长度和地址. 再次使用malloc时,它将比较可用内存列表与满足要求的内存,并将其移交给程序以供“第二次使用”(或N次使用). 当然,可用内存列表的使用取决于具体情况:
1. 如果可用内存大于malloc应用程序,则将其一部分剪切并返回给malloc,其余部分将被视为下一个malloc的可用内存.
2. 如果在malloc期间没有合适的可用内存,则程序中断将照常移动,并且可能会应用新的内存(在上一次映射期间可能会有多余的内存,因此不需要重新映射).
了解了这些基本实现后,我们发现malloc()和free()是更危险的函数. 使用请求的内存时要特别小心,尤其是边界条件,否则结果可能是灾难性的. 例如,在这种情况下,在使用分配的内存之后,仅越过一个字节,而这个字节记录了另一内存的长度. 释放此内存后,free保持错误的长度. 下次有内存请求时,该内存将移交给malloc,这样就会出现“灾难”.
其余的内存分配功能
void * calloc(size_t nmemb,size_t size);
void * realloc(void * ptr,size_t size);
calloc()与malloc类似,它分配大小为nmemb的对象,但与malloc不同: calloc会将分配的内存初始化为0.
realloc()的名称意为“重新分配”,用于调整分配的内存ptr的大小,如果ptr之后的内存不足,它将申请一个新区域,原样复制原始内存,新建增加的内存未初始化. 因此,返回的结果可能与ptr不同,实际上,在某些情况下它并不相同. 因此,重新分配效率不够高. 建议不要将其用作最后的手段.
无效* alloca(size_t大小);
该功能是在堆栈上分配内存. 手册中对此进行了描述:
alloca()函数在调用者的堆栈帧中分配大小字节的空间. 当调用alloca()的函数返回其调用者时,此临时空间将自动释放.
在堆栈上分配内存的方案并不多. 例如,执行非本地跳转时,setjmp和longjmp需要使用分配的内存. 您应该考虑使用alloca,因为他申请的内存将自动释放c语言内存分配对应,所以当longjmp反弹时不会出现内存泄漏. 偶尔使用此功能有利于身心健康.
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-276649-1.html