Archive for the ‘翻译’ Category

抽象代数与计算-Monoid

Posted on 七月 20th, 2010 in 翻译 | 4 Comments »

作者:Mark C. Chu-Carroll
翻译:Fleurer
原文:http://scienceblogs.com/goodmath/2008/02/abstract_algebra_and_computati.php

在前两篇post,我们以范畴的角度观察了群论。范畴论给了我们一个全新的视角来理解对偶。与传统代数不同,范畴论的对偶是从groupoid中出现的,而群则被视为是带对偶的更简单的结构。

看其它的代数结构也是同样。比如环(rings),换个视角,也可以见到一个大不相同的环。

在看范畴论下的环之前,我们不妨先看个简单点的结构。在范畴中,环是通过monoid表示的。不过Monoid绝不仅仅只是环前面的开胃菜,它本身就是好东西。

它们的魅力在什么地方?首先,它们是范畴和代数之间的一座桥梁。我们知道,范畴中的groupoid(广群)使得群论无缝地归入了范畴论。Monoid也可以同样:它们也有范畴中的等价物。而且,Monoid还有地地道道的实际应用——你可以用Monoid表示计算。而且也许不广为人知,计算机科学中很多得力的基础工具其实都是Monoid。

我们先用代数的视角看下,呆会再换到范畴视角。在抽象代数中,Monoid正好跟复合函数的思想一致。开窍了吧,范畴论的基本思路就是复合函数的抽象化!

Monoid跟群类似,是一组支持二元运算的值的集合。Monoid更简单——它只要组合,不要逆元。我们令Monoid里面值的集合为M,二元运算为º,就有三条性质:

1. 封闭性(Closure): ∀a,m∈M: aºb ∈ M
2. 单位元(Identity): ∃ i∈M : ∀f∈M : iºf = fºi = f.
3. 结合律(Associativity): ∀ a,b,c∈M: (aºb)ºc = aº(bºc).

这就是了。有这几条性质,想想函数和复合函数,套进去就满靠谱了。这对大多数函数都适用,于是所有的函数都成了对象,比如定义域和值域都是自然数的函数。封闭性要求两个简单完全函数复合的结果依然是简单完全函数;单位元可以是一个函数f(x)=x;交换律是交换表达式中元素的顺序,不影响最终结果。这都是复合函数天生的性质。

如果拿monoid像群一样,给它加一个操作又会怎样?答案对我们学计科的同学再熟悉不过了:有限自动机!

取一个monoid,(M,º)。我们可以在集合S上定义这monoid的一个操作,也就是:*:M×S→S,取M的一个值和S的一个值,得S的一个值。这个monoid操作显示了两条性质——都是来自monoid。

1. Identity: ∀s∈S, i*s=s.
2. Associativity: ∀a,b∈M, ∀s∈S: a*(b*s) = (aºb)*s.

这意味着什么?意味着我们给这个monoid加上了函数应用。这个monoid操作就是M中函数的应用。

这样,有了一个组可组合的函数,一个可以应用到函数的特定集合。可以得到什么?

一台自动机——也就是一种数学上的计算模型。

这个自动机是怎么出来的?

在这个monoid操作里,monoid的每个成员都是函数,也就是值到值的映射;组合符将这些函数链到一起。对自动机来说,monoid中的每个成员都是计算中的一个步骤,组合符将这些步骤连续起来。这还不是完整的计算——但已经靠一个简单的形式,前进了一大步。

多想想。还记得lambda演算?它就是一个表示计算的逻辑工具,里面除了函数什么都没有。想到了吧,我们刚刚只是重新发明了lambda演算的一部分,以抽象代数的思路。

在以后的post里面我会多讲些——不过我得先给你过上一遍,在范畴论里面这些东西都是什么样子——下一步往哪走,在范畴的大陆上清晰无比。

关于GC的FAQ

Posted on 三月 19th, 2010 in 翻译 | 19 Comments »

翻译:ssword
原文:http://www.iecc.com/gclist/GC-faq.html

