linux内存中的page,pte,alloc_page的flag

内核中关于内存的这几个flag容易混淆:pte,page,alloc_page,他们的功能相互关联,下面简要分析一下他们的区别和其中的重要flag的场景

  • 页表项的flag
    使用页表项的有两个对象:CPU和MMU,其中主要是CPU设置页表,MMU使用页表.
    MMU的功能有两个:
    1.将虚拟地址转换为物理地址
    2.检查访问权限是否合法,它是arch完全相关的
  • page的flag
    page是物理地址空间管理的元数据,它是纯粹的软件概念,基本是通用的
  • alloc_page的flag mask
    根据物理内存的稀有程序,页帧受到区别对待,分成了ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGH;而根据得到的物理页用途不同,一种是在内核运行期间一直占用,另一个种给进程使用,在进程结束就会释放回buddy,这两个页的生命周期对内存碎片的影响程度不同,从这个角度根据可移动性也进行了划分;另一个是当前申请物理内存的紧急程度也不同,在内存紧张的情况下特别是当空闲页小于low水位时得到的对待也不同;
    分配物理页不仅需要size,还需要上面这些指示标志.

alloc_page

1.指示首先要从哪个ZONE中申请页,它并没有直接使用ZONE_DMA区域的值而是使用flag中的低3位存储zone信息,其中什么也不设置即全是0代表ZONE_NORMAL,它是比较特殊的;
第4位表示迁移类型的__GFP_MOVABLE,可以和前面3位zone信息的任何一个组合使用

prefer zone ZONE_MOVABLE ZONE_DMA32 ZONE_HIGHMEM ZONE_DMA
zone modifier bit3 bit2 bit1 bit0

数组GFP_ZONE_TABLE中存储其了不同zone信息和迁移类型的组合关系,其中一些是互斥的,例如0x3既可以用DMA又可以用HIGHMEM,互相矛盾,标记为BAD:

 *       bit       result                                                          
 *       =================                                                         
 *       0x0    => NORMAL                                                                                                                      
 *       0x1    => DMA or NORMAL                                                   
 *       0x2    => HIGHMEM or NORMAL                                               
 *       0x3    => BAD (DMA+HIGHMEM)   
 *       0x4    => DMA32 or DMA or NORMAL
 ...

让开发者直接使用这些位信息是非常不友好的,所以使用宏的形式对这些位信息进行了包装.

标记 用途
GFP_DMA 从ZONE_DMA中分配
__GFP_HIGHMEM 从ZONE_HIGHMEM中分配
GFP_DMA32 从ZONE_DMA32中分配
__GFP_MOVABLE 分配的页面是可迁移的,而不是从ZONE_MOVABLE中申请

内核还提供了一种zone分配fallback机制,当首选的ZONE空闲页满足不了需求的时候,可以从fallback的zone中申请,通常为MOVABLE=>HIGHMEM=>NORMAL=>DMA32=>DMA,假如从NORMAL申请失败则尝试从DMA32中申请,如果还失败则从DMA区域中申请.

和迁移类型协作的时候,首先尝试同一个ZONE中的其他迁移类型(fallback),如果失败则尝试同一个node的其他zone.

2.指示申请者的上下文,例如中断上下文中进行页申请时不能休眠

Flag Description
__GFP_WAIT 申请页的上下文不是紧急的,可以进行睡眠或者重新调度
__GFP_HIGH 在内核线程或者是高优先级的进程中申请的,它可以使用emergecy pool的内存
在申请page时会检查当前空闲页数量是否小于watermark,它允许水位降低到普通上下文水位的一半
__GFP_IO 申请页的上下文正在做底层的IO操作,如果内存紧张不允许发生底层IO来获取空闲页
__GFP_FS 文件系统中申请页,如果内存紧张不要通过收缩文件系统数据来获取空闲页
__GFP_NOWARN 申请页失败时会打印WARNING信息,抑制这种信息打印,调用者会处理这种场景
__GFP_REPEAT 内存紧张时多次尝试回收页,最终没有回收到任何页时返回失败
__GFP_NOFAIL 内存紧张时无限尝试回收页,直至回收到可用页
__GFP_NORETRY 内存紧张时不需要多次进行页回收操作

其中__GFP_IO__GFP_FS标记使用场景:在文件系统或者IO栈上操作中进行页申请操作,此时如果空闲页到达了min水位进行同步页面回收,在页面回收时进行脏页回写或者swap操作,这些操作同样需要进行申请页,这样形成一个死锁状态.
在文件系统上进行页申请需要标记__GFP_FS,在IO栈操作时标记__GFP_IO,而标记__GFP_IO时一般也标记了__GFP_FS,文件系统操作通常离不开IO操作.

