Linux内核初识-第三节-内存管理

本文介绍了Linux内核中的内存管理子系统,详细阐述了虚拟内存的概念及其关键特性,包括超大的地址空间、地址空间保护、内存映射、物理地址分配、共享虚拟内存等。此外,还深入探讨了虚拟内存的具体实现机制,如页表、请求式分页、交换技术及缓存策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux内核-第三节:内存管理(翻译文)

 

我们知道操作系统中最主要的子系统有 1,进程管理 2,内存管理3,文件系统4,网络模块。这些最基本的子系统是一个操作系统不可缺少的部分,这篇文章,将介绍下操作系统中的内存管理子系统。

 

无论何时,计算机内存的容量总不能满足越来越庞大的程序的需求。人们为了解决这个问题,想出了很多的策略,一种最流行,最成功的策略就是虚拟内存机制。什么是虚拟内存呢?这个概念其实很容易想,比如一台机器中实际安装的内存条只有比如256MB大小,当多个进程运行在内存中时,每个进程占据一块内存空间,很快随着进程数的增加,内存马上就不够用了,也就是,这些进程占据的空间大小绝对不能超过实际内存条提供的大小。

 

通过使用虚拟内存技术,我们可以使得进程发现一个惊喜,为他们提供的可使用的内存空间的大小比内存条的实际容量要多了。

 

咋一看,你可能马上得出结论,这个技术不就是在物理内存上建立了一套封装层嘛,这一层负责管理实际的物理内存,并提供一套机制,为用户的内存请求进行应答。但是,我得告诉你,虚拟内存的设计者并没有仅仅将想象力局限在那儿。虚拟内存技术为你提供更多的强大功能。

 

下面看看操作系统中的内存管理子系统的基本功能:

 

1,  超大的地址空间

内存管理子系统通过使用虚拟内存技术,使得操作系统看起来拥有比其所在主机拥有的内存容量更大的内存空间。

 

2,  地址空间保护

运行在机器上的每一个计算机程序都有一个属于自己的内存空间,包括操作系统本身。主意,用户级程序一般是运行在按照虚拟内存机制编排的地址空间中的。而操作系统,运行在实际的物理内存上。由于用户级程序运行在按照虚拟内存机制编排的内存空间中,所以,可以使用有效地手段,使得进程之间完全的分隔起来,不会发生两个进程的地址空间有交集的情况。这就提供了一种保护,比如,用户级进程,可以控制其不能访问操作系统所在的内存空间,从而就杜绝了恶意的用户级程序破坏操作系统的途径;而且,也能保护两个不同的进程防止其发生地址空间重叠从而使用户得到莫名其妙的运行结果。

 

3,  内存映射

内存映射技术可以将一个镜像,或者是文件,直接的映射到一个进程的地址空间中。内存映射使得文件数据直接和进程地址空间建立关联映射。也就是说一点将一个文件映射到某个进程的地址空间的一块区域中,任何对该区域数据的修改,都是在直接的修改该文件中的数据。

 

4,  相对合理的物理地址分配:

内存管理系统保证会公平的为每一个进程合理的分配实际的物理内存。

 

5,  共享虚拟内存:

虽然,虚拟内存直接,使得两个进程之间不会出现地址空间的重叠,但是,有时,你可能想让两个或多个进程共同操作同一个数据集。那么怎么弄?当然,可以想到的策略就是,划分一块内存空间,使得这两个进程能够同时访问。一个显而易见的例子就是Linux Bash,一个Bash下可以运行多个进程,不然的话,每次当要运行一个新的进程,就得创建一个新的Bash,那么会大大的浪费空间。

 

共享内存机制也能被当做IPC使用,IPC使得两个或两个以上的进程能够通过使用共享的一块存储区来进行数据交换,Linux系统支持SVR系统的IPC机制。

 

下面贴出一张虚拟内存的抽象模型:

 

 

 

 

这张图给我们提供了一个大致的虚拟内存结构图。

我们知道由于处理器执行的指令存放在内存中,处理器首先将程序指令从内存中提取出来,并且对指令进行解析,在解析指令的过程中,处理器可能需要获取或者存储一条或多条内存地址,然后处理器执行这条指令,并且从PC(程序计数器)获取下一条指令的地址,并循环上面的过程,这个过程叫做指令周期,可以发现一个指令周期包括取址周期和执行周期. 并且可以发现,其实处理器对内存的访问其实就是包括要麽获取指令,要麽获取数据

 