这是GC邮件列表的FAQ草稿,欢迎评论、标注或贡献内容。它暂时分为三(?)部分,大约是一般问题,技巧及算法,语言接口和高级论题。以后内容要是多了,这些内容可以重新组织便于查阅。

我们也提供了这些内容的文本文件,即GC-algorithms.txt, GC-lang.txt, and GC-harder.txt。

这里一些内容的学术气可能要差些,而更偏重于传道(或者更确切地说,是在没技术成分有减少的前提下增加点传道的因素)。关于垃圾收集多好多好之类,言辞越简洁越好。

Common questions
常见问题

What is garbage collection?
什么是垃圾收集?

垃圾收集可以是语言运行时的一部分,也可以是个附加库。它们能够与编译器、硬件或操作系统打交道,自动检查出程序中不再使用的内存,并将其回收利用。这也称作“自动存储(内存)回收”。

Why is it good?
它好在哪?

手工内存管理既浪费程序员的生命,也容易引入错误。相当多的程序都难免于内存泄漏,使用了错误处理或线程的程序尤其如此。

未使用过垃圾收集的朋友可能难以察觉它的第二大好处,那就是不必纠结于内存管理的细节(“谁来回收这块内存”),从而简化了程序组件(子程序、库、模块、类)间的接口。

你可以在 Object-Orientation FAQ — 3.9) Why is Garbage Collection A Good Thing?了解更多内容

Is garbage collection slow?
垃圾收集会不会很慢?

并非如此,现代的垃圾收集器的速度几乎可与手工内存管理(malloc/free或new/delete)相媲美。垃圾收集可能不如特定程序中相应的allocator快,不过为保证手工allocator正常工作而添加的额外代码(如引用计数)往往会使得程序比垃圾收集更慢,共享资源的多线程程序中尤其如此。

正如开始所说,内存已经足够便宜,垃圾收集可以应用到非常大的堆上(比如1GB)。活动内存如果足够的大,暂停时间依然是个问题。不过对于现代的垃圾收集器而言,其暂停时间通常也就0.1秒,对人类的交互几乎可以不计。要是小块活动内存,暂停时间就更少了:0.01秒以下。

Can I use garbage collection with C or C++?
我可以在C\C++中使用垃圾收集吗?

应该可以。对于存在遗留代码的C和C++可能还差些,不过现代的垃圾收集器(测试良好,高效,无暂停)几乎已经支持了一切。了解更多请看GC, C, and C++ 。

Does garbage collection cause my program’s execution to pause?
垃圾收集会不会让我的程序暂停?

不必。有不一的算法允许垃圾收集并行化、增量化甚至“实时化”。比如C\C++下边就有增量式的垃圾收集。

Where can I get a C or C++ garbage collector?
怎样搞到C\C++的垃圾收集?

Boehm-Weiser collector
http://www.hpl.hp.com/personal/Hans_Boehm/gc/ or
ftp://parcftp.xerox.com/pub/gc/gc.html
Great Circle from Geodesic Systems or 800-360-8388 or http://www.geodesic.com
Kevin Warne or 800-707-7171

Folk myths
坊间传闻

  • GC一定不如手工内存管理快
  • GC一定会让程序暂停
  • 手工内存管理就不暂停
  • GC与C\C++水火不容

Folk truths
其实…

  • 大部分动态创建的对象其实少有与其它对象的关联,通常其引用数就是1。
  • 大部分对象的生存期都很短。
  • 对象大小、生存期呈爆炸式分布,而不是正态分布。
  • VM很重要
  • 缓存很重要
  • 不成熟的优化乃万恶之源。

Tradeoffs
权衡

  • 严格式 vs. 保守式
  • 移动/压缩 vs. 无移动
  • 暂停 vs. 增量 vs. 并行
  • 分代 vs. 无分代

GC, C, and C++

What do you mean, garbage collection and C?
你说什么,C语言的垃圾收集?

可以引入一个垃圾收集器自动管理内存,从而代替malloc和free的手工申请或释放。通常的做法是将malloc替换为垃圾收集器的allocator,而free则替换为一个空函数。比如X11就是如此。

