当前位置: 首页 > Linux

惊人的!20张图解开内存管理的迷雾,豁然开朗

时间:2023-04-06 02:40:38 Linux

日常英语,每天进步一点前言之前很多读者给我反馈,能不能写个图形化的操作系统?既然有那么多读者想看,我最近就在疯狂温习操作系统的知识。操作系统确实是一门难学的课程,至少我觉得比计算机网络难多了,但是其重要性就不用我多说了。在学习操作系统的时候,主要的痛点就是抽象的词或者概念太多,难以理解,非常容易被忽略。尽管我满怀热情地开始学习操作系统,但不到3分钟,睡意突然袭来。..该啃的还是要啃的,该说明的还是要说明的。万众期待的“图形操作系统”系列来了。这篇文章将告诉你内存管理。内存管理还是比较重要的一个环节。了解之后,至少你会对整个操作系统的工作有一个初步的了解。难怪面试时经常会问内存管理。Justdoit,本文大纲:正文虚拟内存如果你是电子相关专业,想必在大学里也曾摆弄过单片机。单片机没有操作系统,所以每次写完代码,都需要借助工具把程序烧进去,这样程序才能运行。另外,单片机的CPU直接操作内存的“物理地址”。在这种情况下,不可能同时在内存中运行两个程序。如果第一个程序在位置2000写入一个新值,它将擦除第二个程序存储在同一位置的所有内容,因此同时运行两个程序根本行不通,这两个程序将立即崩溃。操作系统是如何解决这个问题的?这里的关键问题是两个程序都引用了绝对物理地址,这是我们最想避免的。我们可以“隔离”进程使用的地址,即让操作系统为每个进程分配一组独立的“虚拟地址”。每个人都有。每个人都可以玩自己的地址而不会互相干扰。但是有一个前提,就是每个进程都不能访问物理地址。至于虚拟地址最终是如何落到物理内存中的,对进程来说是透明的,操作系统已经把这些安排的很清楚了。操作系统会提供一种机制来映射不同进程的虚拟地址和不同内存的物理地址。如果程序要访问虚拟地址,操作系统将其转换成不同的物理地址,这样不同的进程在运行时,写入不同的物理地址,这样就不会发生冲突。因此,这里引入地址的两个概念:我们程序使用的内存地址称为虚拟内存地址(VirtualMemoryAddress),而硬件中实际存放的空间地址称为物理内存地址(PhysicalMemoryAddress)。操作系统引入了虚拟内存,进程持有的虚拟地址会通过CPU芯片中内存管理单元(MMU)的映射关系转换成物理地址,然后通过物理地址访问内存,如如下图所示:操作系统是如何管理虚拟地址和物理地址的关系的?主要有两种方法,即内存分段和内存分页。分割是较早提出的。我们先来看看内存分段。一个内存段程序由代码段、数据段、栈段、堆段等几个逻辑段组成。不同的段具有不同的属性,因此将这些段以Segmentation的形式分开。在分段机制下,虚拟地址和物理地址是如何映射的?段机制下的虚拟地址由段选择符和段内偏移量两部分组成。段选择符存储在段寄存器中。段选择器中最重要的是段号,用作段表的索引。段表存储段的基地址、段边界和特权级别。虚拟地址中的段内偏移量应介于0和段边界之间。如果段内偏移量合法,则将段基地址与段偏移量相加得到物理内存地址。上面我们知道,虚拟地址是通过段表映射到物理地址的。分段机制会将程序的虚拟地址分成4段。每个段在段表中都有一个条目。在此项中找到段。基地址加上偏移量,就可以找到物理内存中的地址,如下图所示:如果我们要访问段3中偏移量为500的虚拟地址,我们可以将物理地址计算为,段3的基址为7000+偏移量500=7500。这种分段方式很好,解决了程序本身不需要关心具体物理内存地址的问题,但也有一些不足:首先是内存碎片的问题。二是内存交换效率低。接下来说说为什么会出现这两个问题。我们先来看看,为什么分段会造成内存碎片?让我们来看看这样一个例子。假设有1G物理内存,用户执行多个程序,其中:游戏占用内存512MB,浏览器占用内存128MB,音乐占用内存256MB。这时候如果我们关闭浏览器,还有1024-512-256=256MB的空闲内存。如果256MB不连续,被分成两个128MB的内存段,就会导致没有空间再打开一个200MB的程序。这里内存碎片的问题有两个地方:外部内存碎片,即产生多个不连续的小物理内存,导致无法加载新程序;内部内存碎片,程序的所有内存都加载到物理内存中,但是这个程序的部分内存可能不是很经常使用,也会导致内存浪费;对于以上两种内存碎片问题,解决方法会有所不同。解决外部内存碎片问题的方法是内存交换。可以将音乐程序占用的256MB内存写入硬盘,再从硬盘读回内存。但是当我们读回来的时候,并不能加载回原来的位置,而是紧跟在已经被占用的512MB内存后面。这样就会腾出连续的256MB空间,这样就可以加载新的200MB程序了。这个内存交换空间,在Linux系统中,也就是我们经常看到的Swap空间。这个空间是从硬盘上划分出来的,用于内存和硬盘之间的空间交换。我们再来看看,为什么分段会导致内存交换效率低?对于多进程系统,内存碎片很容易以分段的方式出现。如果出现内存碎片,Swap内存区就得重新swap。这个过程会造成性能瓶颈。因为硬盘的访问速度比内存慢很多,每次内存交换时,我们都需要向硬盘写入一大块连续的内存数据。所以内存交换的话,就是一个占用内存空间大的程序,所以会出现整机卡死的现象。为了解决内存分段的内存碎片和内存交换效率低的问题,出现了内存分页。内存分页和分段的好处是可以产生连续的内存空间,但是会存在内存碎片和内存交换空间过多的问题。要解决这些问题,就需要想办法减少内存碎片。此外,当需要内存交换时,需要交换或从磁盘加载的数据较少,可以解决问题。这种方法也称为内存分页(Paging)。分页就是将整个虚拟和物理内存空间切割成固定大小的段。这样一块连续的、固定大小的内存空间,我们称之为页。在Linux下,每个页面的大小为4KB。虚拟地址和物理地址是通过页表进行映射的,如下图:页表实际上是存放在CPU的内存管理单元(MMU)中的,所以CPU可以直接查到物理内存地址通过MMU访问。当在页表中找不到进程访问的虚拟地址时,系统会产生pagefault异常,进入系统内核空间分配物理内存,更新进程页表,最后返回用户空间恢复进程的运行。分页如何解决分段内存碎片和内存交换效率低的问题?由于内存空间是预先划分好的,不会有像segmentation那样间隙很小的内存,这就是segmentation会产生内存碎片的原因。但是如果使用分页,释放的内存是以页为单位释放的,不会有进程不能使用的小内存。如果内存空间不足,操作系统会释放其他正在运行的进程中“最近未使用”的内存页,即暂时写入硬盘,这称为换出(SwapOut)。一旦需要,就重新加载,这称为换入(SwapIn)。所以一次只写几页或几页到磁盘,不会占用太多时间,内存交换的效率也比较高。此外,分页方式使得在加载程序时不再需要将程序全部加载到物理内存中。将虚拟内存和物理内存的页面映射后,我们并不能真正将页面加载到物理内存中,而只有在程序运行时,才需要使用相应虚拟内存页面中的指令和数据,再加载到物理内存中记忆。在分页机制下,虚拟地址和物理地址是如何映射的呢?在分页机制下,虚拟地址分为两部分,页号和页内偏移量。页码用作页表的索引。页表包含了物理页的每一页所在的物理内存的基地址。基地址和页内偏移量的组合构成了物理内存地址,如下图所示。总结一下,对于一次内存地址转换,其实就是三步:将虚拟内存地址划分为页码和偏移量;根据页码从页表中查询对应的物理页码;直接取物理页号,加上前面的偏移量,就得到了物理内存地址。举个例子,虚拟内存中的页通过页表映射到物理内存中的页,如下图:这看起来没什么问题,但是在实际的操作系统中,这种简单的分页会肯定有问题的。简单分页有什么缺陷吗?存在空间缺陷。因为操作系统可以同时运行很多进程,这不就意味着页表会很大吗。在32位环境下,虚拟地址空间总共有4GB。假设一个page的大小是4KB(2^12),那么大约需要100万(2^20)个page,每个“pagetableentry”需要4byte大小来存储,那么整个4GB空间的映射需要4MB的内存来存储页表。这个4MB的页表看起来并不是很大。但是要知道,每个进程都有自己的虚拟地址空间,也就是说,有自己的页表。那么,如果有100个进程,就需要400MB的内存来存放页表,这已经是非常大的内存了,更何况是64位环境。多级页表要解决上述问题,就需要采用一种称为多级页表(Multi-LevelPageTable)的解决方案。前面我们看到,对于单个页表的实现,在32位、4KB页大小的环境下,一个进程的页表需要保存超过100万个“页表项”,而每个页tableentry是占用4个字节,所以每个页表需要占用4MB的空间。我们将这个拥有超过100万个“页表项”的单级页表进行分页,将页表(一级页表)分为1024个页表(二级页表),每张表(二级页表)级页表))包含1024个“页表项”,构成一个二级页。如下图:你可能会问,如果分二级表,需要4KB(一级页表)+4MB(二级页表)内存才能映射一个4GB的地址空间。这不是占用更多空间吗?当然如果把这4GB的虚拟地址全部映射到物理内存的话,二次分页占用的空间确实更大,但是我们往往不会为一个进程分配那么多内存。其实我们应该换个角度看问题。还记得计算机组成原理中的泛在局部性原理吗?每个进程都有4GB的虚拟地址空间,但显然对于大多数程序来说,所使用的空间远远达不到4GB,因为会有一些对应的页表项是空的,根本没有分配。对于分配的页表项,如果有一个页表最近一段时间没有被访问,当物理内存紧张时,操作系统会将页换出到硬盘上,也就是说,不会占用物理内存。如果使用二级分页,一级页表可以覆盖整个4GB虚拟地址空间,但是如果某个一级页表的页表项没有被使用,则不需要创建二级页表这个页表对应的levelpagetable的页表已经起来了,也就是需要的时候可以创建二级页表。做个简单的计算,假设只使用了20%的一级页表项,那么该页表占用的内存空间只有4KB(一级页表)+20%*4MB(二级页表)=0.804MB,与单级页表的4MB相比,这不是一个巨大的节省吗?那么为什么非分级页表不能这样节省内存呢?从页表的性质来看,存放在内存中的页表的职责是将虚拟地址翻译成物理地址。如果虚拟地址在页表中找不到对应的页表项,计算机系统将无法工作。因此,页表必须覆盖整个虚拟地址空间。非一级页表需要100万以上的页表项来映射,而二级页表只需要1024个页表项(此时一级页表覆盖了整个虚拟地址空间,二级页表在需要时创建)。如果我们将二级分页扩展到多级页表,我们会发现页表占用的内存空间更少,这都要归功于局部性原理的充分应用。对于64位系统,两级分页肯定不行,变成了四级目录,即:全局页目录项PGD(PageGlobalDirectory);上层目录项PUD(PageUpperDirectory);中间页目录项PMD(PageMiddleDirectory);页表项PTE(PageTableEntry);TLB多级页表解决了空间的问题,但是虚拟地址到物理地址的转换需要经过多次转换过程,这显然降低了这两种地址转换的速度,也就是带来了时间开销。程序是本地化的,即在一段时间内,整个程序的执行仅限于程序的某一部分。相应的,执行访问的存储空间也被限制在一定的内存区域。我们可以利用这个特性,将最常访问的页表条目存储在访问速度更快的硬件中,因此计算机科学家在CPU芯片中添加了一个页表,用于存储最常访问的程序。ItemCache,这个Cache就是TLB(TranslationLookasideBuffer),通常称为页表缓存,转发旁路缓存,快速表等。在CPU芯片中,封装了内存管理单元(MemoryManagementUnit)芯片,用于完成地址转换和TLB访问和交互。有了TLB,CPU在寻址的时候会先检查TLB,如果没有找到,就会继续检查常规页表。TLB的命中率其实是非常高的,因为程序最常访问的页面就那么几个。Segmentedpage内存管理内存分段和内存分页不是对立的,它们可以结合起来在同一个系统中使用,然后组合起来,通常称为segmentedpagedmemorymanagement。段页内存管理的实现:首先将程序划分为多个逻辑段,也就是上面提到的分段机制;然后把每个segment分成多个page,也就是把segment出来的连续空间分成固定大小的pages;因此,地址结构由三部分组成:段号、段内页号和页内位移。用于段页地址转换的数据结构是每个程序一个段表,每个段建立一个页表。段表中的地址是页表的起始地址,页表中的地址是某页的物理页号,如图:要在段页地址转换中获取物理地址,三个内存访问需要:首先访问段表,获取页表起始地址;第二次访问页表,获取物理页码;第三次结合物理页码和页内位移得到物理地址。可以采用软硬件结合的方法实现段页地址转换,增加了硬件成本和系统开销,但提高了内存的利用率。Linux内存管理那么,Linux操作系统是如何管理内存的呢?在回答这个问题之前,我们得先看看英特尔处理器的发展史。早期的Intel处理器从80286开始采用分段内存管理,但很快发现没有页内存管理只有分段内存管理是不够的,这将使其X86系列失去市场竞争力。因此不久之后80386就实现了分页内存管理。也就是说,80386不仅完成和完善了从80286开始的段内存管理,还实现了页内存管理。但是80386页内存管理在设计的时候并没有绕过段内存管理,而是基于段内存管理,也就是说页内存管理的功能是由段内存管理决定的。在映射地址上增加了一层地址映射。由于此时段内存管理映射的地址不再是“物理地址”,因此Intel称之为“线性地址”(也叫虚拟地址)。因此,段内存管理首先将逻辑地址映射到线性地址,然后页内存管理将线性地址映射到物理地址。这里对逻辑地址和线性地址进行说明:程序使用的地址通常是没有被段内存管理映射的地址,称为逻辑地址;段内存管理映射的地址称为线性地址,也称为虚拟地址;逻辑地址是“段内存管理”转换前的地址,线性地址是“页内存管理”转换前的地址。了解了Intel处理器的发展历史,我们再来说说Linux是如何管理内存的?Linux内存主要采用分页内存管理,但同时不可避免地涉及到段机制。这主要是上面Intel处理器的发展历史造成的,因为IntelX86CPU在进行页映射之前总是先对程序中使用的地址进行段映射。既然CPU的硬件结构是这样的,Linux内核就不得不服从Intel的选择。但事实上,Linux内核采取的做法是让段映射进程实际上无用。即“上有政策,下有对策”。如果你买不起,就躲起来走吧。Linux系统中的每个段都是从地址0开始的整个4GB虚拟空间(在32位环境下),即所有段的起始地址都是一样的。这意味着Linux系统中的代码,包括操作系统本身的代码和应用程??序代码,都面临着一个线性地址空间(虚拟地址),相当于屏蔽了处理器中的逻辑地址。概念,段仅用于访问控制和内存保护。我们再来看看,Linux的虚拟地址空间是如何分布的呢?在Linux操作系统中,虚拟地址空间内部分为两部分,内核空间和用户空间。具有不同位的系统具有不同的地址空间范围。比如最常见的32位和64位系统如下:从这里可以看出,32位系统的内核空间占用1G,处于最高点,剩下的3G是用户空间;64位系统的内核空间和用户空间空间都是128T,分别占据了整个内存空间的最高和最低部分,其余中间部分未定义。下面说一下内核空间和用户空间的区别:当进程处于用户态时,只能访问用户态内存;只有进入内核态后才能访问内核空间内存;虽然每个进程都有自己独立的虚拟内存,但是每个虚拟内存中的内核地址实际上关联的是同一个物理内存。这样,进程切换到内核态后,就可以方便地访问内核空间内存了。接下来,我们就来详细了解一下虚拟空间的划分。用户空间和内核空间的划分不同,内核空间的分布就不多说了。我们来看看用户空间的分布情况。以32位系统为例,我画了一张图来说明它们的关系:通过这张图可以看到,用户空间内存从低到高共有7种不同类型。内存段:程序文件段,包括二进制可执行代码;初始化数据段,包括静态常量;未初始化的数据段,包括未初始化的静态变量;堆段,包括动态分配的内存,从低地址向上增长;文件映射段,包括动态库、共享内存等,从低地址开始向上增长(与硬件和内核版本有关);栈段,包括局部变量和函数调用上下文等。栈的大小是固定的,一般为8MB。当然,系统也提供了参数,方便我们自定义尺寸;在这7个内存段中,堆和文件映射段的内存是动态分配的。例如,使用C标准库的malloc()或mmap(),可以分别在堆和文件映射段动态分配内存。总结在多进程环境下,为了保持进程间的内存地址不受影响,相互隔离,操作系统为每个进程独立分配了一组虚拟地址空间,每个程序只关心自己的虚拟地址。是的,其实每个人的虚拟地址都是一样的,只是对物理地址内存的分配不同而已。作为程序,您不必关心物理地址。每个进程都有自己的虚拟空间,但是物理内存只有一块,所以当启用大量进程时,物理内存难免会很吃紧,所以操作系统会把不经常使用的内存暂时存放到硬盘中通过内存交换技术(swappedout),在需要的时候加载回物理内存(swappedin)。既然有了虚拟地址空间,就要将虚拟地址“映射”到物理地址上,而物理地址通常由操作系统来维护。那么对于虚拟地址和物理地址的映射关系,可以有分段和分页,也可以是两者的结合。内存分段根据程序的逻辑角度分为栈段、堆段、数据段、代码段等,这样可以分隔不同属性的段,同时又是一个连续的空间.但是每个段的大小不统一,会导致内存碎片和内存交换效率低的问题。于是就出现了内存分页,将虚拟空间和物理空间分成固定大小的页面。例如在Linux系统中,每页的大小为4KB。因为分页后,不会再有小的内存碎片了。同时,内存交换时,只向硬盘写入一页或几页,大大提高了内存交换的效率。再者,为了解决简单分页产生的页表过大的问题,出现了多级页表,解决了空间的问题,但是这样会导致CPU需要很多层的表参与寻址过程,增加了时间开销。因此,根据程序局部性原则,在CPU芯片中增加了一个TLB,负责缓存最近频繁访问的页表项,大大提高了地址转换速度。Linux系统主要采用分页管理,但是由于Intel处理器的发展历史,Linux系统无法避免段管理。所以linux把所有段的基地址都设置为0,也就是说所有程序的地址空间都是一个线性地址空间(虚拟地址),相当于屏蔽了CPU逻辑地址的概念,所以段只是用来访问的控制和内存保护。另外,Linxu系统中虚拟空间的分布可以分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区.Nagging发布的300页“图解网络”PDF已经有一段时间了。最近收到了很多读者的勘误反馈,大部分都是错别字和遗漏。非常感谢这些认真阅读PDF的读者。其中,有一位非常硬核的读者,将近9W字的PDF中的错别字全部改正。而且很详细,详细到什么程度?细节空格太多,标点符号不正确,“在”和“在”的区别等等。让我告诉你小林写了多少错误。..红色的字符串是这位读者更正的记录。再次感谢这位铁杆细心的读者。说实话,有这样的读者,小林还是挺自豪的哈哈。Kobayashi还重新组织了PDF。您可以重新下载修正后的“图文网络V2.0”,在公众号回复“网络”即可获取下载链接。如果您在阅读过程中发现有不明白或错误的地方,请及时反馈,与小林交流。小林是给大家插画的工具人,再见,我们下次再见!