提供了常用场景的flag组合给开发者使用,驱动开发者不需要关心内存管理的细节.

flag 组成的flag 使用场景
GFP_NOWAIT (GFP_ATOMIC & ~__GFP_HIGH) 调用者不能休眠或者调度,但是不会使用emergency pool的内存,它可能分配失败,中断上下文使用
GFP_ATOMIC (__GFP_HIGH) 调用者不能休眠或者调度,允许使用emergency pool的内存,优先级最高的api,中断上下文中使用
GFP_NOIO (__GFP_WAIT) 调用者可以休眠,调度走,触发页面回收时不进行IO操作
GFP_NOFS (__GFP_WAIT | __GFP_IO) 调用者可以休眠,但是内存紧张的时候不能进行文件系统内容回收
GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS) 调用者可以休眠,但是内存紧张的时候不能进行任何的回写操作
普通的进程上下文使用,内核中使用最广泛的上下文场景
GFP_TEMPORARY (__GFP_WAIT| __GFP_IO | __GFP_FS | __GFP_RECLAIMABLE) 调用者可以休眠,但是内存紧张的时候不能进行任何的回写操作,优先从RECLAIM类型中分配
GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL) 用户进程申请的,但是内核或者设备也可以直接访问;主要是设备将buffer映射到用户空间,但是驱动还能操作DMA进行操纵buffer
GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | __GFP_HIGHMEM) 和GFP_USER类似,但是设备可以访问高端内存

分配页面时,buddy会检查当前页分配后是否过低,有个水位watermark在启动时根据zone的大小计算high,low,min的数量.当空闲页小于low时,会唤醒kswapd进行回收操作并且返回页给申请者,kswapd发现空闲页数量大于high时才终止页回收,这样可以减少频繁的触发页回收操作;
如果继续频繁申请页,到达min时,此时就会开始同步直接页回收,一直回收到足够的页之后才会返回分配的页.

对于GFP的flag中__GFP_HIGH会让系统对于min水位容忍度更强,平常min=28个页,低于这个阈值时就会触发同步页面回收,使用`__GFP_HIGH`时低于min=24个页才会触发同步页面回收.
watermark

reflink:https://lwn.net/Articles/594725/
https://lwn.net/Kernel/Index/#gfp_t

pte flag

pte是给MMU使用的,各个厂家的MMU都不一样,它是arch相关的.
在x86上cr3加载一级页表项PGD,而如何通过虚拟地址找到物理地址呢?通常我们看到内核中通过pgd_val, pte_val,pgd_offset等操纵页表项,这些内核的代码都是给CPU执行的,是属于软件的,而MMU就是将这些软件行为进行固化成电路,原理是相同的,不再赘述.
pte的flag定义在include/asm-x86_64/pgtable.h

#define _PAGE_BIT_PRESENT   0                                                   
#define _PAGE_BIT_RW        1                                                   
#define _PAGE_BIT_USER      2                                                                                              
#define _PAGE_BIT_ACCESSED  5        //当页被访问时设置,MMU自动设置该位,页回收时根据该标志位在inactive_list,active_list中移动位置                                              
#define _PAGE_BIT_DIRTY     6            //当pte present时有物理页映射,它描述dirty,writeback过程使用。当pte no present时,复用该标志位来表示后备存储是否是file。                                          
#define _PAGE_BIT_PSE       7   /* 4 MB (or 2MB) page */  系统设置page为4k,当设置PSE设置的是4M/2M的大页                         
#define _PAGE_BIT_GLOBAL    8   /* Global TLB entry PPro+ */                       
#define _PAGE_BIT_NX           63       /* No execute: only valid after cpuid check */

通常页表就是给MMU用的,页表项记录虚实地址对应关系和页的权限,但是Linux通过MMU异常实现了一些复杂的功能,例如COW,按需分配物理页,swap等.

PRESENT RW USER address VMA权限 场景
0 READ USER 合法的VMA内 MAY_READ|MAY_EXEC 1.如果pte为空,映射一个ZERO PAGE
2.pte中_PAGE_BIT_DIRTY置位,则按需读文件
3.如果pte中地址不为空,它表示swap信息,完成swap in操作
1 * USER 合法的VMA内 NONE 非法错误,见下面补充描述
0 * USER 不在合法的VMA内 VM_GROUPUP 扩充栈
0 WRITE USER 合法的VMA内 MAY_READ 按需分配物理页
1 WRITE USER 合法的VMA内 MAY_WRITE COW按需分配页
0 * KERNEL addr >VMALLOC_START && addr < VMALLOC_END vmap_struct只有分配类型的标志:IO,memory vmalloc区域页表同步