也可以令free依然生效,不过垃圾收集器就弱化为一个防弹衣的存在,堵住潜在的内存泄漏。这一做法也是有了很多应用,并且工作良好。好处是方便程序员手工管理内存,而程序员顾不到的地方,就交给垃圾收集。这不一定比空free风格的快,不过可能让堆变得小点。

How is this possible?
如何做到的?

C兼容的垃圾收集可以知道指针在什么地方(例如”bss”,”data”和堆栈里面),保留在堆中的数据结构可以让它们很方便就能找出哪段数据是可能的指针。来个搜索遍历所有可以访问的指针,剩下没被访问的就是垃圾。

This doesn’t sound very portable. What if I need to port my code and there’s no garbage collector on the target platform?
这个听起来移植性好像不怎么样。我需要将代码移植到没有垃圾收集器的平台上该怎么办?

有些代码一定是平台相关的,但是大多操作系统都有足够的功能,所以C的垃圾收集其几乎可以在所有平台移植。一般而言,只要有垃圾收集的实现,移植性就不是问题。在Boehm-Weiser的移植还不多的时候,我曾经自己移植过两次,其时我对操作系统的底层接口还不甚熟悉。

Won’t this leave bugs in my program?
这不会给我程序引入bug吧?

这看你怎么想了。垃圾收集器可以解决程序员的很多问题,从而可以把精力放在其他地方,使得工作完成的更轻松。某种意义上讲,这跟浮点算法和虚拟内存的初衷是一致的。它们被发明出来都是为了解决些折腾程序员的乏味问题(如比例运算、换出页到硬盘)。没有FP和VM支持也可以写代码来实现相应的功能,不过只要有这功能可用,人们就不会自己写。一样的道理。

如果程序中用了垃圾收集风格的代码再扔掉垃圾收集器,内存泄漏的bug是肯定的。同样,如果一个程序用了FP(或VM)相关的代码,再卸掉浮点单元和MMU,bug也是一定的。

实际上,许多使用malloc和free的程序里都有内存泄漏,使用个垃圾收集器反而可以减少程序的bug。这可比手工跟踪内存再手工修复利索多了,要是跟踪发现内存泄漏出在三方库,还根本没办法修复。

Can’t a devious C programmer break the collector?
程序员可不可以把这收集器玩崩溃?

当然可以。不过大多数人应该更喜欢研究些正事,而不是整天想着把自己的工具玩坏掉。收集器需要正在内存空间中的指针,所以想玩坏就有办法。例如双向链表中的翻转指针技巧就不能用——这指针长得不像指针。如果程序把指针写到文件中,呆会再读出来,没准就崩了,因为这些指针指向的内存很可能已经被回收了。没大有程序会这么玩,所以对大多数程序而言,这不是问题。C中一般的(合法的)指针运算都是没问题的。

某技术团队在考虑使用GC时想到一个问题,那就是使用pointer mangling技巧可能会搞出“不透明”的指针。所谓pointer mangling,就是三方库中的指针与一固定的随机数异或下,这一来三方库中的数据只有按照特定的接口才可以访问。这个不兼容保守式GC,也不兼容Ansi C标准的严格编译…甚至会迷惑内存泄漏的跟踪器(跟保守式GC用的技术一样)。不过即便如此,它们依然是合法的程序。

Insert more questions here — send them to

What does a garbage collector do about destructors?
垃圾收集器如何对待析构函数?

析构函数就是对象被释放时候执行的代码,它的用途之一就是来手工回收内存,比如递归地回收对其它对象的引用。垃圾收集器里本没有析构函数的必要:如果一个对象是垃圾,它引用的所有对象就都是垃圾,自然会被收集到。

利用析构函数还可以做点其他事情。有两个应用比较典型:

与外部环境相关的对象的状态。比如文件相关的对象:当一个文件对象被回收时,垃圾收集器应该能保证能够刷新缓冲区、关闭文件,并将文件相关的资源返还给操作系统。

再就是程序需要保留一组在别处引用的对象。程序可能需要知道对象的功能,而不阻止它被收集。

解决这问题有很多方法:

有些系统是“built in”的垃圾收集,它就可以对外部引用的资源有所了解,处理起来也就心里有数
有不少垃圾收集系统有个“弱引用(weak pointer)”的概念,就是不被垃圾收集器认作引用的指针。如果一对象是个弱引用,那么就可以被GC收集。弱引用可以用来实现对象的list之类的数据结构。
再就是,java里可能会这样保护外部资源R:

class ClientR {
   CRWeak wr;
   // delegate all methods to wr;
   ClientR() {
     wr = new CRWeak(this);
   }
}
 
class CRWeak extends WeakReference {
  static ReferenceQueue rq = new ReferenceQueue();
  static {
         Thread th = new CRCleaner(rq);
         th.setDaemon(true);
         th.start();
  }
 
  CRWeak(Object x) {
     super(x, rq);
  }
  ExternalResource r;
  // delegated methods from ClientR
}
 
class CRCleaner extends Thread {
  ReferenceQueue rq;
  CRCleaner(ReferenceQueue rq) { this.rq = rq; }
  public void run() {
         while (true) {
           CRWeak x = (CRWeak) rq.remove();
           // Release x.r
         }
  }
}

当没有对象引用ClientR时候,这块内存就回收了,而对它的弱引用则移动到了各自的引用队列。清扫线程可以保证这些外部资源的回收。

许多GC系统都有“析构函数”的概念。垃圾收集器在回收一对象的时候,会执行该对象的一个函数来执行必要的清理操作。这会比较复杂,因为有些问题需要考虑:

  • 对象是在什么时候才会被收集?它并不像看起来这么简单,这对一些资源吃紧的应用尤为棘手。
  • 析构函数该在哪个线程、资源或者上下文下边执行?
  • 对象的交叉引用该怎么办?
  • 如果一个析构函数使得对象不再是垃圾,又该怎么办?

For more information

Contributors (so far)
贡献者(目前)

David Gadbois
Charles Fiterman
David Chase
Marc Shapiro
Kelvin Nilsen
Paul Haahr
Nick Barnes
Pekka P. Pirinen
GC FAQ table of contents
Techniques and algorithms
Language interfaces
Difficult topics
Silly stuff

简介AT&T风格汇编

Posted on 二月 17th, 2010 in 翻译 | 14 Comments »

作者:vivek
翻译:ssword
原文:http://sig9.com/articles/att-syntax

本文粗谈一下gas(1)的汇编语法,即AT&T风格汇编。初次接触很可能会觉得它别扭,不过若有其他汇编语言的基础,稍事了解即可快速上手。我假设你熟悉INTEL风格汇编——也就是INTEL手册中的那种风格。方便起见,我就用NASM(Netwide Assembler)来做比对。

gas属于GNU Binary Utilities(binutils),也是GCC的一个后端。对编写较长的汇编程序而言它并非首选,不过对于类Unix系统的内核级hacking,它就无可替代了。选择AT&T风格使得gas饱受争议,人们总说它只是GCC的后端,而对开发者不友好。INTEL风格汇编的教众也认为,它在可读性及编译上几乎是令人窒息。尽管如此,有一点必须了解:很多操作系统都选择了gas作为底层代码的汇编器。

基本形式

AT&T汇编程序的结构与其他汇编大同小异,伪指令、标签、指令—即最多带三个操作数的助记符。要说AT&T汇编的不同,最显眼的地方就是它操作数的顺序。

例如,一个简单的数据移动指令在INTEL风格下边是这个样子:

mnemonic	destination, source

在AT&T风格下边则是这样:

mnemonic	source, destination

一部分人(包括我)觉得这种格式更贴切。接下来说说AT&T风格指令中的操作数。

寄存器

每个IA-32架构寄存器的名字必须以’%'作前缀,如%al,%bx,%ds,%cr0,等等。

mov	%ax, %bx

如上的mov指令就是把一个16位寄存器ax中的值移动到另一个16位寄存器bx中。

字面量

每个字面量必须以’$'为前缀。 例如:

mov	$100,	%bx
mov	$A,	%al

第一个指令是把值100移动到寄存器bx中,第二个指令是把一个字节A移动到AL寄存器中。下面这个指令就是错误的:

mov	%bx,	$100

