MMU由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,进行地址转换(MMU是CPU的一部分)
机器指令仍然用逻辑地址指定一个操作数的地址或一条指令的地址
每一个逻辑地址都由一个段选择符(16位)和段内的相对偏移量(32位)组成。段寄存器的唯一目的是存放段选择符。
MMU包含两个部件:分段部件和分页部件,分段机制将逻辑地址转换为线性地址,分页机制把线性地址转换为物理地址。
在RAM芯片上的读或写必须串行地执行,因此一种内存仲裁器的硬件电路插在总线和每个RAM芯片之间。
在整个系统中全局描述符(GDT)只有一张,可以被存放在内存的任何位置,,但CPU必须知道GDT的入口,GDT不仅存放了段描述符,还有其它描述符,都是64bit长,它是全局可见的,对任何一个任务都是这样。
GDT的第一项总是设为0,这就确保空段选择符的逻辑地址会被认为是无效的,因此引起一个处理器异常。?????
分段可以给每个进程分配不同的线性地址空间,而分页可以把同一线性地址空间映射到不同的物理地址空间。
运行在用户态的所有进程都使用一对相同的段来对指令和数据寻址,这两个段就是所谓的用户代码段和用户数据段。
所有段都是从0x00000000 开始,可以得出一个重要的结论,就是在linux逻辑地址与线性地址是一致的,即逻辑地址的偏移量子段与相应的线性地址的值总是一致的。
当指向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符,因为CS寄存器就含有当前的段选择符。
每个处理器有一个任务状态段(tss),相应的线性地址空间都是内核数据段相应的线性地址空间的小子集。
GDT中只有少数项可能依赖CPU正在执行的进程(LDT和TLS段描述符)。
分页单元的一个关键任务是所请求的访问类型与线性地址的访问权限相比较,如果这次内存访问是无效的,就产生缺页异常。
线性地址被分成以固定长度为单位的组,称为页。页内部连续的线性地址被映射到连续的物理地址中。
分页单元把所有的RAM分成固定长度的页框。
把线性地址映射到物理地址的数据结构称为页表。页表存放在主存中,并在启动分页单元之前必须由内核对页表进行适当的初始化。
二级模式通过只为进程实际使用的那些虚拟内存区域请求页表来减少内存使用量。
扩展分页用于把大段连续的线性地址转换成相应的物理地址,在这些情况下,内核可以不用中间页表进行地址转换,从而节省内存并保留TLB项。
与段的3种存取权限不同(读写执行)不同的是,页的存取权限只有两种(读写)。
由于用户进程线性地址空间的需要,内核不能直接对1GB以上的RAM进行寻址。
只有内核能够修改进程的页表,所以在用户态下运行的进程不能使用物理地址。
使用的级别数量取决于CPU类型。
硬件高速缓存基于著名的局部性原理,该原理既使用于程序结构也适用于数据结构。
高速缓存单元插在分页单元和主内存之间。它包含一个硬件高速缓存内存和一个高速缓存控制器。高速缓存内存中存放内存中真正的行,高速缓存控制器存放一个标项数组。
系统的运行速度一般是被CPU从内存中取得指令和数据速率限制的。
当命中一个高速缓存时,高速缓存控制器进行不同的操作,具体取决于存取类型。
cache访问:1 写回操作(write back) 2 写穿操作(write through)。回写方式只更新高速缓存行,不改变RAM的内容,提供了更快的功效。只有当CPU执行一条要求刷新高速缓存表项的指令时,或者当一个FLUSH硬件信号产生时(通常在高速缓存不命中之后),高速控制器才把高速缓存行写回道RAM中。
多处理器系统的每一个处理器都有一个单独的硬件高速缓存,因此需要额外的硬件电路用于保持高速缓存内容的同步。
TLB(translation lookaside buffer)的高速缓存用于加快线性地址的转换。当线性地址被第一次使用时,通过慢速访问RAM中的页表计算出相应的物理地址。同时物理地址被存放在一个TLB表项中,以便以后对同一个线性地址的引用快速的得到转换。
在多处理器系统中,每个CPU都有自己的TLB。
Linux的进程处理很大程度上依赖于分页。
在初始化阶段,内核必须建立一个物理地址映射来指定哪些物理地址范围对内核可用而哪些不可用(或者因为他们映射硬件设备I/O的共享内存,或者因为相应的页框含有BIOS数据)。
宏PAGE_OFFSET的值是区分用户空间和内核空间的范围大小,也是进程在线性地址中偏移量。PAGE_SHIFT是指页大小。****
内核维持着一组自己使用的页表,驻留在所谓的主内核全局目录中,系统初始化后,该组页表还从未被任何进程货任何内核线程直接使用。
在内核刚刚被装入内存后,CPU仍然运行于实模式,所以分页功能没有被启用。
临时页的全局目录放在swapper_pg_dir变量中。
由内核页表提供的最终映射必须把从PAGE_OFFSET开始线性地址转化为从0开始的物理地址。
主内核全局目录仍然保存在swapper_pg_dir变量中,由paging_init()函数初始化。
内核线性地址第四个GB的初始部分映射系统的物理内存至少有128MB的线性地址总是留作他用,因为内核使用这些线性地址实现非连续内存分配和固定映射的线性地址。
间接饮用一个指针变量比间接引用一个立即常量地址要多一次内存访问。
RAM的某些部分永久的分配给内核,并用来存放内核代码及静态内核数据结构。RAM的其余部分称为动态内存,这不仅是进程所需要的宝贵资源,也是内核本身所要的宝贵资源。
内核必须记录每个页框当前的状态。
在一下情况下页框是不空闲的,包含用户态进程的数据,某个软件高速缓存的数据,动态分配的内核数据结构,设备驱动程序缓冲的数据,内核模块的代码等。
页框的状态信息保存在一个类型为page的页描述符中,所有的页描述符存放在mem_map数组中。
在NUMA模型中,给定CPU对不同内存单元的访问时间可能不一样,系统的物理内存被划分为几个节点。在一个单独的节点内,任意给定CPU访问页面所需的时间都是相同的。每个节点都有一个类型为gd_data_t的描述符。
每个页描述符都有到内存节点和到节点管理区(包含相应框)的链接。
当内核调用一个内存分配函数时,必须指明请求页框所在的管理区。
原子请求(GFP_ATOMIC)从不被阻塞,如果没有足够的空间,则仅仅是分配失败而已。
保留内存的数量(KB)存放在min_free_kbytes变量中。它的初始值在内核初始化时设置。
参数gfp_mask是一组标志,它指明了如何寻找空闲的页框。
与直接映射的物理内存末端,高端内存的始端所对应的线性地址存放在high_memory变量中。
返回所分配页框线性地址的页分配器不适用于高端内存,即不适用ZPONE_HIGHMEM内存管理区内的页框。
高端内存页框分配只能通过alloc_pages()函数和它的快捷函数alloc_page()。这些函数不返回第一个被分配页框的线性地址,因为如果该页框属于高端内存,那么这样的线性地址根本不存在。取而代之,这些函数返回第一个被分配页框的页描述符的线性地址。
所有页描述符一旦被分配在低端内存中,他们在内核初始化阶段就不会改变。
内核可以采用三种不同的机制将页框映射到高端内存,分别叫做永久内核映射,临时内核映射及非连续内存分配。
建立永久内核映射可能阻塞当前的进程,这发生在空闲页表项不存在时。因此,永久内核映射不能用于中断处理程序和可延迟函数。
建立临时内核映射绝不会要求阻塞当前进程,但是缺点是只有很少的临时内核映射可以同时建立起来。
永久内核映射使用内核页表中i 个专门的页表,其地址被存放在pkmap_page_table变量中,页表中的表项数由LAST_PKMAP宏产生,该表映射的线性地址从PKMAP_BASE开始。
为了记录该段内存页框与永久内核映射包含的线性地址之间的联系,内核使用了page_address_htable散列表。
page_address()函数返回页框对应的线性地址,如果页框在高端内存中并且没有被映射,则返回NULL。
kmap()函数建立永久内核映射,如果页框确实属于高端内存,则调用kmap_high();
在高端内存的任一页框都可以通过一个“窗口”映射到内核地址空间。留给临时内核映射的窗口数是很少的。
内核必须确保同一窗口永远不会被两个不同的控制路径同时使用。因此,km_type结构中的那个符号只能由一种内核成分使用,并以该成分命名。最后一个符号KM_TYPE_NR本身并不表示一个线性地址,但由每个CPU用来产生不同的可用窗口数。
为了建立临时内核映射,内核调用kmap_atomic()函数。
Linux采用著名的伙伴系统算法来解决外碎片问题,把所有的空闲页框分组为11块链表,每个块链表分别包含大小1,2,4,8,16,32,64,128,256,512,1024个连续的页框。对1024个页框的最大请求对应着4MB大小的连续RAM块。
LRU链表是统称:细分为活动链表,非活动链表;链表中存放的是进程用户态地址空间或者页高速缓存的所有页。前者是最近被访问过的页,后者是一段时间内未曾被访问过的页。
每个伙伴系统使用的主要数据结构:mem_map数组,free_area数组(该元素的free_list数组的第k个元素标识所有大小为2^k的空闲块,该链表包含每个空闲页框块的起始页框的页描述符,指向链表中相邻元素的指针存放在页描述符的lru字段中)。最后,一个2^k的空闲页块的第一个页描述符的private字段存放了块的order,也就是数字k。
__rmqueue();函数用来在管理区中找到一个空闲块。该函数需要两个参数,管理区描述符的地址和order。
__rmqueue()函数假设调用者已经禁止了本地中断并获得了保护伙伴系统数据结构的zone->lock自旋锁。
__free_pcppages_bulk()函数按照伙伴系统的策略释放页框。
为了提升系统性能,每个内存管理区定义了一个“每CPU”页框高速缓存。所有“每CPU”高速缓存包含一些预先分配的页框,他们被用于满足本地CPU发出的单一内存请求。
实现每CPU页框高速缓存的主要数据结构是存放在(zone)内存管理区描述符pageset字段中的一个per_cpu_pageset数组数据结构。
buffered_rmqueue()函数在指定的内存管理区中分配页框,它使用每CPU页框高速缓存来处理单一页框请求。
为了释放单个页框到每CPU页框高速缓存,内核使用free_hot_page()和free_cold_page()函数。
管理区分配器是内核页框分配器的前端,该构件必须分配必须分配一个包含足够多空闲页框的内存区。
伙伴系统算法采用页框作为基本内存区,这适合于对大块内存。
内核函数倾向于反复请求同一类型的内存区。
slab分配器吧对象分组放进高速缓存,每个高速缓存都是同类线性对象的一种“储备”。
包含高速缓存的主内存区被划分为多个slab,每个slab由一个多多个连续的页框组成,这些页框中既包含已分配的对象,也包含空闲的对象。
每个高速缓存都是由kmem_cache类型的数据结构来描述。
高速缓存被分为两种类型,普通和专用,普通高速缓存只由slab分配器用于自己的目的(kmem_cache),而专用高速缓存由内核的其余部分使用。
在系统初始化期间调用kmem_cache_init()和kmem_sizes_init()来建立普通高速缓存。
专用高速缓存是由kmem_cache_create()函数创建的。
为了避免浪费内存空间,内核必须在撤销高速缓存本身之前就撤销其所有的slab。kmem_cache_shrink()函数通过反复调用slab_destroy()撤销高速缓存中所有的slab.
所有普通和专用高速缓存的名字都可以在运行期间通过读取/proc/slabinfo文件得到。
当slab分配器创建新的slab时,它依赖页框分配器来获得一组连续的空闲页框,为了达到此目的,它调用kmem_getpages()函数。
在相反的操作中,通过调用kmem_freepages()函数可以释放分配给slab的页框。
一个新创建的高速缓存没有包含任何slab,因此也没有空闲的对象。
只由当条件都为真(1.已发出一个分配对象的请求 2.高速缓存不包含任何空闲对象)才给高速缓存分配slab。
slab分配器通过调用cache_grow()函数给高速缓存分配一个新的slab,而这个函数调用kmem_getpages()从分区页表分配器获得一组页框来存放一个单独的slab,然后又调用alloc_slabmgmt()获得一个新的slab描述符。
只有当页框空闲时伙伴系统的函数才会使用lru字段,而只要涉及伙伴系统,slab分配器函数所处理的页框就不空闲并将PG_slab标志置位。
每个对象都有类似kmem_bufctl_t的一个描述符。对象描述符存放在一个数组中,位于相应的slab描述符后。
对象描述符只不过是一个无符号整数,只由在对象空闲时才有意义。它包含的是下一个空闲对象在slab中的下标,因此实现了slab内部空闲对象的一个简单链表。
slab分配器所管理的对象可以在内存中进行对齐,也就是存放他们的内存单元的起始物理地址是一个给定常量的倍数,通常是2的倍数,该常量也叫对齐因子。
slab分配器所允许的最大对齐因子是4096,即页框大小。
同一硬件高速缓存行可以映射RAM中很多不同块。
相同大小的对象倾向于存放在高速缓存内相同的偏移量处。因此,不同的slab内具有相同偏移量的对象最终很可能映射在同一高速缓存行中。
高速缓存描述符的array字段是一组向array_cache数据结构的指针,系统中的每个CPU对应于一个元素,每个array_cache数据结构是空闲对象的本地高速缓存的一个描述符。
本地高速缓存描述符并不包含本地高速缓存本身的地址,本地高速缓存存放的是指向释放对象的指针,而不是对象本身。
当创建一个新的slab高速缓存时,kmem_cache_create()函数决定本地高速缓存的大小,分配本地高速缓存,并将它们的指针存放在高速缓存的array字段。
通过调用kmem_cache_alloc()函数就可以获得新对象。
如果对存储区的请求访问不频繁,就用一组普通高速缓存来处理,普通高速缓存中的对象具有几何分布的大小。调用kmalloc()函数就可以得到这种类型的对象。
保留的内存池只能用于满足中断处理程序或者内存临界区发出的原子内存分配请求,而内存池是动态内存的设备,只能被特定的内核成分(池的拥有者)使用。
一个内存池常常叠加在slab分配器之上,内存池能被用来分配任何一种类型的动态内存,从整个页框到使用kmalloc()分配的小内存区。
当内存元素是slab对象时,alloc和free对象一般由mempool_alloc_slab()和mempool_free_slab()函数实现。在这种情况下,mempool_t对象的pool_data字段存放了slab高速缓存描述符的地址。
mempool_create()函数创建一个新的内存池,它接受的参数为内存元素的个数min_nr,实现alloc和free方法的函数地址和赋给pol_data字段的任意值。
为了从内存池分配一个元素,内核调用mempool_alloc()函数,将mmepool_t对象的地址和内存分配标志传递给它。
把内存映射到一组连续的页框是最好的选择,这样会充分利用高速缓存并获得较低的平均访问时间。
在物理内存映射的末尾与第一个内存区域之间插入一个大小为VMALLOC_OFFSET的安全区,目的是为了捕获对内存的越界访问。
每个非连续内存区都对应着一个类型为vm_struct的描述符。
get_vm_area()函数在线性地址VMALLOC_START和VMALLOC_END之间查找一个空闲区域,该函数使用两个参数,被创建内存区的字节大小和指定空闲区类型。
map_vm_area()并不触及当前进程的页表。因此,当内核态的进程访问非连续内存区域时,缺页发生,因为该内存区所对应的进程页表的表项为空。然而,缺页处理程序要检查这个缺页线程地址是否在主内核页表中(init_mm.pgd页全局目和它的子页表)。一旦处理程序发现一个主内核页表包含有这个线性地址的非空项,就把它的值拷贝到相应的进程页表项中,并恢复进程的正常执行。
内核永远也不会收回扎根于在内核页全局目录中的页上级目录,页中间目录和页表。
DMA忽略分页单元而直接访问地址总线,因此,所请求的缓冲区就必须位于连续的页框中。
频繁的修改页表势必导致平均访问内存次数的增加,因为这会使CPU频繁地刷新转换后援缓冲器TLB的内容。