在一个支持虚拟内机制的系统中,所有的内存地址都是虚拟的内存地址,而不是真实的物理内存地址,在真实的处理环境中,毕竟真正存储指令和数据的是真实的物理内存,所以,必须提供一种方式,使得处理器能够将获取的虚拟内存地址转换为真实的物理内存地址,这项需求由操作系统来满足,操作系统通过维护一个表来提供这项服务,下面我们专门会讲操作系统维护的这个表。

 

上面说到,操作系统会维护一个表,这个表存储一些信息,这些信息记录的是虚拟内存地址和物理内存地址的映射关系,现在你可以花几秒钟想想这个表可能的的大体结构。

 

首先考虑物理内存可能有很多的字节,我们总不可能说是将虚拟内存中的地址映射单元按照字节级的大小和物理内存单元进行映射吧,所以,为了简化,首先将物理内存和虚拟内存都划分为很多的大小合理的块,我们把单个的一块叫做页面,在系统中,虚拟内存的页面的大小和物理内存的页面的大小必须是相等的,不然,你可以想想,在建立映射表的时候,肯定会非常的困难,运行在Alpha AXP 平台上的Linux内核的页面大小是8KB,X86平台上运行的Linux的页面大小是4KB,在每一个划分的页面都有一个唯一的编号,叫做页面帧编号(PFN)

 

在这种页面化的内存模型中,一条虚拟内存的地址由两个部分组成,一个是PFN页面帧编号,另外的一个就是偏移量。假如页面大小是4KB,虚拟内存地址结构中110位为偏移量,12位以上为页面帧号码。

 

每一次当处理器获取了一个虚拟内存地址之后,它需要将这个虚拟内存地址解析,首先获得该虚拟内存地址的页面帧号,然后将这个虚拟内存地址中的页面帧号映射到物理内存中的页面帧号,最后在通过虚拟内存地址中的偏移量,在物理内存的那一页中再定位具体的字节。

 

现在的问题是,处理器是从那里获取虚拟内存页面帧号与物理内存的页面帧号之间的映射关系的, 其实它是通过查询由操作系统维护的一个叫做页表的信息表完成任务的,下面我们来看看什么是页表。

 

如上面的图片,进程X,Y都有自己的页表,每一个进程被划分为多个页面(在虚拟内存中),进程X的页表中有一项将X进程的虚拟内存页面0映射到物理内存页面1Y 进程的页表中有一项将其虚拟内存页面1映射到物理内存页面4.

 

每一个页表项理论上应该包含下面的信息:

1, 活动标志,指示当前本页表项是否是可用的。

2, 本页表项描述的物理内存页面帧号。

3, 访问控制信息,这个信息描述这个页可能的使用标记,比如,是否是可写的,是否是可读的,是否包含可执行代码。

 

进程的页表项访问是通过虚拟内存页面帧标号进行索引的,比如页表项6存放的是虚拟内存页面帧5的映射信息,页表项1存放的是虚拟内存页面帧0的映射信息。

 

为了将一个虚拟内存地址翻译成真实的物理内存地址,处理器必须首先获取虚拟内存地址中记录的虚拟页面帧号,以及偏移量,通过将页面大小设置成2n次幂,地址的翻译工作能通过使用掩码和移位轻松完成,比如假设页面的大小为0x2000字节,一个虚拟内存地址0x2194能够被处理器翻译成  ( 偏移0x194, 页面帧号1 )

 

处理器使用进程的虚拟内存页面帧号作为其查询该进程的页表的索引,如果所查询的页表项中数据合法,那么,处理器就会提前该页表项中的存储的物理内存页面帧号,如果该页表项中的数据是非法的,进程将会遇到运行时错误,因为该进程的那个虚拟页面找不到和它对应的物理内存页面。此时处理器就不能解决这个问题,必须把控制权交给操作系统,让操作系统帮助完成这个异常处理。

 

假设如果页表项数据合法,那么处理器就会提前该页表项记录的物理内存页面帧号,然后给这个数字乘于页面大小的值,然后再加上偏移量,处理器就会获得这个虚拟内存地址对应的物理内存地址。

 

通过将虚拟内存地址映射到物理内存地址的机制,虚拟内存能够以任意的顺序被映射到物理内存上,例如,如上图,进程X的虚拟内存页面帧0被映射到物理内存页面帧1,但是虚拟内存页面帧7却被映射到物理内存页面帧0上,这就是虚拟内存机制为我们提供的另外的一种副产品,使得虚拟内存页面帧不必以某种特定的序列方式映射到物理内存页面帧。

 

 

