zoukankan      html  css  js  c++  java
  • Linux内核内存管理

    主要函数以及数据结构

    内核地址空间中,虚拟地址与物理地址之间的转换函数:

    #include <asm/page.h>
    
    __pa(x); //将x虚拟地址转换为物理地址
    
    __va(x); //将x物理地址转换为虚拟地址
    
    

    pgd、pud、pmd、pt表中的数据结构:

    #include <asm/pgtable_types.h>
    
    typedef struct { pgdval_t pgd; } pgd_t;
    
    typedef struct { pudval_t pud; } pud_t;
    
    typedef struct { pmdval_t pmd; } pmd_t;
    
    typedef struct { pteval_t pte; } pte_t;
    
    typedef unsigned long	pteval_t;
    
    typedef unsigned long	pmdval_t;
    
    typedef unsigned long	pudval_t;
    
    typedef unsigned long	pgdval_t;
    
    typedef unsigned long	pgprotval_t;
    
    typedef struct pgprot { pgprotval_t pgprot; } pgprot_t; //用于描述页面属性的
    
    

    page frame到page结构的转换

    #include <asm-generic/memory_model.h>
    
    #define pfn_to_page __pfn_to_page
    
    page_to_pfn
    
    

    由于现在内存模型为SPARSEMEM模型,所以现在pfn到page结构的转换并不是像以前一样通过mem_map page数组计算得到。

    稀疏内存的初始化是在paging_init函数中进行的

    判断pfn是否有效:

    #include <linux/mmzone.h>
    static inline int pfn_valid(unsigned long pfn)
    {
    	if (pfn_to_section_nr(pfn) >= NR_MEM_SECTIONS)
    		return 0;
    	return valid_section(__nr_to_section(pfn_to_section_nr(pfn)));
    }
    

    Linux 内核中,每一页物理内存都属于特定的区,如ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM等。内核中用struct zone 代表一个该内存区。每一个内存区都属于特定的结点,每个结点用pg_data_t或pglist_data结构表示。

    遍历pg_data_t结构,以及其中的zone结构的代码如下:

    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/kernel.h>
    #include <asm/mmzone_64.h>
    
    static int __init my_test_init(void)
    {
    	int i = 0;
    	int j;
    	struct zone *zone;
    	printk("my test init
    ");
    	
    	for(i = 0;i<MAX_NUMNODES;i++){
    		if( !node_data[i] || !node_data[i]->node_present_pages )continue;
    
    		printk("node id:%d
    ",i);
    		printk("node_start_pfn:%ld
    ",node_data[i]->node_start_pfn);
    		printk("node_present_pages:%ld
    ",node_data[i]->node_present_pages);
    		printk("node_spanned_pages:%ld
    ",node_data[i]->node_spanned_pages);
    
    		for(j = 0; j<MAX_NR_ZONES;j++){
    			zone = &node_data[i]->node_zones[j];
    			if(zone->present_pages == 0) continue;
    			printk("	zone id:%d
    ",j);
    			printk("	zone_start_pfn:%ld
    ",zone->zone_start_pfn);
    			printk("	zone_present_pages:%ld
    ",zone->present_pages);
    			printk("	zone_spanned_pages:%ld
    ",zone->spanned_pages);
    		}
    	}
    
    	for(i = 1048576;i<1048576+262144+20;i++){
    		if(!pfn_valid(i)){
    			printk("pfn not valid:%d
    ",i);
    		}
    	}
    
    	return 0;
    }
    
    static void __exit my_test_exit(void)
    {
    	printk("my test exit
    
    ");
    }
    
    MODULE_LICENSE("GPL");
    module_init(my_test_init);
    module_exit(my_test_exit);
    

    每个进程的虚拟地址空间用struct mm_struct 结构表示,虚拟空间中的一个个虚拟空间段用struct vm_area_struct表示。

    遍历某进程虚拟地址空间中的vma结构

    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/kernel.h>
    #include <linux/sched/signal.h>
    #include <asm/pgtable_types.h>
    
    static int __init my_test_init(void)
    {
    	struct task_struct *ptask;
    	struct vm_area_struct *vma;
    	
    	int mypid = 4831;
    
    	for_each_process(ptask){
    		if(ptask->pid == mypid) break;
    	}
    	if(ptask->pid != mypid) return -1;
    
    	vma = ptask->mm->mmap;
    	
    	for( vma ; vma ; vma=vma->vm_next ){
    		printk("0x%lx -------- 0x%lx
    ",vma->vm_start,vma->vm_end);
    	}
    
    	return 0;
    }
    
    static void __exit my_test_exit(void)
    {
    	printk("my test exit
    
    ");
    }
    
    MODULE_LICENSE("GPL");
    module_init(my_test_init);
    module_exit(my_test_exit);
    

    find_vma函数用于在一个虚拟地址空间中,找到一个对应地址的一个虚拟地址区间。

    中断:当发生中断的时候,CPU会将下一条指令压栈,做为返回时去执行的指令。

    异常:当发生异常的时候,CPU会将导致异常的(未执行完成)指令压栈,做为返回时执行的指令。

    因此缺页中断叫法是错误的,应该被称为缺页异常。

    内存中越界访问

    情景:用户mmap一段空间,建立了一段虚拟空间区间,然后unmap掉了,用户再去访问该区间会造成越界访问。

    do_page_fault的处理流程:

    • 通过读取cr2寄存器,得到映射失败的线性地址
    • 通过find_vma查找包含该线性地址的vma
    • 没有找到,则发生越界访问,向程序发送SIGSEGV信号,程序退出。

    用户堆栈扩展

    do_page_fault的处理流程:

    • 通过读取cr2寄存器,得到映射失败的线性地址
    • 判断该地址是否在sp指针下方的一段空间中
    • 调用expand_stack,扩展堆栈区对应的vma的大小
    • 调用handle_mm_fault->handle_pte_fault->do_anonymous_page
    • do_anonymous_page中建立映射

    物理页面的使用和周转

    当一个pte_t指向的数据在物理内存中时,则该pte指向物理内存地址。如果数据已经被换出,则该pte_t变成一个swp_entry_t,指向数据所在交换设备的页号。

    swap_info_struct 结构的数组 swap_info

    swap_avail_head链表中保存所有可用的swap_info_struct(空间还没有使用完的),swap_active_head 链表保存所有的swap_info_struct

    swp_entry_t分为4个部分:

    • 第一部分:offset(页在交换设备的位置)
    • 第二部分:type(指页面在那个交换设备中)
    • 第三部分:radix_tree部分
    • 第四部分:P位
    static inline swp_entry_t swp_entry(unsigned long type, pgoff_t offset);
    static inline unsigned swp_type(swp_entry_t entry);
    static inline pgoff_t swp_offset(swp_entry_t entry);
    static inline swp_entry_t pte_to_swp_entry(pte_t pte);
    

    释放交换页面的函数:

    mm/swapfile.c:swap_free

    获得一个交换页面的函数:

    mm/swapfile.c:get_swap_page

    将一个页面加入swap cache中

    mm/swap_state.c:add_to_swap_cache

    注意,只有用户空间中的页面是可以回收的,内核空间中的页面是不可以回收的。在缺页中断中,每次分配的页,都会加入到对应的LRU链表中

    遍历page LRU的操作:

    static int __init simple_init(void)
    {
    	int i = 0;
    	int j;
    	int k;
    	struct zone *zone;
    	struct lruvec *lruvec;
    	unsigned long flags = 0;
    	int lru_count;
    	const char *str;
    
    	printk("simple module init
    ");
    	
    	for(i = 0;i<MAX_NUMNODES;i++){
    		if( !node_data[i] || !node_data[i]->node_present_pages )continue;
    
    		printk("node id:%d
    ",i);
    		printk("node_start_pfn:%ld
    ",node_data[i]->node_start_pfn);
    		printk("node_present_pages:%ld
    ",node_data[i]->node_present_pages);
    		printk("node_spanned_pages:%ld
    ",node_data[i]->node_spanned_pages);
    
    		for(j = 0; j<MAX_NR_ZONES;j++){
    			zone = &node_data[i]->node_zones[j];
    			if(zone->present_pages == 0) continue;
    			
    			
    			printk("	zone id:%d
    ",j);
    			printk("	zone_start_pfn:%ld
    ",zone->zone_start_pfn);
    			printk("	zone_present_pages:%ld
    ",zone->present_pages);
    			printk("	zone_spanned_pages:%ld
    ",zone->spanned_pages);
    
    			spin_lock_irqsave(&zone->lru_lock,flags);
    			lruvec = &zone->lruvec;
    			for(k = LRU_INACTIVE_ANON;k<NR_LRU_LISTS;k++){
    				switch(k){
    				case LRU_INACTIVE_ANON:
    					str="LRU_INACTIVE_ANON";
    					break;
    				case LRU_ACTIVE_ANON:
    					str="LRU_ACTIVE_ANON";
    					break;
    				case LRU_INACTIVE_FILE:
    					str="LRU_INACTIVE_FILE";
    					break;
    				case LRU_ACTIVE_FILE:
    					str="LRU_ACTIVE_FILE";
    					break;
    				case LRU_UNEVICTABLE:
    					str="LRU_UNEVICTABLE";
    					break;
    				default:
    					printk("error???????
    ");
    				}
    
    				if(list_empty(&lruvec->lists[k])){
    					printk("		%s empty
    ",str);
    				}else{
    					struct page *page;
    					lru_count = 0;
    					list_for_each_entry(page,&lruvec->lists[k],lru){
    						lru_count++;
    					}
    					printk("		%s not empty,page count:%d
    ",str,lru_count);
    				}
    
    			}
    			spin_unlock_irqrestore(&zone->lru_lock,flags);
    		}
    	}
    
    	return 0;
    }
    

    输出:

    [ 1693.905195] simple module init
    [ 1693.909260] node id:0
    [ 1693.913126] node_start_pfn:1
    [ 1693.916800] node_present_pages:65406
    [ 1693.920792] node_spanned_pages:65503
    [ 1693.924600] 	zone id:0
    [ 1693.929347] 	zone_start_pfn:1
    [ 1693.933641] 	zone_present_pages:3998
    [ 1693.937466] 	zone_spanned_pages:4095
    [ 1693.941177] 		LRU_INACTIVE_ANON not empty,page count:36
    [ 1693.941177] 		LRU_ACTIVE_ANON not empty,page count:139
    [ 1693.941177] 		LRU_INACTIVE_FILE not empty,page count:184
    [ 1693.941177] 		LRU_ACTIVE_FILE not empty,page count:405
    [ 1693.941177] 		LRU_UNEVICTABLE empty
    [ 1693.960620] 	zone id:1
    [ 1693.964087] 	zone_start_pfn:4096
    [ 1693.968114] 	zone_present_pages:61408
    [ 1693.972089] 	zone_spanned_pages:61408
    [ 1693.975919] 		LRU_INACTIVE_ANON not empty,page count:400
    [ 1693.975919] 		LRU_ACTIVE_ANON not empty,page count:1908
    [ 1693.975919] 		LRU_INACTIVE_FILE not empty,page count:2479
    [ 1693.975919] 		LRU_ACTIVE_FILE not empty,page count:5168
    [ 1693.975919] 		LRU_UNEVICTABLE empty
    [ 1704.263177] simple module exit
    

    物理页面的分配

    函数:struct page * alloc_pages(gfp_t gfp_mask,unsigned order);

    2.4.0内核代码内存分配流程:

    1.首先获得当前CPU所在节点结构(pg_data_t)。然后根据分配标志(gfp_mask)得到分配策略(pg_data_t结构中的node_zonelists数组元素)

    2.当发现可分配页面短缺时,唤醒kswapd和bdflush两个线程

    3.如果分配策略中的zone有满足:空闲内存页的数量,在low水平之上,则分配

    4.如果现在还是没有分配到页面,则考虑分配策略中zone中的不活跃干净页面

    5.如果还是没有分配到页面,则考虑回写不活跃脏页面,让其变为不活跃干净页面(分配大块内存时)。

    6.如果还是没有分配到页面,则以min阈值进行页面分配

    7.还是没有分配到页面,则说明系统有问题。

    内存分配失败的原因有两种方面,一种是系统中可分配内存页面的总量实在已经太少了,二是分配的是大块内存,而系统中当前没有大块内存

    4.0.0内核代码内存分配流程:

    快速分配:

    1.还是先获得分配策略(node_zonelists)

    2.检查分配策略中的每个zone:检查空闲内存是否在指定水位之上的,如果在则尝试从其分配,没有的话,看能不能进行页面回收,如果可以的话,先进行页面回收在进行判断水位、分配。

    如果还是没分配到内存则进入到慢速分配:

    1.唤醒kswapd进程

    2.尝试分配

    3.如果失败,则判断是否可以在低水位下进行分配

    4.进行页面compaction,在进行分配(异步)

    5.进行页面compaction,在进行分配(同步)

    内存回收

    kswapd守护进程:

    1.判断可供分配的页面是否短缺(inactive_shortage函数和free_shortage函数)

    2.如果页面短缺,先尝试将不活跃脏页面写出,变成不活跃干净页面(page_launder函数),在不活跃脏页面链表中,有大概率该页面已经是干净的,可以直接将其移动到不活跃干净页面。(不活跃干净页面可以用来直接分配)

    3.如果页面还是短缺,将活跃队列中的页面老化(refill_inactive),调用swap_out将一个进程中满足条件的活跃页面转移到不活跃脏页面队列中(第一步总是转入到不活跃脏页面队列中)。

    对各种(不活跃、活跃)队列中页面的扫描次数的增加,会使页面的老化速度也增加。因此页面的寿命实际上是以扫描的次数为单位的。

    新版内核中:

    对内存管理区调用shrink_zone --》 shrink_lruvec --》 shrink_list --》 shrink_active_list和shrink_inactive_list --》shrink_page_list进行页面回收。页面属于某个内存管理区zone的lru(lruvec字段)链表中((匿名、映射)活跃、不活跃)(在缺页异常时加入页对应内存管理区的lru链表中)。

    内核缓冲区的管理

    在slab方法中,每个重要的数据结构都有一个自己的专用缓冲区队列,每个队列中的对象个数是动态变化的,不够时临时添加。

    每个对象的缓冲区队列并非由各个对象直接构成,而是由一连串的“大块”(slab)构成,每个大块中则包含对象。

    缓冲区队列中的大块(slab)根据对象的使用状态处于三种状态中:完全分配完毕,部分分配,处于空闲状态

    每个大块(slab)上有个对象链接数组,用来实现一个空闲对象链。

    每个大块(slab)的头部有一个小小的区域是不使用的,称为着色区。用于将同一对象缓冲区中,不同大块(slab)中的相对位置相同的对象错开,改善高速缓存的效率。

    树形结构:

    • 总根cache_cache是一个kmem_cache_t结构,用来维持第一层slab队列,这些slab上的对象都是kmem_cache_t数据结构。
    • 每个第一层slab上的每个对象,即kmem_cache_t数据结构都是队列头,用来维持一个第二层slab队列。
    • 第二层队列基本上都是某种对象,即数据结构专用的。
    • 每个第二层slab上都维持着一个空闲对象队列。

    其中,最高层次的slab队列是cache_cache,队列中的每个slab载有若干个kmem_cache_t数据结构。而这样的数据结构又是某个数据结构的缓冲区头部。

    void *kmem_cache_alloc(kmem_cache_t *cachep,int flags);
    void *kmem_cache_free(kmem_cache_t *cachep,void *objp);
    
    void *kmalloc(size_t size,int flags);
    void kfree(const void *objp);
    
    void *vmalloc(unsigned long size);
    void vfree(void *addr);
    

    当数据结构较大时,因而不属于“小对象”时,slab的结构略有不同。不同之处是不将slab的控制结构放在它所代表的slab上,而是将其游离出来,集中放在另外的slab上。

    kswap扫描各个slab队列,找出和收集空闲不用的slab,并释放所占用的页面。

    专用缓冲区队列的建立

    kmem_cache_create:

    1.首先从cache_cache中分配一个kmem_cache_t结构

    2.进行一些列的计算,以确定最佳的slab构成(包括一个slab由几个页面构成,划分成多少个对象)

    缓冲区的分配与释放

    kmem_cache_alloc:

    1.查看是否有空闲slab,如果没有则分配一个slab

    2.否则从已有的slab中,分配一个对象

    对于分配用于slab的每个页面的page数据结构,要通过宏操作SET_PAGE_CACHE和SET_PAGE_SLAB,设置其链接指针prev和next,使它们分别指向所属的slab和slab队列。同时,还要把page结构中的PG_slab标志位设置为1.

    kmem_cache_free:

    根据释放后,slab的状态(空闲、部分空闲)做相应处理

    外部设备存储空间的地址映射

    ioremap、iounmap函数

    对于内存页面的管理,通常我们都是先从虚拟空间分配一个虚拟区间,然后为虚拟区间分配相应的物理内存页面并建立映射。

    ioremap:

    先从内核虚拟地址空间中分配一个虚拟地址区间(内核的内存描述符为init_mm,内核的虚拟地址区间描述符为vm_struct(vm_area_struct是用户虚拟地址区间描述符),链接成为vmlist链表),然后在建立到外部设备存储空间的映射。

    mmap

    资料

    关于稀疏内存实验的总结

    https://blog.csdn.net/huyahuioo/article/details/53286445

    Linux内核资料集合

    http://140.120.7.21/

  • 相关阅读:
    mbed TLS 介绍
    PostGIS:Working with Raster Data
    TIN数据格式:DEM的三种表示方法之一
    ArcScene显示DEM
    Python与MapNik 等高线渲染&抽稀
    Android GPS定位
    osmdroid通过点击获取当前坐标
    osmdroid高级教程
    mongodb 用户 权限 设置 详解
    Mongodb设置用户权限(整理版)
  • 原文地址:https://www.cnblogs.com/r1ng0/p/12809697.html
Copyright © 2011-2022 走看看