linux下内存

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的内容。


__get_free_page()或_alloc_page()从分区页框分配器中获得页框,kmem_cache_alloc()或kmem_alloc()使用slab分配器为专用或通用对象分配块,而vmallc()或vmalloc_32()获得一块非连续的内存区域。假设所请求的内存区得到满足,这些函数都返回一个页描写叙述符地址或线性地址(即所分配动态内存的起始地址)。


posted @ 2017-07-26 10:11  cxchanpin  阅读(291)  评论(0编辑  收藏  举报