请求式分页:

     在上面的一节中我们讲到,虚拟内存可以比物理内存寻址更大的空间,所以,摆在操作系统面前的一个问题就是,怎样管理这些物理内存,使得机器上有限的物理内存能够被更加合理的,高效的使用。

 

      操作系统解决这个问题的一种方案就是,只加载那些当前被执行程序使用的虚拟页面,例如,一个数据库交互程序可能使用来查询数据库,这种情况下,我们当然不需要将所有的数据一次性全加载进内存,我们只需要将那些被数据库查询软件请求的数据加载进物理内存就行了,这句大大的节省了有限的物理内存空间。所以这种,只把被请求的虚拟页面加载进物理内存的技术,叫做请求式分页技术(Demand Paging)

 

当处理器尝试着访问一个当前不在内存中的虚拟内存地址时,处理器就不能在页表中找到这条虚拟页面地址对应的页表项,例如,在上节的图中,进程X的页表中不存在进程X的虚拟页面帧2对应的页表项,所以任何的对处于虚拟内存页面2中的虚拟内存地址的访问,处理器都无法找到这个地址对应的物理内存的地址,所以,这时候,处理器就会向操作系统抛出一个页面异常。

 

如果虚拟内存地址是合法的,但是,这个地址所引用的页面不再内存中驻留,而在位于磁盘上的交换分区上,这时操作系统就会从磁盘上把这个页面,首先在物理内存中找一块空闲页面帧,然后将磁盘上的这个页面载入到这个物理内粗页面帧里面,由于,对磁盘的访问会耗费很长的时间,往往如果该进程是单线程的话,进程就会变成阻塞,然后等待磁盘访问的完成,这个阶段中,处理器可以处理其他的进程,当将磁盘上的页面加载进物理内存之后,该进程表上就会追加一项,来记录这条映射。然后这个过程完成之后,处理器就能继续执行这个进程的指令了。

 

Linux操作系统使用请求式分页来将可执行程序镜像加载进进程的位于德虚拟内存中,当一个命令执行的时候,包含这条命令的文件就会被打开,然后文件内容就会被映射进进程的虚拟内存空间。这个技术就是所谓的内存映射技术。

 

然而,要了解的是只有程序镜像的第一个部分被真正加载进了物理内存中,改程序镜像剩余的部分,依然是驻留在磁盘上的,当程序镜像被执行的时候,一旦处理器产生页面异常,,Linux操作系统就会使用进程内存映射来决定将磁盘上那一部分的程序镜像加载进内存中。

 

 

交换:

     如果一个进程需要将虚拟内存页面载入到物理内存页面中,但是当前却没有没有足够的物理内存空间,这个时候,操作系统就要发挥作用,操作系统必须为这个页面分配空间,方式就是,通过忽略其他页面对内存的请求。

 

      如果一个页面被修改了,操作系统必须负责维持页面的内容,因为这个页面可以在后来可能的请求中被再次的访问,这种类型的页面叫做,脏区(dirty page),当脏区被从内存中移除的时候,它会被保存到一个特殊的文件中,这种文件叫做交换文件,对于交换文件的访问代价十分的昂贵,所以,操作系统必须设计出一种算法,尽量的减少对页面过度转移,也就是,不要将一个页面不停地在交换文件和内存空间之间来回的移动,这是,非常耗费资源的。

 

       如果操作系统中用来从内存中选择需要交换的页面的算法很垃圾的话,系统付出的代价是非常非常大的,应该尽量的避免,将同一个页面不断地从交换文件和内存之间来回的移动,比如,像上面的虚拟内存模型图一样,如果,物理内存页面帧1要被经常地访问的话,将它忽然转移到交换文件的行为就是不恰当的。一个进程在运行时,经常要使用到的那些页面集,我们叫做(working sets),确保这些页面集长时间的驻留在内存中是很必要的。

 

       Linux操作系统中有很多种页面交换算法,其中一种就是,LRU,最近最少使用原则,这种方法对每一个页面赋予一个年龄(age),姑且将其看一个存在在内存中的页面真的有了生命了,操作系统记录每个页面被访问的次数,一个页面被访问的次数越多,那么我们说这个页面越年轻,越有活力,如果一个页面长时间没有被访问,或者是访问次数很少,那么我们说这个页面年龄大了,请将它交换进交换文件中吧。这种方法是一种有效地交换策略。

 

