MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权。(它具有虚拟地址和物理地址转换,内存访问权限保护等功能,这使得Linux操作系统能单独为系统的每个用户进程分配独立的内存空间并保证用户空间不能访问内核空间地址,为操作系统的虚拟内存管理模块提供硬件基础)。
一、MMU的历史
许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程序。但随着图形
界面的兴起还用用户需求的不断增大,应用程序的规模也随之膨胀起来,终于一个难题出现在程序员的面前,那就是应用程序太大以至于内存容纳不下该程序,通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片
段。覆盖块0首先运行,结束时他将调用另一个覆盖块。虽然覆盖块的交换是由 OS完成的,但是必须先由程序员把程序先进行分割,这是一个费时费力的工作,而且相当枯燥。人们必须找到更好的办法从根本上解决这个问题
。不久人们找到了一个办法,这就是虚拟存储器(virtual memory).虚拟存储器的基本思想是程序,数据,堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存
在磁盘上。比如对一个16MB的程序和一个内存只有4MB的机器,操作系统通过选择,可以决定各个时刻将哪4M的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个16M的程序运行在一个只具有4M
内存机器上了。而这个16M的程序在运行前不必由程序员进行分割。
二、MMU的相关概念——地址范围、虚拟地址映射为物理地址 以及 分页机制
任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。这个范围的大小由CPU的位数决定,例如一个32位的CPU,它的地址范围是0~0xFFFFFFFF (4G),而对于一个64位的CPU,它的地址范围为
0~0xFFFFFFFFFFFFFFFF (64T).这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物
理地址,大多数时候我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集。这里举一个最简单的例子直观地说明这两者,对于一台内存为256M的32bit x86主机来说,它的虚拟地址空间范围是0~0xFFFFFFFF(4G),
而物理地址空间范围是 0x000000000~0x0FFFFFFF(256M)。
在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚
拟地址映射为物理地址。
大多数使用虚拟存储器的系统都使用一种称为分页(paging)机制。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页桢(frame).页和页桢的大小必须相同。在这个例子中我们有
一台可以生成32位地址的机器,它的虚拟地址范围从 0~0xFFFFFFFF(4G),而这台机器只有256M的物理地址,因此他可以运行4G的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放4G程序的外部存
储器(例如磁盘或是FLASH),以保证程序片段在需要时可以被调用。在这个例子中,页的大小为4K,页桢大小与页相同—— 这点是必须保证的,因为内存和外围存储器之间的传输总是以页为单位的。对应4G的虚拟地址和256M
的物理存储器,他们分别包含了1M个页和64K个页桢。
三、MMU的功能
1、将虚拟地址映射为物理地址
现代的多用户多进程操作系统,需要 MMU, 才能达到每个用户进程都拥有自己独立的地址空间的目标。使用MMU, 操作系统划分出一段地址区域, 在这块地址区域中, 每个进程看到的内容都不一定一样。例如MICROSOFT
WINDOWS操作系统将地址范围4M-2G划分为用户地址空间,进程A在地址0X400000(4M)映射了可执行文件,进程B同样在地址 0X400000(4M)映射了可执行文件,如果A进程读地址0X400000, 读到的是A的可执行文件映射到RAM
的内容,而进程B读取地址0X400000时,则读到的是B的可执行文件映射到RAM的内容。
这就是MMU在当中进行地址转换所起的作用。
2、提供硬件机制的内存访问授权
多年以来,微处理器一直带有片上存储器管理单元(MMU),MMU能使单个软件线程工作于硬件保护地址空间。但是在许多商用实时操作系统中,即使系统中含有这些硬件也没采用MMU。
当应用程序的所有线程共享同一存储器空间时,任何一个线程将有意或无意地破坏其它线程的代码、数据或堆栈。异常线程甚至可能破坏内核代码或内部数据结构。例如线程中的指针错误就能轻易使整个系统崩溃,或至
少导致系统工作异常。
就安全性和可靠性而言,基于进程的实时操作系统(RTOS)的性能更为优越。为生成具有单独地址空间的进程,RTOS只需要生成一些基于RAM的数据结构并使MMU加强对这些数据结构的保护。基本思路是在每个关联转换中“
接入”一组新的逻辑地址。MMU利用当前映射,将在指令调用或数据读写过程中使用的逻辑地址映射为存储器物理地址。MMU还标记对非法逻辑地址进行的访问,这些非法逻辑地址并没有映射到任何物理地址。
这些进程虽然增加了利用查询表访问存储器所固有的系统开销,但其实现的效益很高。在进程边界处,疏忽或错误操作将不会出现,用户接口线程中的缺陷并不会导致其它更关键线程的代码或数据遭到破坏。目前在可靠
性和安全性要求很高的复杂嵌入式系统中,仍然存在采无存储器保护的操作系统的情况,这实在有些不可思议。
采用MMU还有利于选择性地将页面映射或解映射到逻辑地址空间。物理存储器页面映射至逻辑空间,以保持当前进程的代码,其余页面则用于数据映射。类似地,物理存储器页面通过映射可保持进程的线程堆栈。RTOS可以
在每个线程堆栈解映射之后,很容易地保留逻辑地址所对应的页面内容。这样,如果任何线程分配的堆栈发生溢出,将产生硬件存储器保护故障,内核将挂起该线程,而不使其破坏位于该地址空间中的其它重要存储器区,如
另一线程堆栈。这不仅在线程之间,还在同一地址空间之间增加了存储器保护。
存储器保护(包括这类堆栈溢出检测)在应用程序开发中通常非常有效。采用了存储器保护,程序错误将产生异常并能被立即检测,它由源代码进行跟踪。如果没有存储器保护,程序错误将导致一些细微的难以跟踪的故障
。实际上,由于在扁平存储器模型中,RAM通常位于物理地址的零页面,因此甚至NULL指针引用的解除都无法检测到。
1. 物理页申请(allocation)
核心算法是:Binary Buddy Allocator.
1. 空闲块管理
每个zone有一个free_area数组, 第0个元素表示的块大小是1个页, 第1个元素表示的块大小是2个页...最大的块大小是512个页.
每个区有一个bitmap, 每一位用来记载一对buddy的使用状态, 如果bit是0, 表示那对page(两页)都是full or free. 如果bit是1, 表示只有其中一页是在用.
2. 申请页
页申请的核心调用是: __alloc_pages(). 页申请顺序如下:
首先找最大能符合的块, 如果一个空闲块不能满足, 更高一级的块将分割成2个buddy, 一个被占用, 一个放入低一级的freelist.
当块被释放时, 检查每对buddy, 如果两者都空闲, 把他们合并到更高一级的块数组里去, 同时放入更高一级的freelist. 如果一个buddy还在被用, 那此块将加入到当前级的freelist.
如果一个zone已经没有足够空闲页,而且又需要分配,那申请将退到下一级,一般顺序是:ZONE_HIGHMEM --> ZONE_NORMAL --> ZONE_DMA. 如果空闲页击中pages_low门限,kswapd将开始释放页。
3. 释放页
核心调用是: __free_pages_ok(). 当一个buddy释放时,尽可能立即合并buddy。有一种不理想的情况是,在最坏的情况下,在很多合并发生之后,立即有发生切割,在相同的块上面。
4. Get Free Page(GFP) Flags
GFP标志决定了VM分配器和kswapd对页的申请和释放的工作方式。
_GFP_DMA, __GFP_HIGHMEM, __GFP_WAIT, __GFP_HIGH, __GFP_IO, __GFP_HIGHIO, __GFP_FS.
5. 进程Flags
进程可以设置标志,来决定分配器的行为方式。
GFP_ATOMIC, GFP_NOIO, GFP_NOHIGHIO, GFP_NOFS, GFP_KERNEL, GFP_USER, GFP_HIGHUSER, GFP_NFS, GFP_KSWAPD.
6. 避免碎片
外部碎片: 因为所有可用的内存只存在于小块,分配器无力满足分配请求。
内部碎片: 是指所浪费的空间,由于大的块用于分配满足小的内存请求。
内部碎片的容易出现,而且破坏力严重,可以看到的是,单靠buddy系统来解决,没有明显的效果,可以解决这个问题的方法是综合slab分配器的使用。
2. 非连续内存分配
vmalloc()是内核可以用来分配连续虚存,但非连续物理内存的方法。
对于内核,可以用来分配的虚存介于VMALLOC_START和VMALLOC_END之间。VMALLOC_START的起始位置取决于可用的物理l内存总量。因为是给不连续物理内存分配,所以每块物理单元都是用:vm_struct来描述。此结构很简单
,包括单元块使用标志,起始地址,长度,及至向 next的指针。
一个有意思的地方是,每一个离散的被分配的块之间至少会有一页的间隔,以此来避免overrun。
1. 分配/释放非连续区域
首先找到可以满足分配的足够大的一块区,再分配PGD条目,PMD条目,PTE条目,最后才分配页。释放的顺序正好相反,先释放PGD条目,PMD条目,PTE条目,最后是页
3. MMU系统初始化
1. Paging启动
在bootloader过程中,内核会被调入到物理地址0x100000(1M),开始执行的是head.S, 在这段指令的执行中,Paging会被激活,之后内核跳到PAGE_OFFSET+some_address,开始执行start_kernel (start_kernel()将不
会执行,直到Paging被激活??)。start_kernel()会初始化所有的内核数据,并且开始init内核线程的运行。对MMU的初始化,是在setup_arch()里完成的,这个函数是平台相关的,主要完成底层的初始化,首先,此函数会计
算总共的Low- memory和High-memory的页数(在setup_memory()里完成),接下来,setup_arch()调用init_bootmem ()来初始化boot-time内存分配器(也称之为bootmem分配器),此分配器的工作是为内核的初始化和永久数
据提供内存页,在boot之后,这些页是不参与MMU的管理的。 (如果这些页是固定地址的,地址是如何分配的??)
2. 初始化内核页表
在setup_arch()里,内核页表的初始化是调用paging_init()完成的。首先,内核尽可能地将所有的物理内存影射到 PAGE_OFFSET(3G)与4G之间。内核页表是存在于swapper_pg_dir,是表示一种影射关系,也称之为内核页
目录。目录里的页主要用于初始化Paging,在系统启动的时候。
接一下来,内核调用fixrange_init()来分配一些页表给预编译阶段就所需的虚存,其方法是set_fixmap(). 这些fixmap表会在运行时被影射到物理页上。
初始化fixmaps后,如果CONFIG_HIGHMEM被设置,内核会分配一些页表给kmap()分配器。kmap()分配器可以让内核映射任意物理页到内核虚拟地址空间,作为临时使用。
fixmap和kmap页表占据了内核虚拟空间里最顶端的部分,有最大128M在内核虚空间的顶部将为此保留。所以,所有物理页其被映射后的虚存地址如果高于4G-128M(即如果访问大于896M的物理内存),将会当作HIGHMEM
zone的操作来处理(当然,前提是CONFIG_HIGMEM开关被打开)。内核访问HIGHMEM的页,需要通过kmap()来存取。如果 CONFIG_HIGMEM开关未打开,这部分物理页将访问不到,只是浪费而已,对2.4的最近版本,此开关是缺省
打开的。
最后,对kmap/zone分配器进行初始化,然后是调用free_area_init()建立mem_map和Buddy free_lists, 此时,所有的free_list项都被初始化成空,并且标记为保留(即暂时VM还不能存取??)
3. setup_arch()是在start_kernel()里被调用的,paging_init()是在setup_arch()被调用,当 paging_init()结束后,其他的一些内核子系统会继续用bootmem分配器申请一些内核内存段...,之后,Buddy free_lists的
保留状态标记将被清除,并释放所有可用的内存页在他们相应的Zone里,最后,free_all_bootmem()被调用,所有被bootmem分配器申请的虚拟页将释放到他们相应的Zone里去。
4. Page Swap and Cache
A user process page is kept in either the page cache or the swap cache, the kernel tries to keep as much as pages remain in the memory, so that page faults can be serviced quickly, but, physical
memory is always limited resources, thus, swap cache can be a better-than-none replacement.
Logically, the cache is a layer between the kernel memory management code and the disk I/O code. Practically, the cache here means, only freeing the memory has to be necessary, we swap the pages to
disk, otherwize, keeping the pages as long as they can.
The kernel maintains several page lists which comprise the whole page cache:
1. active_list: pages on the list have page->age > 0, may be clean or dirty, and may be mapped by process page-table entries.
2. inactive_dirty_list: pages on the list have page->age == 0, may be clean or dirty, and are not mapped by any process PTE.
3. inactive_clean_list: each zone has its own inactive_clean_list, which contains clean pages whose age == 0, not mapped by any process PTE.
When a page fault is occurred, the kernel first looks for the fault page in the page cache, if found, it can be moved directly to the active_list.
Overall, the kernel performs the page replacement more like "not recently used", rather than strict LRU. Furthermore, each page either for an executable image or mmap()ed file is associated with a
per-inode cache, allowing the disk file to be used as backing storage for the page. For those anonymous pages(i.e. the pages like malloc()ed memory) are assigned an entry in the system swap file,
and those pages are maintained in the swap cache.
The life cycle of a user page:
1. Page loading, there are three ways that can lead to a page loaded from the disk.
a. The process attempts to access the page, it is then read in by the page-fault handler and added to the page cache and to the process page table.
b. The page is read in during a swap read-ahead operation. The reason is simple as a cluster of blocks on disk is easy to read sequentially.
c. The page is read in during a mmap cluster read-ahead operation, in which case a sequential of adjacent pages following the fault page in a mmaped file is read.
2. when the page is written by the process, it becomes dirty. At this point, the page is still on the active_list.
3. suppose the page is not used for a while, thus the page->age count is gradually reduced by the periodic invocations(actually, the prime is refill_inactive()) of the kernel swap daemon kswapd().
The frequence of kswapd() invocations will increase as the memory pressure increases.
4. if the physical memory is tight, kswapd() will call swap_out() to try to evict pages from the process's virtual address space.Since the page hasn't been referenced and has age 0, the PTE will be
dropped, and the only remaining reference to the page is the one resulting from its presence in the page cache (assuming, of course, that no other process has mapped the file in the meantime).
swap_out() does not actually swap the page out; rather, it simply removes the process's reference to the page, and depends upon the page cache and swap machinery to ensure the page gets written to
disk if necessary. (If a PTE has been referenced when swap_out() examines it, the mapped page is aged up - made younger - rather than being unmapped.)
5. Time passes... a little or a lot, depending on memory demand.
6. refill_inactive_scan() comes along, trying to find pages that can be moved to the inactive_dirty list. Since the page is not mapped by any process and has age 0, it is moved from the active_list
to the inactive_dirty list.
7. Process A attempts to access the page, but it's not present in the process VM since the PTE has been cleared by swap_out(). The fault handler calls __find_page_nolock() to try to locate the page
in the page cache, and lo and behold, it's there, so the PTE can be immediately restored, and the page is moved to the active_list, where it remains as long as it is actively used by the process.
8. More time passes... swap_out() clears the process 's PTE for the page, refill_inactive_scan() deactivates the page, moving it to the inactive_dirty list.
9. More time passes... memory gets low.
10. page_launder() is invoked to clean some dirty pages. It finds P on the inactive_dirty_list, notices that it's actually dirty, and attempts to write it out to the disk. When the page has been
written, it can then be moved to the inactive_clean_list. The following sequence of events occurs when page_launder() actually decides to write out a page:
* Lock the page.
* We determine the page needs to be written, so we call the writepage method of the page's mapping. That call invokes some filesystem-specific code to perform an asynchronous write to disk with
the page locked. At that point, page_launder() is finished with the page: it remains on the inactive_dirty_list, and will be unlocked once the async write completes.
* Next time page_launder() is called it will find the page clean and move it to the inactive_clean_list, assuming no process has found it in the pagecache and started using it in the meantime.
11. page_launder() runs again, finds the page unused and clean, and moves it to the inactive_clean_list of the page's zone.
12. An attempt is made by someone to allocate a single free page from the page's zone. Since the request is for a single page, it can be satisfied by reclaiming an inactive_clean page; The page is
chosen for reclamation. reclaim_page() removes the page from the page cache (thereby ensuring that no other process will be able to gain a reference to it during page fault handling), and it is
given to the caller as a free page.
Or:
kreclaimd comes along trying to create free memory. It reclaims the page and then frees it.
Note that this is only one possible sequence of events: a page can live in the page cache for a long time, aging, being deactivated, being recovered by processes during page fault handling and
thereby reactivated, aging, being deactivated, being laundered, being recovered and reactivated...
Pages can be recovered from the inactive_clean and active lists as well as from the inactive_dirty list. Read-only pages, naturally, are never dirty, so page_launder() can move them from the
inactive_dirty_list to the inactive_clean_list "for free," so to speak.
Pages on the inactive_clean list are periodically examined by the kreclaimd kernel thread and freed. The purpose of this is to try to produce larger contiguous free memory blocks, which are needed
in some situations.
Finally, note that the page is in essence a logic page, though of course it is instantiated by some particular physical page.
vivi研发笔记:MMU分析
这几天一直在看MMU部分,现在有了基本的认识,还不深入,解决了初级问题,并且仿照vivi完成了一个测试实例,对深入理解和验证推论的结果很有帮助。在学习的过程中,体会到几种方法还是比较实用的:
· 从历史的角度去了解技术,梳理清楚发展主线,效率更高。
· 采用软硬件结合的方法分析,理解更为深入。
· 与同一爱好的朋友交流,可以发现认识的盲区,认识更为全面。
· 应该阅读英文的Datasheet,中文翻译往往有错误,有时候是致命的,以前在c8051F020的SPI的设计中就因为这个问题研究了一周,最后才发现中文翻译有问题,这也足以说明还是原版更具有可信度。但是呢,不要绝
对化,原版也可能有错误。在阅读学习的过程中,如果理解到设计的巧妙,那么很好,拿来使用。如果暂时不能理解,或者认为原版有错误,那么不妨持怀疑的态度进行学习,即使错了,也能学到很多东西,甚至从错误中学
到的东西更多。
· 充分利用网络资源。站在巨人的肩上可以更高更远!但是,你必须训练让自己具备找到巨人的能力!
下面开始MMU的探讨。因为还只是处于初级阶段,本文仅就MMU的基础方面和应用作一下探讨,其中还有没有明确的地方,待解决。
一、MMU是什么?
MMU,英文名称为Memory Manage Unit, 中文可以为“内存管理单元”,或者“存储器管理单元”。MMU是硬件设备,它与virtual memory是紧密联系在一起的。
看一下s3c2410 datasheet Appendix 1中关于ARM920T的介绍(因为s3c2410采用的是ARM920T的处理器)。对ARM9系列处理器有如下几种:
--ARM9TDMI(ARM9TDMI Core)
--ARM940T(ARM9TDMI core plus cache and protection unit)
--ARM920T(ARM9TDMI core plus cache and MMU)
可见ARM920T具备了MMU功能部件。而且还有cache。它采用了一种变形的Harvard架构,拥有16KB的Instruction cache和16KB的Data cache,MMU和cache有密切的联系,后面会谈到。
总之,由于s3c2410这款SoC采用了ARM920T的处理器(处理器内核为ARM9TDMI,关于这些命名的区分在前面已经讨论过了),所以拥有了 MMU和cache。有了这个硬件基础,软件上才可能使用这个功能。那么,现在的问题
是,s3c2410拥有MMU,那么MMU到底有什么用呢?还是从历史发展的角度看一下。
二、从历史发展的角度看MMU的作用
这一部分可结合蔡于清的讲解【网址:http://www.another-prj.com/viewthread.php?tid=28&extra=page%3D1】来看,下面的大部分内容转载此处,针对自己的理解做了一些扩充性说明。只是需要注意的是,在蔡于清此
部分的讲解中,有几处小的错误,完成此部分的讲解后可以进行更正。
MMU功能部件是与虚拟内存技术(virtual memory)紧密联系在一起的。
第一阶段:最初,计算机内存很小,而且非常昂贵,大多数都是以KB为单位的。相应的,当时程序规模很小,不复杂,所以内存还是能够满足需求的。在看《Linkers and Loaders》的时候,也是从这个阶段讲解,不过此
书的核心视角是从Linkers和Loaders的发展来看的。也就是,计算机刚刚出现时,还是比较简陋的,各种复杂的技术是伴随着人们需求的提高而出现的。把握住这一点,就可以从需求的角度入手探讨技术,可以分析它如何满
足了这样的需求。通过这种分析,理解上就比较简单一些了。
第二阶段:程序规模扩大,考虑到成本问题,出现了overlay技术,也就是内存覆盖策略。基本的原理就是把程序分割成许多称为“覆盖块”的片断。覆盖块 0首先加载运行,结束时调用另一个覆盖块。覆盖块的调度是由
OS来完成的,但是事先需要分割,这部分工作是程序员借助Linkers来完成的。但是毕竟枯燥,由此带来的开销也比较大。于是进入第三个阶段。
第三阶段:出现virtual memory。虚拟存储器的基本思想是程序,数据,堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上。比如对一个16MB
的程序和一个内存只有4MB的机器,OS通过选择,可以决定各个时刻将哪4M的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个16M的程序运行在一个只具有4M内存机器上了。而这个16M的程序在
运行前不必由程序员进行分割。
伴随着这种技术的出现,“virtual address,即VA”和“physical address, 即PA”也就出现了。一般来说,CPU看到的地址是VA,VA是有地址线来决定的。比如,s3c2410是32位的SoC,那么它的寻址空间为 2^32=4GB
,那么VA空间也就是4GB。但是在嵌入式系统中,物理存储器是不会有这么大的。现在这块s3c2410的实际内存SDRAM也就 64MB,远远小于4GB。也就是说,VA是4GB,PA是64MB,PA的地址空间是VA地址空间的子集。既然PA没有
VA那么大,而且CPU只能看到 VA,那么CPU如何找到PA呢?这也正是MMU的基本作用之一,就是提供VA到PA的转换机制,除了硬件的支持外,软件上实际就是维护一张表,表中的内容是VA到PA的转换法则。由于有了MMU,那么就
可以实现利用VA找到实际物理内存区域。
现在讨论为什么要实现VA到PA的映射。就ARM而言,系统上电后,CPU的PC指向0x00000000或者0xffff0000,这是由CPU的设计者决定的。在这个位置,一般安排非易失性存储器地址空间,比如rom,flash等。但是flash等
响应速度慢,这就称为提高系统性能的一个瓶颈。而 sdram则具有很高的响应速度,为了提高系统运行速度,可以把flash中的应用程序下载到sdram中执行,也就是一个简单的loader的功能实现。这样就出现一个问题,ARM响
应exception时,程序指针指向固定的VA,比如,假设发生了IRQ中断,那么PC执行0x00000018(如果是高端启动,则指向0xffff0018处。)但是此处仍然为非易失性存储器,也就是说,程序的一部分仍然在flash或者rom中执
行。这时可以利用 MMU,把sdram的地址映射到0x00000000起始的一片连续地址空间,而把原来flash映射到其他不相冲突的存储空间位置。例如,flash 的地址范围0x00000000-0x00ffffff,sdram的地址范围0x30000000-
0x31ffffff。那么可以把sdram映射到 0x00000000-0x1fffffff(此处地址空间未被占用)。映射完成后,如果处理器异常,假设依然为IRQ中断,pc指向 0x00000018,但是pc实际上是从物理地址0x30000018处读取指令。通
过mmu的映射,可以实现系统运行的加速。这个地方也可以说明 bootloader中常见的中断向量表的设置,为什么有些使用b,有些使用ldr了。【b的跳转空间只能是+-32M,而ldr可以大的多了。】
在实际的应用过程中,还可能会把两片不连续的物理地址空间分配给sdram,而在os中,习惯上把sdram的空间连续起来,方便实现动态内存管理。通过mmu可以实现不连续的物理地址空间映射为虚拟地址空间。
另外一个需求就是,实现不同的运行级别,那么一些关键的代码可以设定不被普通应用程序访问。这也是通过mmu控制访问权限来实现的。
综上三个阶段所述,可见MMU的作用主要就是两个:
· 实现VA到PA的映射(可以因此实现方便的动态内存管理)
· 实现不同的访问权限。
三、结合s3c2410来分析MMU的几处硬件特点
首先看看ARM920T的框图:
可以验证前面的几个概念:
·位于中心的ARM9TDMI Processor Core发出的地址有两种,IVA和DVA,都是VA。其中I代表Instruction, D代表Data。也就是说,CPU核心看到的都是32bits的VA。
·Dcache、Icache、Dmmu、Immu看到的都是对应的MVA(modified virtual address),这个是比较复杂的地方,下面专门拿出这个来讲解。
·MMU处理后的输出地址都是对应的PA,通过AMBA Bus Interface连接到ASB总线上面。
这样,从硬件上对地址的概念就比较清晰了。也可以很明显的看出MMU的功能:将VA转换成PA。但是现在存在的一个问题是,MVA是什么,为什么要用到MVA?
可以看CP15协处理器的register 13。这个寄存器是进程识别寄存器,主要的操作如下:
Reading from CP15 register 13 returns the value of the process identifier. Writing CP15 register 13 updates the process identifier to the value in bits [31:25]. Bits [24:0] should be zero.
寄存器的字格式为:
很清晰,ProcID为7bits,剩下的25bits should be zero,也就是可以实现2^25=32M的地址对齐。从这个道理上讲,每个进程拥有32M的MVA地址空间,而最多支持的进程数为2^7=128个。这样,128*32M=4GB,正是全部的
虚拟地址空间。但是,英文的datasheet上却并非如此,写的记录数字为64个进程,同样每个进程32M,怎么可能达到4GB?参看下图:
我觉得上图中的63应该改为127。因为这个63处不可能对应4GB,而应该对应2GB。判断此处属于datasheet的错误。
还有,这个procID是何时,有谁写入的?有谁来维护?根据推断,在bootloader阶段,只需要一个进程就可以了,所以,procID一直都是复位后默认的0,不需要改变。但是后面有了OS后,要想实现多进程,那么就需要对
此维护了。所以procID的维护者是系统软件OS。在创建一个新进程的时候,要把进程号写入procID。
另外,关于MVA部分的转换公式,实际上还是有疑问的。
Addresses issued by the ARM9TDMI core in the range 0 to 32MB are translated by CP15 register 13, the ProcID register. Address A becomes A + (ProcID x 32MB). It is this translated address that is
seen by both the Caches and MMU. Addresses above 32MB undergo no translation.
写成伪代码,可以参考《s3c2410完全开发》。
if VA < 32M then
MVA = VA | (ProcID << 25)
else
MVA = VA
thisway.diy说利用PID来生成MVA的目的是为了减少切换进程时的代价:如果两个进程占用的VA有重叠,不进行上述处理的话,当进行进程切换时必须进行VA到PA的重新映射,这就需要重新建立页表,使无效cache和TLB等
等,代价很大。但是如果进行上述处理的话,进程切换就省事多了:假设两个进程1、2运行时的VA都是0-32M,则它们的MVA分别是(0x02000000-0x03ffffff)、(0x04000000-0x05ffffff),前面看到的MMU、cache使用MVA
而不是使用 VA,这样就不必进行重建页表等工作了。
但是这里带来的一个问题是,如果进程运行时的VA小于32M,那么根据PID的不同,可以达到4GB空间的任意部分,也就是,虽然可以避免运行VA小于 32M时的不同进程的“撞车”,但是同时带来的是VA小于32M可能与VA大
于32M的进程产生了“撞车”。这样不是更为普遍吗?现在从原理上还不能理解。翻看《ARM Architecture Reference Manual》,发现对于ARM核,如果采用MVA,那么进程切换实际上对应着Fast context switch extension,
不知道原理是什么。对于研究bootloader来说,现在不设计到多进程,整个系统就是一个独立的单进程,PID就是默认的 0x0。这个问题可能要后推了。
四、提出几个问题
1、在vivi中为什么使用了MMU?是否可以不用?
这个问题已经解决。实际上,在nand flash启动的情况下,vivi中可以不使用MMU。因为一是中断向量表是放在sram里,响应速度比sdram还要快。另外,在bootloader 阶段,只有一个进程,不存在多进程的内存空间重叠
的问题。也因为一个进程,所以单纯的PA就满足需求,没有必要用VA。开始时,也不需要区分访问权限。大量的工作,比如进程切换、权限访问等等,都是在EOS中处理的。所以,这种情况下,可以不使用MMU。我把vivi中关
于MMU的部分去除,编译下载,可以正常引导内核启动,没有问题。
那么,vivi为什么要开启MMU呢?原因也是比较简单的,就是追求系统运行的高效。因为s3c2410的Icache不受MMU的影响,而Dcache 和write buffer则必须开启了MMU功能之后,才能使用。而使用Dcache和write buffer后
,对系统运行速度的提高是非常明显的,后面还将通过实验来验证这一点。也就是说,在nand flash启动时,vivi使用了MMU,主要是为了获得Dcache和write buffer的使用权,借此提高系统运行的性能。
2、使用了MMU,那么软硬件是如何分工协作的?
这个基本搞清楚了,但是还有一个遗留问题。针对于s3c2410,可以分为如下几个阶段:
· 第一阶段 软件准备
MMU在软件上的实现过程,实际上就是一个查表映射的过程。建立页表(translation table)是MMU功能的重要的一步。页表就是内存的一块区域,由一个个固定格式的entry组成。其中每个entry对应一个VA到PA的转换,
每一项的长度是一个word,还可以完成访问权限和缓冲特性的限定。在软件上,就是要把这个表填好。重映射就是修改相应的entry,改变了原来的映射规则,很简单。
这步工作是要软件提前准备的。需要注意的是,明确如何找到这个页表。对于表的查找,需要知道表的基地址和偏移地址,在cp15的register 2用于保存页表的基地址,这样就可以查找到相应的PA了。
· 第二阶段 硬件完成VA-MVA
硬件根据ARM9TDMI发出的VA和CP15的register 13来自动生成MVA。
· 第三个阶段
硬件自动实现cache查询,如果没有,则根据cp15的register 2和MVA找到translation table中的entry,实现相应的PA转换,读取内存,然后根据cache算法更新cache。也就是说,这个阶段也是硬件实现的。不过软件上
对 cache要进行相应的管理,这个地方的算法相对还是比较复杂的。
综上,对单进程而言,软件操作上就是维护translation table,并且处理好cache相关操作。
五、实验
实验内容比较简单,综合了前面的串口实验,灯循环点亮实验,中断实验,nand flash实验,另外,加入了MMU功能。利用MMU功能的开启,观察灯循环点亮实验,如果开启了Dcache和write buffer,灯闪的速度明显快的
多,几乎看不出间隔,而把其关闭,则还能够看出间隔。这还是在12MHz的前提下,还没有把PLL功能开启。如果把 PLL功能开启,还需要进行相应的调整。
源代码如下,完全仿照vivi的架构,另外,mmu部分基本是采用vivi的源代码,具体的分析留待vivi源代码分析时解决。其实,如果开启了 mmu,cache和write buffer是否能够合理有效的使用还是一个问题。如果使用不
当,带来的问题可能会比较奇怪,而且难以解决。在这个过程中,需要对照现有的较好的代码进行分析,总结规律,然后应用到自己的设计中去。