用户空间编程的时候为了防止错误访问,可能会mmap一段没有权限的区域来充当gap,多线程的线程私有空间都需要一些gap区域来防止越界访问,这部分属于设计上的防护措施,包括内核中的gap区域都利用了MMU对于权限访问的检查。

page flag

page的flag定义在include/linux/page-flags.h,它是软件通用的。每个物理页都需要有一个page对象来管理,内存越大,page占用内存越多。例如8G内存,页帧大小4k,每个page占64bytes,总共就需要128M=8G/4k*64的内存,在实际使用中需要严格管理page对象,所以它里面有很多的union的使用,在page处于不同管理下中代表不同功能。

#define PG_locked        0  	//当把页内容写入磁盘或者从磁盘载入到页时,置位,在此期间不能对该页数据进行读/写;在IO完成后清除该标志位           
#define PG_error         1         //当把页内容写入磁盘或者从磁盘载入到页时,如果发生IO错误,置位来向调用者传递错误信息;调用者进行异常处理后,清除标志                                        
#define PG_referenced        2     //和PG_active一起完成lru算法,见下面                                 
#define PG_uptodate      3        //标记当前内容是否有效,当读IO成功完成后,置位;写操作发生错误后,清除标志                                              
                                                                                
#define PG_dirty         4     //当内存和磁盘中内容不一致的时候,需要写入磁盘永久保存。当发生写操作后(write/truncate等)标记,writeback定时回写脏页,回写完成后清除标志                                                 
#define PG_lru           5     //页在活动或非活动页链表中,参与lru算法 ,见下面                         
#define PG_active        6      //置位表示在active_list中,否则在Inactive_list中;和PG_referenced一起完成lru算法                                                
#define PG_slab          7  //page被slab使用            
                                                                                
#define PG_owner_priv_1      8  /* Owner use. If pagecache, fs may use*/        
#define PG_arch_1        9       //在x86 上没有使用                                         
#define PG_reserved     10     //设置标志使页面不被交换或者标记没有被使用。                                                 
#define PG_private      11  /* If pagecache, has fs-private data */             
                                                                                
#define PG_writeback        12 //writeback正在将页写到磁盘上            
#define PG_nosave       13  /* Used for system suspend/resume */                
#define PG_compound     14  //复合页                     
#define PG_swapcache        15 //页属于swap的page cache
                                                                                
#define PG_mappedtodisk     16  /* Has blocks allocated on-disk */              
#define PG_reclaim      17  //页面回收内存对页已经做了写入磁盘的标记                     
#define PG_nosave_free      18  /* Used for system suspend/resume */            
#define PG_buddy        19  //page时空闲的,在buddy管理中

1.内核中连续的page组合在一起,buddy中管理页是以2^order 的形式管理的,从挂载在free_area的不同list来区分当前连续页的数量;当申请页时,也是以2^order的形式申请的,但是从buddy移除掉之后就不知道究竟有几个连续的页,通过PG_compound来标记是复合页,通过lru.next来实际管理之间的关系。
2.随着linux的长时间运行,空闲页面会越来越少,为了防止linux内核进入请求页面的僵局中,Linux内核采用页面回收算法从用户进程和内核page cache中回收内存,另外slab也是可以回收的,不过只是那些注册了收缩函数的的slab,例如dcache和icache,根据需要把要回收页框的内容交换到磁盘上的交换区。页面回收算法使用LRU的思想,但是给了它两次机会。它使用PG_referenced来标示页最近是否被引用过,使用PG_active来标示页当前的活跃程度。
page lru
有两个接口mark_page_accessed和page_referenced,使用mark_page_accessed处理刚刚引用过的页在LRU中的位置,根据当前页在inactive_list/active_list上和PG_referenced进行状态迁移;

inactive,unreferenced        ->      inactive,referenced
inactive,referenced          ->      active,unreferenced
active,unreferenced          ->      active,referenced

shrink_list中使用page_referenced进行清除当前PG_referenced标志,如果当前页在inactive_list并且当前PG_referenced没有设置,那这个页妥妥的要被回收了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章