linux下内存
Posted Destihang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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标志置位。
DMA忽略分页单元而直接访问地址总线,因此,所请求的缓冲区就必须位于连续的页框中。
频繁的修改页表势必导致平均访问内存次数的增加,因为这会使CPU频繁地刷新转换后援缓冲器TLB的内容。
以上是关于linux下内存的主要内容,如果未能解决你的问题,请参考以下文章