当前位置: 首页 > Linux

【翻译】C程序员应该知道的内存知识(一)

时间:2023-04-06 02:41:21 Linux

系列更新:C程序员应该知道的内存知识(一)C程序员应该知道的内存知识(二)C程序员应该知道的内存知识(三)内存C程序员应该知道的知识(4)第一部分《踩坑记:go服务内存暴涨》好像还蛮流行的。文章的核心内容虽然很小,但是为了让大多数人看得懂,中间还是花了不少篇幅来讲解。尽管如此,还是觉得解释的不够透彻。想了想,我觉得文中提到的《What a C programmer should know about memory》[1]不错。想利用假期翻译学习一遍(顺便练练英文)。内容有点长,我会分成几篇,有兴趣的请关注本号。以下是正文。C程序员应该知道的内存知识2007年,UlrichDrepper写了一篇《MemoryKnowledgeEveryProgrammerShouldKnow》[2],篇幅很长但干货满满。但是这么多年过去了(译注:原文写于2015年2月),“虚拟内存”这个概念仍然让很多人着迷,就像某种魔法一样。呃,忍不住引用一下(译注:应该是Queen的AKindofMagic)。即便是这篇文章的正确性,时隔这么多年还在被质疑[3](译注:有人问文章中有多少内容在stackoverflow上仍然有效)。发生了什么?北桥?这到底是什么?这不是巷战。(译注:北桥是早期电脑主板上的重要芯片,用来处理来自CPU、内存等的高速信号)我会尽量展示学习这些知识的实用性(也就是你能做什么),包括“学习锁的基础知识”,以及更多有趣的东西。您可以将其视为那篇文章和您每天使用的东西之间的粘合剂。本文的例子将使用Linux下的C99(译注:c语言标准1999版)编写,但很多题目都是通用的。注意:我对Windows不是很熟悉,但我很乐意提供一个链接,指向解释它的文章(如果有的话)。我将尝试标记哪些方法是特定于平台的。但我只是个人,所以如果您发现差异,请告诉我。了解虚拟内存-错综复杂除非您正在处理某些嵌入式系统或内核空间代码,否则您是在保护模式下工作。(译注:指x86CPU提出的保护模式,通过硬件提供的一系列机制,操作系统可以以低权限运行用户代码)。这太棒了,您的程序可以拥有自己的[虚拟]地址空间。“虚拟”这个词在这里很重要。这意味着,除其他事项外,您不受可用内存的限制,但也没有资格使用任何可用内存。如果你想使用这个空间,你必须向操作系统询问一些真实的东西来制作“衬垫”。这称为映射。这个后盾可以是物理内存(不一定是RAM),也可以是持久存储(译注:一般指硬盘)。前者称为“匿名映射”。别着急,让我们马上进入正题。一个虚拟内存分配器(VMA,virtualmemoryallocator)可能会给你一块不属于它的内存,希望你不要使用它是徒劳的。就像今天的银行一样。这被称为过度使用[4],并且有合法的应用程序需要这样做(例如稀疏数组),这意味着内存分配不会被简单地拒绝。char*block=malloc(1024*sizeof(char));如果(块==NULL){返回-ENOMEM;/*sad:(*/}检查NULL返回值是一种很好的做法,但没有以前那么强大。由于过度使用机制的存在,OS可能会给你的内存分配器一个有效的指针,但是whenyouwanttoaccessit-clang*。这里的“clang”是平台相关的,但通常表现为yourprocessKilledbyOOMKiller[5]。(译注:OOM即OutOfMemory,当内存不足时,Linux会根据一定的规则挑选出一个进程杀掉,并在dmesg中留下记录)——这里有点过于简单了;在后面的章节中会有进一步的解释,但我倾向于在深入细节之前先回顾一下更基本的东西。ProcessmemorylayoutProcessmemorylayoutiswellexplainedin《Anatomy of a Program in Memory》[6]byGustavoDuarte所以我只是引用原文,希望这是一个合理的使用。我只有一些小的评论,因为这篇论文只描述了内存x86-32的布局,好在x86-64变化不大,只是进程可以使用更大的地址空间如图所示,地址空间从高到低,最下面是0x00000000,最高的是0xFFFFFFFF,一共4GB(2^32)。高1GB是内核空间,用户代码不能读写,否则会触发段错误。图中右侧标注的0xC0000000为3GB;TASK_SIZE是Linux内核编译配置的名称,表示内核空间的起始地址。随机栈偏移:加入随机偏移可以大大降低被栈溢出攻击的风险。栈(向下增长):进程的栈空间向下增长,栈底在高地址。PUSH指令会减少CPU的SP寄存器(堆栈指针)。图中右侧的RLIMIT_STACK是内核对栈空间大小的限制,一般为8MB,可以通过setrlimit系统调用修改。MemoryMappingSegment:内存映射区,通过mmap系统调用,将文件映射到进程的地址空间(包括libc.so等动态库),或者匿名映射(不需要映射文件,让OS分配更多带衬里的地址空间))。Heap:我们常说的堆空间,自下而上增长,通过brk/sbrk系统调用扩大其上限BSS段:包含未初始化的静态变量Data段:代码中静态初始化的变量Text段(ELF):进程中的这里提到的可执行文件(机器码)中段(segment)的概念来源于x86cpu的段页内存管理图。不总是。MMS通常(详见Linux内核代码x86/mm/mmap.c:113和arch/mm/mmap.c:1953)从堆栈最低地址下方的某个随机地址开始。注意是“usually”,因为它也可能在栈之上,如果栈空间限制很大(或者无穷大;译注:可以用setrlimit修改),或者启用兼容布局。这有多重要?-不重要,但可以让您了解免费地址范围。在上图中,可以看到3个不同的变量存储区:进程的数据段(静态存储,或者说堆内存分配)、内存映射段和栈。我们从这里开始。理解栈上的内存分配框:alloca()-在调用者的栈帧上分配内存getrlimit()-获取/设置资源限制ssigaltstack()-设置或获取信号栈上下文stack比较容易理解,毕竟每个人都知道如何将变量放在堆栈上,对吗?例如:int楼梯=2;intheaven[]={6,5,4};变量的有效性受范围限制。在C中,作用域指的是一对花括号{}。所以每次遇到右花括号时,相应变量的作用域就结束了。然后是alloca(),它在当前栈帧上动态分配内存。栈帧与内存帧(也称为物理页)不同,它只是一组压入栈中的数据(函数、参数、变量等)。由于我们在栈顶(译注:SP寄存器总是指向栈顶),我们可以使用剩余的栈空间,只要不超过栈大小限制即可。这就是变长数组(variable-length,VLA)和alloca的原理。不同的是VLA是有范围限制的,alloca分配的内存有效期可以一直持续到当前函数返回。这里没有语言律师业务(译注:没有人关心你,爱它),但是如果你在循环中使用alloca,你可能会踩坑,因为你不能释放它分配的空间:voidlaugh(void){for(unsignedi=0;i