共享虚拟内存:

     前面我们提到过,虚拟内存机制使得多个进程之间共享同一个内存区域变得很简单,所有的内存访问都是通过页表进行映射的,并且,每一个进程都有自己的页表,对于两个共享同一个物理内存空间的进程来说,这个物理内存页面页帧号必须同时出现在这两个进程的进程表中,至于其分别对应这两个进程的哪个虚拟内存页帧,就不确定了。

 

物理内存寻址模式,虚拟内存寻址模式:

     让操作系统也在编制的虚拟内存空间中占有一个空间,是不是有点不必要了,如果将操作系统也编制在虚拟内存地址空间之中,那么想想,操作系统就必须为他自己维护一个页表了,很多的多任务处理器都支持物理内存寻址模式以及虚拟内粗寻址模式。当处理器处于物理内存寻址模式,系统中没有页表的感念,处理器不会进行任何的地址转换,Linux内核就是运行在物理内存地址空间的。

 

      Alpha AXP处理器没有特殊的物理寻址模式,相反,处理器将内存空间划分成多个区域,然后从这些区域之中选择两个作为物理映射地址,内核所在的地址空间叫做KSEG地址空间,这个地址空间包含物理内存地址0xfffffc0000000000以上的所有的地址。为了能够执行KSEG地址空间中的代码(肯定是内核代码了),或者从这儿访问数据,代码就必须在内核模式下执行,在Alpha处理器上,Linux内核代码从地址0xfffffc0000310000开始。

 

 

访问控制:

     在前面的介绍中我们知道页表项中包含的是虚拟内存页帧号所映射的物理内存页帧号,但是这儿要补充一点,页表项里面不仅仅包含的是映射关系,而且也包含了访问控制信息,由于处理器使用页表项来保存进程的虚拟内存地址到物理内存地址的映射关系,所以,对处理器来说,使用页表项中的访问控制信息来检查进程是否有权对内存的某一部分进行访问就变的很简单了。

 

在开始描述内存访问控制是如何做到的之前,我们有必要讨论下为什么要限制进程对特定内存区域的访问。一些内存区域,比如,包含可执行指令的内存区域,必须是只读的(read-only),操作系统必须阻止其他的进程或者该进程本身对包含该进程可执行指令的内存区域的写入操作。相比之下,包含程序数据的页面可以进行写入操作,但是,企图将这个内存区域里面的数据当做可执行指令执行的行为,必须被制止。大多数的处理器,都至少有两种工作模式,就是我们熟知的内核态,以及用户态。对于大多数的操作系统厂商来说,绝对不希望操作系统的内核代码能被一个系统用户不受任何限制的运行,当然也不希望操作系统的内核数据结构能够被运行在用户态下面的用户程序轻易地访问到。

 

下来是时候说说虚拟内存系统是如何实现内存访问控制的。因为,页表项中的内存访问控制信息是据处理器而异的,所以,这儿以Alpha AXP处理器为例,进行说明,其他的处理器虽然各有差异,但是大致结构是相似的。

 

下图列出Alpha AXP处理器的页表项结构:

 

 

从这张图里面我们能看出点信息:

 

可以看见页表项有64位,前32位是状态位,后32位存的是虚拟内存页帧映射的物理内存页帧号。我们从第0位开始,看每一位控制的是什么:

 

V : 如果这一位被设置的话,就表示这个页表项当前处于活动状态,也就是,这个页表项中保存了进程的一个虚拟内存页帧到物理内存页帧的映射,以及其他的信息。

 

FOE : Fault on Execute, 无论什么时刻,如果企图从这个页面中执行指令,处理器都会向操作系统提交一个页面错误,并将控制权交给操作系统。

 

FOW: “Fault on Write, 和上面的一样,不过这一次引起页面错误的原因是,企图对该页面进行写入操作。

 

FOR: “Fault on Read, 和上面一样,不过这一次引起页面错误的原因是,企图从该页面进行读操作。

 

ASM Address Space Match.当操作系统希望从地址解析缓存中清除一些项的时候使用到。

 

KRE: 指示,运行在内核模式下的程序可以读该页面。

 

URE: 指示,运行在用户模式下的程序可以读该页面。

 

GH: 控制是否使用一个变换缓存来映射一个块,还是使用多个变换缓存映射一个块。

 

