Chinaunix首页 | 论坛 | 博客
  • 博客访问: 150226
  • 博文数量: 14
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 145
  • 用 户 组: 普通用户
  • 注册时间: 2014-02-12 15:27
个人简介

文章分类

全部博文(14)

文章存档

2014年(14)

分类: LINUX

2014-04-14 10:52:27

引言

这里我们选择C语言作为编程语言,因为它能帮我们实现对程序的完全控制,并达到高的性能。许多人认为,提升性能就是尽可能的减少CPU指令。然而在现代的硬件架构上,性能要考虑的因素复杂得多,绝不仅仅是CPU。程序要处理内存,CPU,磁盘和网络I/O等等。每一项处理都会增加程序的开销,每一项处理必须被正确理解以保证程序的性能和可靠性。

就像程序的复杂度会影响CPU性能一样,影响磁盘读写、网络延迟的因素也很好理解。然而,对内存的影响因素似乎不太好理解。我们的客户经验表明,即便是广泛使用的工具,如top,大多数系统管理员还是无法理解其输出。

本文是关于内存系列文章五篇中的第一篇。我们将要讨论的话题包括:内存的定义、内存是如何管理的、如何阅读工具的输出信息等……这一系列将专注于开发人员和系统管理员都感兴趣的话题。尽管其中大部分规则适用于大多数现代操作系统,我们的讨论更偏向于Linux系统和C语言。

我们不是第一个写关于内存文章的。我们强烈推荐Ulricht Drepper的经典文章《What every programmer should know about memory》。

本文将给出内存的定义,并假定读者至少对地址或者进程等概念有基本的认识。它也会经常涉及到一些内容,如系统调用、用户态与内核态的差异,不过你所需要知道的是用户态的进程运行于与硬件交互的内核之上,通过系统调用,进程可以与内核通信来获取更多的资源。通过阅读相应的手册,你可以获得系统调用的详细说明。

虚拟内存

在现代操作系统中,每个进程都运行在各自独立的内存空间。与直接将内存地址映射到硬件地址不同,操作系统作为一个硬件抽象层,为每个进程创建了独立的虚拟内存空间。通过使用内核为每个进程维护的转换表,由CPU完成物理内存地址与虚拟地址间的映射。 (每次内核切换CPU核上运行的进程时,它同时会改变该CPU上的转换表)。

译者注:这里转换表应该指PTBR寄存器,进程切换时会设置相应的PTBR寄存器。进行地址转换时,先通过GDT完成逻辑地址到线性地址的转换,再完成线性地址到物理地址的转换(线性地址到物理时,先查TLB,TLB没查到时,再通过PTBR搜索页表项)。

虚拟内存设计的目的。首先,它实现了进程间的隔离。 一个用户态的进程只能通过虚拟内存地址进行内存访问。这样它就只能访问已映射到自己虚拟地址空间中的数据,因此它无法访问其他进程的内存(明确共享的除外)。

第二个目的是硬件抽象。内核可以随意更改虚拟地址所映射的物理地址。它也能选择在真正需要时才为某一虚拟地址映射物理内存。此外,当内存长时间没有被使用并且系统的物理内存吃紧时,它可以将内存换出到磁盘上。这为内核提供了很多自由,唯一的限制是,当程序读取内存时,它实际上去磁盘上读前面写的内容。

第三个目的是可以为不在RAM中的对象分配地址。这是mmap和文件映射的原理。你可以将虚拟内存地址映射到文件,这样对文件的访问操作就好比访问一段内存缓冲区。这是一个非常有用的抽象,它有助于保持代码的简洁,而在64位系统中,你有一个巨大的虚拟地址空间,如果你愿意,你可以将整个硬盘驱动器映射到虚拟内存。

第四个目的是共享。由于内核知道各个运行进程虚拟空间的映射情况,它可以避免对象在内存中的重复加载,让使用相同资源的各进程的虚拟地址指向同一块物理内存(即使该虚拟地址因进程而异)。共享的一个好处是内核的写时拷贝(copy-on-write, COW)机制:当两个进程使用相同的数据,其中一个进程修改了它,而另一个进程不允许看到这种变化,内核会在数据被修改时进行数据拷贝。最近,操作系统也实现了对不同地址空间中相同内存的检测,并能自动将它们映射到相同的物理内存(将他们标记为COW对象),在Linux系统中,这个机制被称为KSM(Kernel SamePage Merging)。

fork()

采用COW机制广为所知的是fork()函数。在类Unix系统中,fork()是一个系统调用,它通过复制当前进程来创建一个新进程。当fork()返回时,两个进程在同一点继续往下执行,并且它们拥有相同的已打开文件句柄和相同的内存。(这里应该指映射的物理内存相同)由于COW机制,fork()不会将一个进程的物理内存复制两份,只有被父进程或子进程修改的数据会在RAM中复制。通常使用fork()后紧接着会调用exec(),它会使原有的虚拟地址空间无效,COW机制避免了对父进程内存完全无用的拷贝。

译者注:调用exec类函数后,操作系统会load新的程序以替换当前进程(进程号会被保留),在这个过程中会替换原进程的代码段,并更新数据段和堆栈段。

COW机制的另一个作用是,fork()可以用很小的代价创建一个进程(私有)内存的快照。如果你想在一个进程的内存中执行一些操作,同时能确保它没有被修改的风险,并且不想增加开销、采用容易出错的锁定机制,那么用fork吧,在子进程中完成你的工作,并将计算结果返回给父进程(通过返回的值、文件、共享内存、管道等方式)。

