这节课最后要讨论的内容,也是后面的一个实验,就是memory mapped files。这里的核心思想是,将完整或者部分文件加载到内存中,这样就可以通过内存地址相关的load或者store指令来操纵文件。为了支持这个功能,一个现代的操作系统会提供一个叫做mmap的系统调用。这个系统调用会接收一个虚拟内存地址(VA),长度(len),protection,一些标志位,一个打开文件的文件描述符,和偏移量(offset)。
这里的语义就是,从文件描述符对应的文件的偏移量的位置开始,映射长度为len的内容到虚拟内存地址VA,同时我们需要加上一些保护,比如只读或者读写。
假设文件内容是读写并且内核实现mmap的方式是eager方式(不过大部分系统都不会这么做),内核会从文件的offset位置开始,将数据拷贝到内存,设置好PTE指向物理内存的位置。之后应用程序就可以使用load或者store指令来修改内存中对应的文件内容。当完成操作之后,会有一个对应的unmap系统调用,参数是虚拟地址(VA),长度(len)。来表明应用程序已经完成了对文件的操作,在unmap时间点,我们需要将dirty block写回到文件中。我们可以很容易的找到哪些block是dirty的,因为它们在PTE中的dirty bit为1。
当然,在任何聪明的内存管理机制中,所有的这些都是以lazy的方式实现。你不会立即将文件内容拷贝到内存中,而是先记录一下这个PTE属于这个文件描述符。相应的信息通常在VMA结构体中保存,VMA全称是Virtual Memory Area。例如对于这里的文件f,会有一个VMA,在VMA中我们会记录文件描述符,偏移量等等,这些信息用来表示对应的内存虚拟地址的实际内容在哪,这样当我们得到一个位于VMA地址范围的page fault时,内核可以从磁盘中读数据,并加载到内存中。所以这里回答之前一个问题,dirty bit是很重要的,因为在unmap中,你需要向文件回写dirty block。
学生提问:有没有可能多个进程将同一个文件映射到内存,然后会有同步的问题?
Frans教授:好问题。这个问题其实等价于,多个进程同时通过read/write系统调用读写一个文件会怎么样?
这里的行为是不可预知的。write系统调用会以某种顺序出现,如果两个进程向一个文件的block写数据,要么第一个进程的write能生效,要么第二个进程的write能生效,只能是两者之一生效。在这里其实也是一样的,所以我们并不需要考虑冲突的问题。
一个更加成熟的Unix操作系统支持锁定文件,你可以先锁定文件,这样就能保证数据同步。但是默认情况下,并没有同步保证。
学生提问:mmap的参数中,len和flag是什么意思?
Frans教授:len是文件中你想映射到内存中的字节数。prot是read/write。flags会在mmap lab中出现,我认为它表示了这个区域是私有的还是共享的。如果是共享的,那么这个区域可以在多个进程之间共享。
学生提问:如果其他进程直接修改了文件的内容,那么是不是意味着修改的内容不会体现在这里的内存中?
Frans教授:是的。但是如果文件是共享的,那么你应该同步这些变更。我记不太清楚在mmap中,文件共享时会发生什么。
你们会在file system lab之后做这里相关的mmap lab,这将会是我们最后一个虚拟内存实验。
最后来总结一下最近几节课的内容,我们首先详细看了一下page table是如何工作的,之后我们详细看了一下trap是如何工作的。而page fault结合了这两部分的内容,可以用来实现非常强大且优雅的虚拟内存功能。我们这节课介绍的内容,只是操作系统里面基于page fault功能的子集。一个典型的操作系统实现了今天讨论的所有内容,如果你查看Linux,它包含了所有的内容,以及许多其他有趣的功能。今天的内容希望能给让你们理解,一旦你可以在page fault handler中动态的更新page table,虚拟内存将会变得有多强大。