怎么说呢,这条指令是要把寄存器bx的值移动给一个字面量,显然不靠谱。

内存寻址

在AT&T风格中,内存引用起来是这个格式:

segment-override:signed-offset(base,index,scale)

按你寻址的需求,其中的部分可以省略

%es:100(%eax,%ebx,2)

注意下,基地址及偏移中的数不带前缀’$'。拿几个例子和对应的NASM风格做个比较应该好些:

GAS memory operand			NASM memory operand
------------------			-------------------
 
100					[100]
%es:100					[es:100]
(%eax)					[eax]
(%eax,%ebx)				[eax+ebx]
(%ecx,%ebx,2)				[ecx+ebx*2]
(,%ebx,2)				[ebx*2]
-10(%eax)				[eax-10]
%ds:-10(%ebp)				[ds:ebp-10]

实例:

mov	%ax,	100
mov	%eax,	-100(%eax)

第一个指令是把寄存器AX中的值移动到数据段寄存器(默认)偏移100的内存位置,第二个指令是把寄存器eax中的值移动到[eax-100]。

操作数大小

有时指明操作数的大小是必须的,尤其是移动字面量到内存。例如这个指令:

mov	$10,	100

这里只说了把值10移动到内存偏址100处,而没有说值的大小。在NASM中,这通过给操作数后面跟个byte/word/dword之类的关键词来指明。而在AT&T风格里,是通过指令中b/w/l之类的后缀指明。如:

movb	$10,	%es:(%eax)

把值为10的一个字节移动到内存地址[ex:eax],另如:

movl	$10,	%es:(%eax)

把值为10的一个长整数移动到同一位置。

再几个例子:

movl	$100, %ebx
pushl	%eax
popw	%ax

控制流程

jmp,call,ret等指令可以转移程序的执行位置。在同一代码段中跳转,是近距跳转(near)。若是跳转到不同的代码段,就是远程跳转(far)。可用的跳转地址可以来自相对偏移(label)、寄存器、内存以及段偏移指针。相对偏移通过label指明,如下:

label1:
	.
	.
  jmp	label1

使用寄存器或者内存的值做地址的操作数必须加个前缀’*'。若是远程跳转,必须加个’l'作前缀,如‘ljmp’,‘lcall’等等。例如:

GAS syntax			NASM syntax
==========			===========
 
jmp	*100			jmp  near [100]
call	*100			call near [100]
jmp	*%eax			jmp  near eax
jmp	*%ecx			call near ecx
jmp	*(%eax)			jmp  near [eax]
call	*(%ebx)			call near [ebx]
ljmp	*100			jmp  far  [100]
lcall	*100			call far  [100]
ljmp	*(%eax)			jmp  far  [eax]
lcall	*(%ebx)			call far  [ebx]
ret				retn
lret				retf
lret $0x100			retf 0x100

段偏移指针按下面的格式指明:

jmp	$segment, $offset

例如:

jmp	$0x10, $0x100000

记住这些很快就能上手了。要了解gas的更多细节,不妨参阅这个文档

Haskell趣学指南!

Posted on 十二月 12th, 2009 in 翻译 | 10 Comments »

译言前几天被杯具了,还好当初嗅到苗头全都抓了来 -v-

新地址见http://fleurer-lee.com/lyah

用自己写的小工具生成的html,parser的代码不到200行,代码写的非常之perl,不过显然还算够用。
代码高亮用的一个jquery插件Chili,很容易扩展,随便改两个正则就自制了个haskell高亮

顺便校对了下翻译。

  • ChinaUnix上的朋友对“柯里函数”等译法的意见比较大,不过在校对中没有做修改。关于人名构成的术语,例如“Fourier transform”还是“傅立叶变换”,译者认为后者更好看。
  • “Function Application”直译作“函数应用”,在这里译为“函数调用”。相应的“partial application”直译作“部分应用”,在这里译为“不全调用”。
  • “predicate”在这里译为“限制条件”,因为译者认为“断言”这个词有点吓人。
  • 保留了List,Tuple,List Comprehension等名词,不过将Triple之类译为“三元组”,“function with two parameters”译为“二元函数”。
  • 把原先译文中“在处理数字时是非常有用的”类的句式改为“在处理数字时非常有用”,“的”真的是很别扭的。
  • 有一节的标题“Texas Range”译为“德州区间”,因为译者老家在德州…囧

