[Pthread] Linux中的内存管理(一)--Paging
前几次我们分析了程序是如何在Linux上被加载运行的。这次我们尽量简洁的分析一下在Linux中,程序是如何使用内存,而系统又是怎样管理内存的。
内存访问和管理是一个跨越应用程序,操作系统,硬件平台的一个复杂的过程。虽然过程很复杂,我们还是尝试着去理解一下。
1. IA-32架构中的内存管理机制
我们从硬件开始吧,内存归根结底是硬件,内存(Random Access Memory)在加电的情况下,在地址总线上放上要读取/写入的地址,然后就可以在控制信号的帮助下,从数据总线上得到/写入相应地址的数据了。数据总线的宽度通常与处理器寄存器位数相同(这也是8位机,16位机等叫法的由来)。这是内存的硬件特征。
有了这么个硬件内存,系统应该怎么使用呢?我们来看看硬件平台是如何设计的
最开始的时候,如在Intel 8086上,使用的是被后来称为"实地址"的模式。在实模式下,直接在程序中使用物理地址来访存,因为程序所使用的物理地址可能冲突,这给程序开发和运行带来了很大的困难。那个时候的程序员在开发应用程序的时候,如何合理有效的使用内存是相当重要的话题。
此后,为了便于程序的开发,提高系统的多道任务和系统的稳定性。从80286开始,处理器提供了"保护模式",逐渐引入了段,页等内存管理机制。最先引入的是分段处理机制,在分段处理中,系统有个全局的GDT(Global Descriptor Table),而每个进程能够有自己的LDT(Local Descriptor Table),程序也被分为数据段,代码段,堆栈段,其基地址分别存储在不同的段寄存器中。程序中使用的是逻辑地址,而逻辑地址通过段基地址+偏移量的方式映射成为物理地址。这样一来进程使用的地址空间就被不同的段基地址分开了。
但是分段还是会有一些问题,比如段不定长,但段内物理内存要求连续等。所以之后系统又引入了分页机制。在分页机制中,内存被分成了固定大小的页(常见的是4KB)。不同的进程通过使用不同的内存页来分开,因为页大小固定,所以方便了系统的管理和维护。
在常见的IA-32体系下,同时使用了段页式内存管理。其规定程序中使用的是逻辑地址,逻辑地址通过段映射机制变成线性地址,线性地址通过页映射机制最后变成物理地址。即:
逻辑地址---->段映射---->线性地址---->页映射---->物理地址
当然这只是硬件厂商的设计,他们按照这个设计来提供了辅助的硬件,如段寄存器, MMU, TLB等。在这个硬件平台上运行的软件(操作系统)也受这个设计的限制,或者说从这个设计中得益。
2. Linux中的内存管理机制
Linux(for IA-32)基本上遵循了上一节提到的设计。当然这样做的目的也是为了充分利用硬件,发挥硬件的最大效率。
首先在Linux中程序使用的是也是逻辑地址,逻辑地址空间是每个进程独占的,其大小通常为4G,也常常被称为虚存。虚存的大小与物理内存的大小并没有关系。即一个使用了1G虚存的程序,也可以运行在只有256M物理内存的机器上面。
接着在Linux中也使用了段映射,不过在Liunx中所有的段寄存器都指向一个相同的段地址范围,即所有进程的线性地址是相同的,都是从0x00000000到0xffffffff(0-4G)。简单点说就是在Linux中段映射并没有什么实质的作用,经过段映射的线性地址==逻辑地址。因为所有进程的段空间都一样,所以大家共享一个全局的GDT,而没有使用LDT。
接着是页映射,因为段映射什么事情都没干,所以从逻辑地址转换到物理地址的工作都由页映射来处理。在Linux中,线性地址被分组成定长的页,物理地址也被分成定长的页(4k),两者是一一对应的,便于管理和维护。每个进程有自己独立的页面目录PGD,保存在进程的mm_struct结构中。当进程切换的时候,内核将该进程的页目录物理地址放到控制寄存器CR3中,然后系统根据页目录找到相应的页表地址,然后根据相应的页表,找到最终的物理地址(这个物理地址的寻址上限也是4G,即最大支持4G的物理内存,当然有别的技术提升,这里就不谈了)。
看完这段大家可能会有个两个问题,为什么Linux不使用分段机制? 既然不使用分段机制,为什么还要保留它?
第一个问题刚才提到过,即分段机制有其缺陷,即段长不定,而且这些段要么全部换入,要么全部换出,粒度太大。定长的分页机制在内存利用,管理上都有其优势。此外,分段机制有着Intel历史遗留的性质,为了方便迁移到那些不支持分段机制的平台上,只好在段映射时少做事情。
那么为什么不能直接抛弃分段机制,只保留分页机制呢?这是因为在IA-32体系中规定段模式是不能被禁止的,反而分页模式是可选的,也就是说在Intel平台上必须要进行段映射(主要是为了MMU),Linux不能直接从逻辑地址就直接映射到物理地址。因此Linux只能使用基地址为0,大小为4GB的段来绕过这个问题,这样的段实际上是没有什么意义的(这种模式也被称为flat模式)。
在Linux上,访存的大致过程是这样的:
程序的逻辑地址==段映射后的线性地址---->页映射---->物理地址
经过这一系列操作,一个虚存空间的逻辑地址就变成了内存上的物理地址。通常情况下物理内存都小于进程虚存的大小(4G),即便是物理内存较大,多道程序运行也允许多个进程"同时"占用着物理内存,所以物理内存通常是不够的。需要Linux把某些暂时用不到的页面从物理内存中换出,将挪出的内存交给最需要的进程。当要重新使用那些被交换出去的页面的时候,访存指令会触发一个Page-Fault异常,在异常处理函数中把换出的页面再装回内存中,当然此时可能需要再换出另一个页面。通常被换出的内存放到磁盘的一个特殊分区中,即swap分区(在Windows上叫虚拟内存,注意和进程的虚存空间区别)。所以当你的内存不够时,可以适当的调大swap的大小,当然因为性能的原因扩展内存容量可能更有效果。
小结:
1. IA-32中的地址转换:逻辑地址-->段映射-->线性地址-->页映射-->物理地址
2. 分段机制和分页机制,理论上是项目独立的,既可以只使用分段,也可以是使用分页,当然也可以混合使用。
3. Linux采用的是基于分页机制的内存管理,但是为了和Intel平台兼容,保留了段映射操作。
4. Linux除了要参与从逻辑地址到物理地址的寻址过程,还要负责物理内存页面的换入换出工作。
5. 从应用程序的角度,只需要考虑逻辑地址的使用就可以了,也就是好好利用自己独占的这4G虚拟内存。
在本文的开头,我说过内存访和管理是一个跨越应用程序,操作系统,硬件平台的一个复杂的过程。这次我们看了看硬件平台和操作系统做了些什么事情,下次我们将站在应用程序的角度,看看虚存是如何被使用和管理。
To Be Continued
Pthread 08/01/20
内存访问和管理是一个跨越应用程序,操作系统,硬件平台的一个复杂的过程。虽然过程很复杂,我们还是尝试着去理解一下。
1. IA-32架构中的内存管理机制
我们从硬件开始吧,内存归根结底是硬件,内存(Random Access Memory)在加电的情况下,在地址总线上放上要读取/写入的地址,然后就可以在控制信号的帮助下,从数据总线上得到/写入相应地址的数据了。数据总线的宽度通常与处理器寄存器位数相同(这也是8位机,16位机等叫法的由来)。这是内存的硬件特征。
有了这么个硬件内存,系统应该怎么使用呢?我们来看看硬件平台是如何设计的
最开始的时候,如在Intel 8086上,使用的是被后来称为"实地址"的模式。在实模式下,直接在程序中使用物理地址来访存,因为程序所使用的物理地址可能冲突,这给程序开发和运行带来了很大的困难。那个时候的程序员在开发应用程序的时候,如何合理有效的使用内存是相当重要的话题。
此后,为了便于程序的开发,提高系统的多道任务和系统的稳定性。从80286开始,处理器提供了"保护模式",逐渐引入了段,页等内存管理机制。最先引入的是分段处理机制,在分段处理中,系统有个全局的GDT(Global Descriptor Table),而每个进程能够有自己的LDT(Local Descriptor Table),程序也被分为数据段,代码段,堆栈段,其基地址分别存储在不同的段寄存器中。程序中使用的是逻辑地址,而逻辑地址通过段基地址+偏移量的方式映射成为物理地址。这样一来进程使用的地址空间就被不同的段基地址分开了。
但是分段还是会有一些问题,比如段不定长,但段内物理内存要求连续等。所以之后系统又引入了分页机制。在分页机制中,内存被分成了固定大小的页(常见的是4KB)。不同的进程通过使用不同的内存页来分开,因为页大小固定,所以方便了系统的管理和维护。
在常见的IA-32体系下,同时使用了段页式内存管理。其规定程序中使用的是逻辑地址,逻辑地址通过段映射机制变成线性地址,线性地址通过页映射机制最后变成物理地址。即:
逻辑地址---->段映射---->线性地址---->页映射---->物理地址
当然这只是硬件厂商的设计,他们按照这个设计来提供了辅助的硬件,如段寄存器, MMU, TLB等。在这个硬件平台上运行的软件(操作系统)也受这个设计的限制,或者说从这个设计中得益。
2. Linux中的内存管理机制
Linux(for IA-32)基本上遵循了上一节提到的设计。当然这样做的目的也是为了充分利用硬件,发挥硬件的最大效率。
首先在Linux中程序使用的是也是逻辑地址,逻辑地址空间是每个进程独占的,其大小通常为4G,也常常被称为虚存。虚存的大小与物理内存的大小并没有关系。即一个使用了1G虚存的程序,也可以运行在只有256M物理内存的机器上面。
接着在Linux中也使用了段映射,不过在Liunx中所有的段寄存器都指向一个相同的段地址范围,即所有进程的线性地址是相同的,都是从0x00000000到0xffffffff(0-4G)。简单点说就是在Linux中段映射并没有什么实质的作用,经过段映射的线性地址==逻辑地址。因为所有进程的段空间都一样,所以大家共享一个全局的GDT,而没有使用LDT。
接着是页映射,因为段映射什么事情都没干,所以从逻辑地址转换到物理地址的工作都由页映射来处理。在Linux中,线性地址被分组成定长的页,物理地址也被分成定长的页(4k),两者是一一对应的,便于管理和维护。每个进程有自己独立的页面目录PGD,保存在进程的mm_struct结构中。当进程切换的时候,内核将该进程的页目录物理地址放到控制寄存器CR3中,然后系统根据页目录找到相应的页表地址,然后根据相应的页表,找到最终的物理地址(这个物理地址的寻址上限也是4G,即最大支持4G的物理内存,当然有别的技术提升,这里就不谈了)。
看完这段大家可能会有个两个问题,为什么Linux不使用分段机制? 既然不使用分段机制,为什么还要保留它?
第一个问题刚才提到过,即分段机制有其缺陷,即段长不定,而且这些段要么全部换入,要么全部换出,粒度太大。定长的分页机制在内存利用,管理上都有其优势。此外,分段机制有着Intel历史遗留的性质,为了方便迁移到那些不支持分段机制的平台上,只好在段映射时少做事情。
那么为什么不能直接抛弃分段机制,只保留分页机制呢?这是因为在IA-32体系中规定段模式是不能被禁止的,反而分页模式是可选的,也就是说在Intel平台上必须要进行段映射(主要是为了MMU),Linux不能直接从逻辑地址就直接映射到物理地址。因此Linux只能使用基地址为0,大小为4GB的段来绕过这个问题,这样的段实际上是没有什么意义的(这种模式也被称为flat模式)。
在Linux上,访存的大致过程是这样的:
程序的逻辑地址==段映射后的线性地址---->页映射---->物理地址
经过这一系列操作,一个虚存空间的逻辑地址就变成了内存上的物理地址。通常情况下物理内存都小于进程虚存的大小(4G),即便是物理内存较大,多道程序运行也允许多个进程"同时"占用着物理内存,所以物理内存通常是不够的。需要Linux把某些暂时用不到的页面从物理内存中换出,将挪出的内存交给最需要的进程。当要重新使用那些被交换出去的页面的时候,访存指令会触发一个Page-Fault异常,在异常处理函数中把换出的页面再装回内存中,当然此时可能需要再换出另一个页面。通常被换出的内存放到磁盘的一个特殊分区中,即swap分区(在Windows上叫虚拟内存,注意和进程的虚存空间区别)。所以当你的内存不够时,可以适当的调大swap的大小,当然因为性能的原因扩展内存容量可能更有效果。
小结:
1. IA-32中的地址转换:逻辑地址-->段映射-->线性地址-->页映射-->物理地址
2. 分段机制和分页机制,理论上是项目独立的,既可以只使用分段,也可以是使用分页,当然也可以混合使用。
3. Linux采用的是基于分页机制的内存管理,但是为了和Intel平台兼容,保留了段映射操作。
4. Linux除了要参与从逻辑地址到物理地址的寻址过程,还要负责物理内存页面的换入换出工作。
5. 从应用程序的角度,只需要考虑逻辑地址的使用就可以了,也就是好好利用自己独占的这4G虚拟内存。
在本文的开头,我说过内存访和管理是一个跨越应用程序,操作系统,硬件平台的一个复杂的过程。这次我们看了看硬件平台和操作系统做了些什么事情,下次我们将站在应用程序的角度,看看虚存是如何被使用和管理。
To Be Continued
Pthread 08/01/20