只要你的计算足够快,这样父进程和子进程间仍然会共享很大一部分内存,它将工作得非常好。它同样有助于保持你的代码简单,程序的复杂性被隐藏在内核的虚拟内存代码中,而不是在你的代码中。

虚拟内存以页来划分。页的大小由CPU决定,通常是4KiB。这也意味着在内核中内存管理的粒度以页为单位。当你需要新的内存时,内核分配给你一个或多个页面,同样当你释放内存时,也会释放出一个或多个页面……每个更好粒度的内存分配API(如malloc)都是在用户态实现的。

对于分配的每一个页面,内核会进行一系列的权限控制:页面可读、可写和/或可执行(注意:并不是所有的组合都是可能的)。这些权限要么在进行内存映射时设置,要么通过mprotect()系统调用设置。尚未分配的页面是没有访问权限的。(备注:这里的未分配可能指未进行物理内存的映射)当你试图在页面上执行禁止的操作时(例如,没有读权限却从页面中读取数据),你就会触发段错误(在Linux系统中)。顺便说一下:你可能会看到这样的情况:由于段错误是以页作为粒度,你可以执行缓冲区外的访问却没有导致段错误。

译者注:假设页A没有读权限,p指向页A中的某个地址,*(p+offset),offset > 4K,访问的是另一个页,而这个页有读权限。

内存类型

在虚拟内存空间中分配的内存并非都是相同的类型。我们可以通过两条坐标轴对其分类:横轴代表内存是私有的(该进程所特有的)或共享的,纵轴表示内存是文件映射的或非文件映射的(这种情况用匿名表示)。这就创建一个分类方法,它包括4种内存类型:

私有内存

私有内存,顾名思义是指每个进程所特有的内存。实际上在程序中处理的内存大部分是私有内存。

由于私有内存的修改对其它进程不可见,所以它是写时拷贝的。另一方面这也意味着,即便内存是私有的,几个进程仍可能共享相同的物理内存来存储数据。特别是二进制文件和共享库的情况。一个常见的错误认识是:因为每个进程会加载Qt和KDElibs所以KDE会占用很多RAM。事实上,因为COW机制,对于这些库的只读部分所有的进程将会使用完全相同的物理内存。

对于文件映射的私有内存,进程所做的修改不会被写回底层文件,而对文件所做的修改可能会也可能不会对进程生效。

共享内存

共享内存是为进程间通信设计的。只有通过正确使用mmap()或专门的调用(shm*系列函数)来明确请求共享内存时才会被创建。当一个进程写共享内存时,所有映射了相同内存的进程都能看到修改。

对于文件映射的内存,任何映射了该文件的进程都会看到文件中的修改,因为这些修改通过文件自身传递。

匿名内存

匿名内存是纯粹在RAM中的。但在被写之前内核实际上不会将它映射到物理地址。因此,在实际被使用前,匿名内存不会给内核添加任何压力。这就允许一个进程在它的虚拟内存空间中保留大量内存而不真正使用RAM。结果就是内核可以让你保留比实际可用还要多的内存。这种行为往往被引申为过量使用(或内存过量使用)。

文件支持与交换

当内存属于文件映射时,数据是从磁盘上的文件加载的。大部分时候它是在需要时加载的,然而你可以给内核提示,以便它在读之前能进行预取操作。当你了解自己程序的特定访问模式时(通常是顺序访问),这有助于让程序更快速。为了避免使用太多的RAM,你也可以告诉内核,无法进行内存映射时,也不用非要为页面分配RAM了。所有这些都可以使用madvise()系统调用完成。

当系统的物理内存不足了,内核会尝试将一些数据从RAM交换到磁盘。如果内存是文件映射和共享类型,这就很容易了。因为文件是数据源,数据只是从RAM中移除了,那么下一次读取时,它还会从文件中加载。

内核也可以选择将匿名或私有内存从RAM中移除。在这种情况下,数据会被写到磁盘上的特定位置。这也被称为被换出。在Linux系统中,交换的数据通常存储在一个特定的分区,在其他系统中可能是一个特殊的文件。然后,它像文件映射的内存那样操作:当它被访问时,从磁盘上读取并重新加载到RAM。

由于使用了虚拟地址空间,交换页面的换进换出对于进程来讲就是完全透明的…然而由磁盘I/O产生的延时是能被感知到的。

下一篇:常驻内存和工具介绍

这里我们介绍了一些关于内存的重要概念。尽管我们谈论了一些关于物理内存的内容,以及它与保留地址空间的差异,我们还是回避了对进程实际内存压力的处理。在下一篇文章中,我们将讲这方面的内容,并介绍了一些工具,让你了解进程中的内存消耗。


1.现在,家用CPU已具有48bit的地址带宽,这意味着2^48字节的寻址空间,即256TiB。

2.这在你拥有的大量内存多次出现时是非常有用的,例如,当你运行一个虚拟服务器并且上面运行多个相同操作系统的虚拟机。

3.在Intel的CPU上,还有一个备选的页大小:2MiB,然而在Linux系统上可用的2MiB大小的页数量是有限的,并且需要映射一个伪文件,这使得它们很难用。


来源声明:本文来自Intersec Tech Talk的博文《》,由IDF实验室童进翻译。

(全文完)

阅读(3458) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~