修改的比较仓促,bug依然还有很多。呵呵,欢迎提意见! ^_^

update: 可以svn checkout它的源码:https://lyah-cn.googlecode.com/svn/trunk/

内核如何管理内存

Posted on 十二月 6th, 2009 in 翻译 | 12 Comments »

作者:Gustavo Duarte
翻译:ssword
原文:http://duartes.org/gustavo/blog/post/how-the-kernel-manages-your-memory


我们已经对类型的虚拟地址空间布局有了一定了解,接下来我们进入内核,了解其内存管理机制。再拿gonzo的图示出来:

mm_struct

Linux内核使用进程描述符task_struct的实例来表示进程。task_struct中的mm字段指向内存描述符mm_struct,它储存了内存中各个段的起始位置(如上图)、进程中使用物理页的数量(rss是Resident Set Size的缩写)、虚拟地址区域的大小以及其他一点细节。在内存描述符中,我们还可以发现其两大心腹:虚拟内存区域和页表。Gonzo的内存区域表示起来如下:

memorydescriptorandmemoryareas

每块虚拟内存区域(VMA)都是一段连续的虚拟地址,其间没有重叠。vm_area_struct的实例就是对一段内存区域的描述,其中包含了内存区域的起始地址、描述行为和访问控制的标志以及表示文件映射的vm_field字段,没有文件映射的VMA就是匿名的(anoymous)。除去内存映射段,如上的每个段(如堆、栈)都单独与一个VMA相关联。x86的机器大都如此组织内存,不过并非如此不可–VMA本身不关心自己位于哪个段。

对程序员而言,VMA在其内存描述符,既有mmap字段里顺序的一段链表,又有mm_rb字段的一棵红黑树。这棵红黑树使得内核得以快速按照给出的虚拟地址来找出相应的内存区域。像读取文件/proc/pid_of_process/maps的内容,内核就只是简单遍历这个VMA的链表再将其输出。Windows下的EPROCESS差不多就是task_struct和mm_struct的混合体,而其相对于VMA的等价物就是虚拟地址描述符(Virtual Address Descriptor),简称VAD,储存在一颗AVL树中。你说Windows和Linux最有趣的东西是啥?就是那点小差异。

4GB的虚拟地址空间被划分为页。32位的x86处理器允许将页划分为三种大小:4kb、2mb或4mb。Linux和Windows的用户虚拟地址空间的页大小都是4kb。地址中0-4085字节是第一页,4096-8191是第二页,以此类推。VMA的大小必为页大小的整数倍。如下就是按4kb分页的3gb用户空间:

pagedvirtualspace

处理器依据页表将虚拟地址转换为物理地址,不同进程的页表各不相同。因此在进程切换时,用户空间的页表也随之切换。Linux将指向进程页表的指针储存于内存描述符的pgd字段中。每个虚页都与一个页表入口(PTE)相关联,在一般的x86分页机制下,它就是一个4字节长的记录:

x86pagetableentry4kb

Linux有专门的函数来读写PTE的属性标志。P位表示次页表是否位于物理内存。若清零,读取这块内存就会触发一个页异常。牢记,就算该位清零,内核依然可以修改其他属性域。R/W表示读/写(read/write);若清零,该页只读。U/S表示用户/超级用户(user/supervisor),若清零,该页只允许内核访问。有了这些标志,才可以实现我们前面所见的只读内存和内核空间保护。

D位与A位分别表示dirty和accessed。若一个页曾被写过,它就被标记为dirty;若曾有过读或写,它就会被标记为accessed。这两个标志都挺难搞:进程只管设置它们,清零却只允许内核来做。PTE保存与此页相关联的起始地址,以4kb对齐。这个貌似简单的属性域有一点不足,那就是限制了物理内存最大只能是4GB。物理地址扩展相关的PTE属性域改天再讲。