UWE:控制,只有运行在用户模式下的程序能够对这个页面进行写入操作。

 

KWE: 控制,只有运行在内核模式下的程序能够对这个页面进行写入操作。

 

Page frame number: 对于处于活动状态的页面,也就是V位被设置的页表项,这个域包含的是这个页表项所映射的物理内存页帧号。对于V位未设置的,就是处于非活动状态的页面,如果这个域被设置为非0, 那么这个信息指示的是给页面在交换文件中的位置。

 

接下来的两个位,由Linux定义并使用:

_PAGE_DIRTY: 如果这意味被设置了,那么这个页面需要被写入到交换文件中。

 

_PAGE_ACCESSED

Linux 使用这一位来标记一个页面是否被访问过。

 

 

缓存(Cache)

前面的知识应该已经使你能够对内存管理系统在你脑子中建立起来了一个基本框架模型了,但是,如果你按照上面的理论来真实实现你的模型的话,我得说,你所实现的模型可能工作起来不是很高效。要知道无论是操作系统设计师还是处理器设计师,他们都在不遗余力的努力将系统设计的更加高效,低效的运行缓慢的系统立即使使用者厌烦了。 所以下面我们讨论下在系软硬件统设计时,设计师是是用什么手段来进一步提高软硬件系统的性能的。 

 

 

      当然努力地设计出更快的处理器和更快更大的内存是革新的根本,但是当从这些途径改进已经达到饱和的时候,找到另外的提高性能的方法就很必要了。

 

      设计师们通过给系统中引进cache(缓存)来进一步提升系统性能,cache作为一个将系统需要的信息和数据暂存起来的场所而发挥作用,使得系统的一些操作变得更快。Cache不仅仅使用在硬件设计上,而且在软件设计上也被广泛采用。

 

       Linux操作系统中就使用了大量的与cache相关的内存管理技术,下面罗列出Linux系统中的Cache:

 

 

Buffer Cache(缓冲区缓存)

       缓冲区缓存存放的是块设备驱动所使用的数据缓冲区。

 

       这些缓冲区一把都是有固定大小的(大多数为512字节),包含的东西要么是从块设备读取的数据块,要么是马上要写入到块设备的数据块。

 

       我们知道,块设备每次只能以固定块大小的方式读取或者写入,比如磁盘,最小的操作单元式扇区,而一个扇区一般为512字节,所以对于块设备来说,读取和写入的操作单元不可能是一个字节一个字节的来的。

 

 

       缓冲区缓存通过使用设备标识符以及块编号来索引。块设备只能通过缓冲区缓存来访问,如果数据能够在缓冲区缓存找到的话,那么就没有必要再从物理磁盘上查找这个数据而浪费时间,因为对磁盘的访问比处理器处理数据慢将近6个数量级。

 

 

 

 

页面缓存:

       页面缓存用来加速从磁盘上访问数据和可执行程序的二进制镜像。

 

 

 

 

交换区缓存:

       只有修改过的页面才能保存在交换文件中。只要页面在被写入到交换文件之后未修改,那么下一次该页面被交换出的时候,就没有必要将这个页面写入交换文件了,因为该页面已经在交换文件中了,这样就能省略很多的无用的页面交换操作,由于页面交换操作是内存与磁盘之间的I/O操作,所以,这样可以大大减少系统开销。

 

 

硬件缓存:

       硬件缓存的一个例子出现在处理器中,根据统计发现,内存中大量的页面中,往往只用很少的页面会被高频率的访问,所以,为了加快处理器对虚拟内存地址到物理内存地址的转换,设计师在处理器(更精确的是在MMU中)中引入TLB(Translation Look-aside Buffers)缓冲,TLB其实保存的是一个或多个进程的页表项中的几条。下面说说TLB是如何工作的。

 

       当一个虚拟内存地址被处理器获取之后,下来由处理器中的MMU(内存管理单元)将虚拟内存地址转换为物理内存地址,首先MMU会在TLB缓存中查询,看看该地址所在的虚拟内存页帧号是否存在于TLB缓冲中,如果有,那么在看看其他的访问控制信息,比如,是否允许读,等等。如果,该虚拟内存地址所在的虚拟内存页帧号不在TLB中保存,那么处理器才会进行一次常规的进程的页表查询,然后根据一个替换算法,将新查询到的页表项替换进TLB缓冲中,所以,可以看见,TLB可以提高性能。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值