当前位置: 首页 > 科技观察

Linux是如何管理内存的?

时间:2023-03-20 16:33:48 科技观察

Linux的内存管理模型非常直接,因为Linux的这种机制使其具有可移植性,可以在具有类似内存管理单元的机器上实现Linux。我们来看看Linux内存管理是如何实现的。的。基本概念每个Linux进程都会有一个地址空间,地址空间由三个段区组成:文本段、数据段和堆栈段。下面是一个进程地址空间的例子。数据段包含程序变量、字符串、数组和其他数据的存储。数据段分为两部分,初始化数据和未初始化数据。未初始化的数据就是我们所说的BSS。数据段的初始化需要编译时确定的常量和程序启动时具有初始值的变量。BSS段中的所有变量在加载后都初始化为0。不同于代码段(Textsegment),数据段datasegment是可以改变的。程序总是修改它的变量。此外,许多程序需要在执行时动态分配空间。Linux允许数据段随着内存的分配和释放而增长或收缩。为了分配内存,程序可以增加数据段的大小。C语言中有一套标准库malloc,经常用来分配内存。进程地址空间描述符包含一个动态分配的内存区域,称为堆。第三段是堆栈段。在大多数机器上,堆栈段将位于虚拟内存地址顶部的地址位置并向下扩展(朝向地址空间0)。例如,在32位x86机器上,堆栈从0xC0000000开始,这是允许用户模式进程看到的3GB虚拟地址限制。如果堆栈超出堆栈段,则会发生硬件故障,页面会下降一页。程序启动时,栈区并不是空的,而是包含所有的shell环境变量和进入shell调用它的命令行。例如,当您键入cpcxuanlx时,cp程序将在堆栈上使用字符串cpcxuanlx运行,以便您可以找出源文件和目标文件的名称。当两个用户运行同一个程序时,比如编辑器,内存中会保存两份编辑器程序代码,但这种方式效率不高。Linux系统支持共享文本段作为替代。在下图中,我们将看到两个进程A和B,它们具有相同的文本区域。data段和stack段只有在fork之后才会共享,共享也是共享未修改的页面。如果任何一个需要增长但没有相邻的空间来容纳它,也没有问题,因为相邻的虚拟页面不必映射到相邻的物理页面。除了动态分配更多内存外,Linux中的进程还可以通过内存映射文件访问文件数据。此功能允许我们将文件映射到进程空间的一部分,并且可以像位于内存中的字节数组一样读取和写入文件。映射文件使得随机读取和写入比使用read和write这样的I/O系统调用更容易。访问共享库使用这种机制。如下图,我们可以看到两个相同的文件会被映射到同一个物理地址,但是它们属于不同的地址空间。映射文件的好处是两个或多个进程可以同时映射到同一个文件,任何进程对文件的写操作对其他文件都是可见的。通过使用映射临时文件的方法,可以为多线程共享内存提供高带宽,进程退出后临时文件消失。但实际上,没有两个地址空间是相同的,因为每个进程都维护着不同的打开文件和信号。Linux内存管理系统调用让我们来讨论一下关于内存管理的系统调用。事实上,POSIX并没有指定任何内存管理的系统调用。但是,Linux有自己的内存系统调用。主要的系统调用如下。如果遇到错误,s的返回值为-1,a和addr为内存地址,len表示长度,prot表示控制保护位,flags为其他标志,fd为文件描述符,offset为文件偏移量.brk通过给出超出数据段的第一个字节的地址来指定数据段的大小。如果新值大于原来的值,那么数据区就会越来越大,反之就会越来越小。mmap和unmap系统调用控制映射文件。mmp的第一个参数addr决定了文件映射的地址。它必须是页面大小的倍数。如果参数为0,系统分配地址并返回a。第二个参数是长度,它告诉要映射多少字节。它也是页面大小的倍数。prot确定映射文件的保护位,可以将其标记为可读、可写、可执行或这些的组合。第四个参数flags可以控制文件是私有的还是可读的,addr是必需的还是只是一个提示。第五个参数fd是要映射的文件描述符。只有打开的文件才能被映射,所以如果要进行文件映射,就必须打开文件;最后一个参数偏移量将指示文件何时开始,不一定每次都从零开始。Linux内存管理实现内存管理系统是操作系统最重要的部分之一。从计算的早期开始,我们一直在使用比系统中实际存在的内存更多的内存。内存分配策略克服了这个限制,其中最著名的是虚拟内存。虚拟内存允许系统通过在竞争进程之间共享更多内存来拥有更多内存。虚拟内存子系统主要包括以下几个概念。大地址空间操作系统使系统看起来比实际物理内存大很多,因为虚拟内存比物理内存大很多倍。保护系统中的每个进程都会有自己的虚拟地址空间。这些虚拟地址空间彼此完全独立,因此运行一个应用程序的进程不会影响另一个应用程序。此外,硬件虚拟内存机制允许对关键内存区域进行内存保护。内存映射内存映射用于将图像和数据文件映射到进程地址空间。在内存映射中,文件的内容直接映射到进程的虚拟空间中。公平的物理内存分配内存管理子系统允许系统中每个正在运行的进程公平地分配系统的物理内存。共享虚拟内存尽管虚拟内存允许进程拥有自己的内存空间,但有时您需要共享内存。比如几个进程同时在shell中运行,就会涉及到IPC进程间通信问题。这时候你需要的是共享内存来传递信息,而不是通过每个进程拷贝一份来独立运行。下面正式讨论一下什么是虚拟内存。虚拟内存的抽象模型。在考虑Linux用于支持虚拟内存的方法之前,考虑一个没有陷入过多细节的抽象模型是很有用的。处理器在执行指令时,会从内存中读取指令并进行译码(decode),当指令被译码时,会得到某个位置的内容,并存入内存中。然后处理器继续执行下一条指令。这样,处理器始终访问内存以获取指令和存储数据。在虚拟内存系统中,所有地址空间都是虚拟的而不是物理的。但是物理地址是实际存取的,所以处理器需要根据操作系统维护的表将虚拟地址转换为物理地址。为了便于转换,虚拟地址和物理地址被分成固定大小的块,称为页面。这些页面具有相同的大小,如果页面大小不相同,操作系统将难以管理。AlphaAXP系统上的Linux使用8KB页面,而Intelx86系统上的Linux使用4KB页面。每个页面都有一个唯一的编号,即页框编号(PFN)。以上就是Linux内存映射模型。在这种页模型中,虚拟地址由两部分组成:偏移量和虚拟页框号。每次处理器遇到虚拟地址时,都会获取偏移量和虚拟页框号。处理器必须将虚拟页框号转换为物理页号,然后以正确的偏移量访问物理页。上图显示了两个进程A和B的虚拟地址空间,每个进程都有自己的页表。这些页表将进程中的虚拟页面映射到内存中的物理页面。页表中的每一项都包含一个有效标志(validflag):表示该页表项是否有效,该项描述的物理页框号访问控制信息,该页如何使用,是否可写,以及是否可以执行代码。虚拟地址的虚拟地址映射到内存的物理地址。首先需要计算出虚拟地址的页框号和偏移量。页面大小是2的幂,可以通过移位来完成。如果当前进程试图访问虚拟地址但无法访问,这种情况称为缺页异常。这时虚拟操作系统的故障地址和缺页原因会通知操作系统。通过以这种方式将虚拟地址映射到物理地址,可以将虚拟内存以任意顺序映射到系统的物理页面。按需分页由于物理内存远小于虚拟内存,因此操作系统需要注意避免直接使用低效的物理内存。一种节省物理内存的方法是只加载当前正在执行的程序使用的页面(这不是一种延迟加载的想法吗?)。例如,可以运行一个数据库来查询数据库,在这种情况下不会将所有数据加载到内存中,只加载需要检查的数据。这种仅在需要时才将虚拟页面加载到内存中的技术称为按需分页。交换如果进程需要将虚拟页放入内存,但当时没有可用的物理页,则操作系统必须丢弃物理内存中的另一个页以为该页腾出空间。如果该页面已被修改,则操作系统必须保留该页面的内容,以便以后可以访问它。这种类型的页面称为脏页,当它从内存中删除时,它会保存在一个称为交换文件的特殊文件中。相对于处理器和物理内存的速度,对交换文件的访问非常慢,操作系统需要兼顾将页面写入磁盘并将它们保存在内存中以供重用。Linux使用最近最少使用(LRU)页面老化技术来公平地选择可能从系统中删除的页面。该方案涉及系统中的每个页面。页面的年龄随着访问次数的变化而变化。如果一个页面访问次数越多,则该页面越年轻。如果某个页面的访问量太少,则该页面更容易被换出。物理和虚拟寻址模式大多数多功能处理器支持物理寻址模式和虚拟寻址模式的概念。物理寻址模式不需要页表,并且处理器不会尝试在该模式下执行任何地址转换。Linux内核链接在物理地址空间中运行。AlphaAXP处理器没有物理寻址模式。相反,它将内存空间划分为几个区域,并将其中两个区域指定为物理映射地址。这个内核地址空间称为KSEG地址空间,它包含从0xfffffc0000000000开始的所有地址。为了从KSEG中链接的代码(根据定义,内核代码)执行或访问数据,该代码必须在内核模式下执行。链接到Alpha上的Linux内核,从地址0xfffffc0000310000开始执行。访问控制页表的每一项也包含访问控制信息,主要检查进程是否应该访问内存。必要时需要限制对内存的访问。例如,包含可执行代码的内存自然是只读内存;操作系统不应允许进程通过其可执行代码写入数据。相反,可以写入包含数据的页面,但尝试执行该内存的指令将失败。大多数处理器至少有两种执行模式:内核模式和用户模式。除非处理器在内核模式下运行,否则您不想访问用户可执行的内核代码或内核数据结构。访问控制信息存储在上面的PageTableEntry,页表项,上图是AlphaAXP的PTE。位字段具有以下含义V表示有效,有效位FORFaultwhilereading,FaultwhiletryingtoreadthispageFOWErrorwriting,ErrorwhiletryingtowriteFOEErrorwhileexecuting,Faultwhiletryingtodothis处理器报告一个页面错误并在页面中有指令时将控制权传递给操作系统。ASM地址空间匹配。当操作系统想要清除翻译缓冲区中的某些条目时使用此选项。使用单个翻译缓冲区条目而不是多个翻译缓冲区条目映射整个块时使用的GH提示。KRE运行在内核态的代码可以读取页面URE运行在用户态的代码可以读取页面KWE运行在内核态的代码可以写入页面UWE运行在用户态的代码可以写入页面Pageframenumber对于VsetbitPTE,该字段包含物理页这个PTE的framenumber(页框号)。对于无效的PTE,如果此字段不为零,则它包含有关页面在交换文件中的位置的信息。除此之外,Linux使用两个位_PAGE_DIRTY如果设置,页面需要写出到交换文件_PAGE_ACCESSEDLinux用于将页面标记为已访问。可以使用缓存上面的虚拟内存抽象模型来实现,但是效率不会太高。操作系统和处理器设计者都试图提高性能。但除了提高处理器、内存等的速度外,最好的方法是维护有用信息和数据的缓存,使某些操作更快。在linux中,很多内存管理相关的buffer都会用到,buffer就是用来提高效率的。BufferCache缓冲区缓存包含块设备驱动程序使用的数据缓冲区。还记得什么是块设备吗?回想一下,块设备是一种可以以固定大小的块存储信息的设备。它支持在固定大小的块、扇区或簇中读取和(可选)写入数据。每个块都有自己的物理地址。通常块大小在512-65536之间。所有传输的信息将在连续的块中。块设备的基本特点是每个块都是相对对立的,可以独立读写。常见的块设备包括硬盘、蓝光光盘和USB驱动器。与字符设备相比,块设备通常需要更少的引脚。缓冲区缓存用于通过设备标识符和块号快速查找数据块。如果可以在buffercache中找到数据,就不需要从物理块设备中读取数据,访问速度会快很多。页面缓存页面缓存用于加速对磁盘上的图像和数据的访问。它用于一次一页地缓存文件的内容,并且可以通过文件和文件内的偏移量进行访问。当页面从磁盘读入内存时,它们被缓存在页面缓存中。swapareacache只有修改过的(脏页)才会保存在swap文件中只要这些页在写入swap文件后没有被修改过,下次交换页时就不需要再写入交换文件,因为页面已经在文件中交换。可以直接丢弃。在交换密集型系统上,这可以节省许多不必要且昂贵的磁盘操作。硬件高速缓存处理器中通常使用一种硬件高速缓存。页表条目的缓存。在这种情况下,处理器并不总是直接读取页表,而是根据需要缓存页面的翻译。这些是翻译后备缓冲区,也称为TLB,包含来自系统中一个或多个进程的页表条目的缓存副本。引用虚拟地址后,处理器会尝试找到匹配的TLB条目。如果找到,则可以将虚拟地址直接转换为物理地址,并对数据执行正确的操作。如果处理器找不到匹配的TLB条目,它会通过向操作系统发出TLB未命中的信号来请求操作系统的支持和帮助。系统特定的机制用于将异常传递给可以修复问题的操作系统代码。操作系统为地址映射生成一个新的TLB条目。异常清除后,处理器将再次尝试转换虚拟地址。这次能够执行成功。使用缓存也有缺点,为了省力,Linux不得不使用更多的时间和空间来维护这些缓存,如果缓存被破坏,系统就会崩溃。Linux页表Linux采用三级页表。访问的每一个页表都包含下一级页表图中的PDG,代表全局页表。当创建新进程时,会为新进程创建一个新的页目录PGD。要将虚拟地址转换为物理地址,处理器必须获取每一级字段的内容,将其转换为包含页表的物理页的偏移量,并读取下一级页表的页框号。如此重复三次,直到找到包含虚拟地址的物理页的页框号。Linux运行的每个平台都必须提供翻译宏,允许内核遍历特定进程的页表。这样,内核就不需要知道页表条目的格式或它们的排列方式。页面分配和释放对系统中的物理页面提出了许多要求。例如,当图像加载到内存中时,操作系统需要分配页面。系统中所有的物理页都是由mem_map数据结构来描述的,它是一个mem_map_t的列表。包括一些重要的属性count:这是页面的用户数的计数,当页面被多个进程共享时,计数大于1age:这是描述页面的年龄,用于判断该页是否适合丢弃或交换map_nr:这是这个mem_map_t所描述的物理页框号。页面分配代码使用free_area向量查找并释放页面,其中free_area的每个元素都包含有关页面块的信息。页面分配Linux页面分配使用众所周知的伙伴算法来分配和释放页面。页面以2的幂的块为单位分配。这意味着它可以分配1页、2页、4页等,只要系统中有足够的可用页面来满足需求。判断标准是nr_free_pages>min_free_pages。如果满足,则在free_area中寻找所需大小的页块,完成分配。free_area的每个元素都有一个分配页面和该大小块的空闲页面块的映射。分配算法搜索请求大小的页块。如果没有请求大小的页块可用,则搜索两倍于请求大小的页块,然后重复,直到搜索完free_area并找到页块。如果找到的页块大于请求的页块,则对找到的页块进行细分,直到找到合适大小的块。由于每个块都是2的幂,所以拆分过程很简单,只需将块分成两半即可。空闲块在适当的队列中排队,分配的页面块返回给调用者。如果请求一个2页块,第一个4页块(从第4页的框架开始)将被分成两个2页块。第一页(从框架页4开始)将作为分配页返回给调用者,第二块(从页6开始)将作为2页的空闲块排队到free_area数组上级的元素1。页面释放最常见的后果之一是内存碎片,它将大的空闲页面分成小的页面。页面释放代码尽可能将页面重新组合成更大的空闲块。每次释放页面时,都会检查相同大小的相邻块以查看它们是否空闲。如果是这样,它将与新释放的页面块结合,为下一个页面大小的块形成一个新的空闲页面块。每次将两个页面块重新组合成一个更大的空闲页面块时,页面重新分配代码会尝试将页面块重新组合成一个更大的空闲页面。这样,可用页面块将使用尽可能多的内存。比如上图中,如果要释放page1的页面,会和page0已经空闲的pageframe合并,作为一个2page大小的freeblock排队到element1自由区域。内存映射内核有两种内存映射:共享的和私有的。当进程只读文件而不写文件时使用私有类型。这时候私有映射效率更高。但是,对私有映射页面的任何写操作都会导致内核停止映射该文件中的页面。因此,写入操作既不会更改磁盘上的文件,也不会对访问该文件的其他进程可见。按需分页一旦可执行映像被内存映射到虚拟内存中,就可以执行它。因为只有图像的开头被物理拉入内存,所以它会快速访问物理??内存中尚不存在的虚拟内存区域。当进程访问没有有效页表的虚拟地址时,操作系统会报告此错误。页错误描述了页错误所在的虚拟地址及其导致的内存访问(RAM)类型。Linux必须找到表示发生页面错误的内存区域的vm_area_struct结构。由于搜索vm_area_struct数据结构对于有效处理页面错误至关重要,因此它们在AVL(Adelson-Velskii和Landis)树结构中链接在一起。如果出错的虚拟地址没有vm_area_struct结构,则进程访问了非法地址,Linux将向进程发送SIGSEGV信号,如果进程没有该信号的处理程序,进程将终止。Linux然后根据该虚拟内存区域允许的访问类型检查发生的页面错误类型。如果进程以非法方式访问内存,例如写入只读区域,也会发出内存访问错误信号。现在,Linux已经确定页面错误是合法的,因此必须对其进行处理。本文转载自微信公众号“JavaBuilder”,可通过以下二维码关注。如需转载本文,请联系Java开发者公众号。