虚拟页是内存保护的基本单元,其中的所有字节共享同一U/S和R/W标志。不同的虚拟页可能映射自同一物理页,但其保护标志并不一定相同。注意下,PTE中并没有包含执行权的标志。这带来的后果就是,早期的x86系列CPU可能将堆栈段上的代码执行,因而易于遭到堆栈缓冲区溢出的攻击(如今使用return-to-libc或其他技巧,依然可以对堆栈中不可执行的代码搞溢出)。更深一层,PTE中执行权限标志的缺失使得VMA中的保护标志不一定能够应用到硬件的保护机制。内核已尽其全力,然而体系结构的限制尤在。

虚拟内存也不是什么都存。它只是将一个程序的地址空间映射到实在的物理内存,即物理地址空间。虽然在某种意义上有些内存操作也需要经过总线,但我们在此大可忽略之,从而将物理地址看作是从0开头,以字节为单位递增的一段地址。内核将物理地址空间划分为n个页框。处理器对页框的存在并不关心,不过对内核而言页框至关重要,因为页框就是管理物理内存的基本单元。32位的Linux和Windows都是用4kb的页框,如下是个2GB内存机器的例子:

Linux的每个页框都带有一个描述符以及n个属性标志。这些描述符管理了电脑的所有物理内存,使得每个页框的状态都可以明确。物理内存通过伙伴内存分配机制进行管理,对伙伴系统而言,一个页框若可以分配,那它就是free的。分配来的页框可以是匿名,以储存程序数据;也可以是页缓存,以包含来自文件或设备的数据。也有其他类型的页框,在这里先略过就是。Windows下有个类似的页框号(Page Frame Number,PFN)数据库来跟踪物理内存。

现在把虚拟内存区域、页表、页框放到一起,看看其整体是如何工作。如下是个用户堆的例子:

heapmapped

蓝色方形表示了VMA中包含的页,箭头表示页经页表与页框形成的映射关系。某些虚拟页上并没有箭头,因为它们PTE中Present标志都是清零的。这些页可能是从未使用,也可能是已被换出。不过无论怎样,访问这些页都会导致页异常,即便这些页在VMA中也是如此。VMA和页表不能一一对应,可能难以理解,不过确实是经常发生的事情。

VMA就像是程序与内核间的通信员。你先下个什么请求(内存分配,文件映射等等),内核说“行”,它就创建或更新一个合适的VMA。但它并不会马上将其一步到位,而是等到有了页异常再来。内核是个懒惰且狡诈的混蛋,这就是虚拟内存的基本作风。熟悉与否,这一思想随处可见。VMA保存上面分配来的内容,而PTE决定其具体的行为。这两个数据结构共同管理着程序的内存,遇到页异常、释放内存、换页等操作的时候,都有它俩的份。看个这个内存分配的简单例子:

heapallocation

程序使用brk()系统调用来申请更多内存,内核就简单更新下堆的VMA,说“行了”。不过这时并没有页框真正分配出来,页也不在物理内存中。程序一旦要访问这些页,处理器就会触发页异常,并执行do_page_fault()。它使用find_vma()找出异常发生地址对应的VMA,若找到,检查VMA的权限标志以防恶意访问(读或写);如果没有合适的VMA,就不再管这个内存访问而交给处理器触发一个段异常。

找到相应VMA之后,内核必须检查PTE的内容以及VMA的类型,以处理这个异常。在我们的例子里,PTE显示出这个页不在物理内存中。这里我们PTE为空(全为零),Linux中就表示这段虚拟页从未被映射过。这是个匿名的VMA,因此只能用do_anoymous_page()处理。它会分配一个页框,修改PTE将那发生异常的虚拟页映射到新分配的页框上。

也有例外。例如页已经换出,那它PTE中Present标志就是0,里面则保存了页内容在硬盘上的地址,它使用do_swap_page()读取硬盘并将其装载置页框。

我们内核内存管理之旅大约已行至一半。在下篇post里,我们会讨论上文件及性能的因素,以了解内存管理的整体。


囧,译到四分之三的时候发现已经有人译过了:http://blog.csdn.net/drshenlei/archive/2009/07/15/4350928.aspx