深入理解Linux内核-内存管理

页框管理

内核对整个物理内存进行分页,每页大小为4KB或者4MB(大小无所谓,不同os都可能不一样),一般认为Linux的页大小为4KB,内核必须记录好每个页框的信息,所以linux内核把所有的页框都用struct page来描述,page也叫做页描述符,所有的page形成数组,放在mem_map变量中,mem_map的大小占用整个RAM(内存)的1%左右!记住以下两个宏定义

  1. virt_to_page(addr):把线性地址addr转化为页描述符地址
  2. pfn_to_page(pfn):把页框号转化为页描述符地址

页描述符page的字段主要包括flags、_count、index等,具体看书。

非一致内存访问NUMA和一致内存访问UMA

自己查资料,我们现在用的电脑一般为UMA。

内存管理区

Linux把整个内存分为三个区,分别为:

  1. ZONE_DMA:包含了低于16MB的内存页框,一般用于DMA,也可以用于别的用途。
  2. ZONE_NORMAL:包含了高于16MB且低于896MB的内存页框
  3. ZONE_HIGHMEM:包含了高于896MB的内存页框

保存的页框池

为了避免在申请页框的时候内存不够而导致的阻塞,系统会预留一些内存,这些内存被叫做保存的页框池,存放在min_free_kbytes变量中。

分区页框分配器

整个分配器如下组成:
在这里插入图片描述

管理区分配器可以接收动态内存的分配和释放请求,拿到请求后,可以根据具体的内存管理区去执行内存的分配和释放。每个内存管理区使用了伙伴系统来处理。下面十几个具体的函数

  1. 请求页框
    1. alloc_pages(gfp_mask,order):请求2的order次方个连续的页框,并返回第一个页框的页框描述符地址,如果失败返回NULL;
    2. alloc_page(gfp_mask):等价于alloc_pages(gfp_mask,0)
    3. __get_free_pages(gfp_mask,order):请求2的order次方个连续的页框,并返回第一个页的线性地址,如果失败返回NULL;
    4. get_zeroed_page(gfp_mask):请求1个初始化为0的页框,并返回线性地址,如果失败返回NULL;
    5. __get_dma_pages(gfp_mask,order):在DMA区域请求2的order次方个连续的页框,并返回第一个页的线性地址,如果失败返回NULL;

注意:请求的时候可以指定具体的区域,比如DMA区域,一般设置在gfp_mask中!

  1. 释放页框
    1. __free_pages(page,order):page是一个页框描述符,将其以及其后的2的order次方的描述符中count字段减一,减到0的会释放掉该页。
    2. free_pages(addr,order):addr是一个线形地址page,将其描述符中count字段减一,减到0的会释放掉该页。
    3. __free_page(page):不解释。
    4. free_page(addr):不解释。

高端内存页框的内核映射

针对ZONE_DMA和ZONE_NORMAL的页框请求和释放都很简单,这就是简单的线形映射,内核空间的前896MB都是这样线形映射的,所以很简单,但是高端内存(ZONE_HIGHMEM)就不一样了,因为32位的内核空间只有3-4GB,总长就1GB,前面以及占用了896MB了,所以只剩下128MB可以去映射高端内存,所以这一节着重记录一下高端内存的映射。

注意:64位的系统就没这么麻烦了,64位系统的用户空间、内核空间都远远大于物理内存,直接使用就行了!

《深入理解LINUX内核》这本书里面举了个例子,我觉得挺好:

例如,内核调用了__get_dma_pages(GFP_HIGHMEM,0)在高端内存中分配一个页框,此时如果有一个高端页框也不能反回其线性地址,因为根本不存在,所以返回NULL。

此时就要用到alloc_pages和alloc_page这两个函数了,获取到高端页框的描述符地址,然后为了让内核能访问到,必须使用内核空间的后128MB去映射。内核可以使用三种机制来映射高端内存,分别是永久内核映射临时内核映射以及非连续内存分配

永久内核映射

永久内核映射允许内核建立高端页框到内核地址空间的长期映射,它的页表存在于128MB之中,用变量pkmap_page_table变量保存。页表的表项数由LAST_PKMAP宏产生,一般就是512或1024项,不多,一共也就2MB或4MB。且该段页表映射的线形地址从PKMAP_BASE开始。Linux为每一个页表项都设置了一个计数器,存放在pkmap_count数组中,计数器的含义:

  1. 计数器为0,则代表该页表项没有映射任何高端内存,是可用的。
  2. 计数器为1,则代表该页表项没有映射任何高端内存,但不可用,因为从他最后一次被使用后,相应的TLB表还没有被刷新。
  3. 计数器大于1,由n-1个内核成分正在使用它。

此外,内核还用了page_address_htable散列表来映射物理页框和线性地址的关系,key为页框page,高端页框、低端页框都适用!函数page_address(page)就是用来查page_address_htable找出page对应的线性地址,如果page是低端页框,直接物理地址-3GB**就得到线性地址了,如果page是高端页框,就用散列表,三列表中如果不存在就返回NULL。

**kmap()**函数用来建立永久内核映射,代码等价如下:

void *kmap(struct page *page){
   
    if(!PageHighMem(page))//如果是低端内存,就直接返回映射address。
        return page_address(page);//上面的
    return kmap_high(page);
}

其中kmap_high()函数等价于:

void *kmap_high(struct page *page){
   
    unsigned long vaddr;
    spin_lock(&kmap_lock);
    vaddr = (unsigned long) page_address(page);
    if(!vaddr)
        vaddr = map_new_virtual(page);
    pkmap_count[(vaddr-PKMAP_BASE)>>PAGE_SHIFT]++;
    spin_unlock(&kmap_lock);
    return (void *)vaddr;
}

kmap_high使用了自旋锁,保证多处理器上的并发问题,同时它没有关中断,因为中断处理函数里不能调用kmap()。map_new_virtual函数才是真的去做映射,其代码等价于:

for(;;){
   
    int count;
    DECLARE_WAITQUEUE(wait, current);//将当前进程包装成一个等待结构体,wait就是该结构体指针,是个宏定义!相见之前做的笔记
    for(count = LAST_PKMAP;count > 0; --count){
   
        last_pkmap_nr = (last_pkmap_nr + 1) & (LAST_PKMAP-1);//这里的last_pkmap_nr是全局变量,便于从上次调用map_new_virtual产生的last_pkmap_nr接着扫描。
        if(!last_pkmap_nr){
   
            flush_all_zero_pkmap();//扫描,回收count=1的页表项,置count=0并删除其page对应的散列项、并刷新TLB。
            count = LAST_PKMAP;
        }
        if(!pkmap_count[last_pkmap_nr]){
   
            unsigned long vaddr = PKMAP_BASE + (last_pkmap_nr<<PAGE_SHIFT);
            set_pte(
                &(pkmap_page_table[last_pkmap_nr]),
                mk_pte(page, __pgprot(0x63))
            );//设置页表!!!
            pkmap_count[last_pkmap_nr] = 1;
            set_page_address(page, (void *)vaddr);//在散列表中建立散列项
            return vaddr;
        }
    }
    current->state = TASK_UNITERRUPTIBLE;//无可用页表,就去睡把!
    add_wait_queue(&pkmap_map_wait,&wait);
    spin_unlock(&kmap_lock);
    schedule();
    remove_wait_queue(&pkmap_map_wait,&wait);
    spin_lock(&kmap_lock);
    if(page_address(page)) 
        return (unsigned long) page_address(page);
    //否则继续执行死循环。
}

**kunmap()**函数用来撤销映射:

void kunmap(struct page *page){
   
    spin_lock(&kmap_lock);
    if(--pkmap_count[((unsigned long)page_address(page)-PKMAP_BASE)>>PAGE_SHIFT]==1)
       if(waitqueue_active(&pkmap_map_wait))
           wake_up(&pkmap_map_wait);
    spin_unlock(&kmap_lock);
}

临时内核映射

临时内核映射更加简单,它不阻塞当前进程,可以用于中断处理程序和可延迟函数内部,但他必须保证当前没有其他的内核控制路径在使用同样的映射,所以它不能被阻塞。高端内存任何一页都可以通过一个窗口映射到内核空间,但是这些窗口非常少,每个CPU都有13个窗口,用枚举enum km_type表示如下,最后一个KM_TYPE_NR仅仅表示前面有多少个(13个),这些个窗口都是固定映射的(见第二章的“固定映射的线性地址”),他们的开始和结束都由enum fixed_addresses数据结构中的FIX_KMAP_BEGINFIX_KMAP_END指定,如下所示,其中KM_TYPE_NR就是13,NR_CPUS是cpu数目。

enum km_type {
   
	KM_BOUNCE_READ,
	KM_SKB_SUNRPC_DATA,
	KM_SKB_DATA_SOFTIRQ,
	KM_USER0,
	KM_USER1,
	KM_BIO_SRC_IRQ,
	KM_BIO_DST_IRQ,
	KM_PTE0,
	KM_PTE1,
	KM_IRQ0,
	KM_IRQ1,
	KM_SOFTIRQ0,
	KM_SOFTIRQ1,
	KM_TYPE_NR
};
enum fixed_addresses {
   
	FIX_HOLE,
	FIX_VDSO,
	...
	FIX_KMAP_BEGIN,	/* reserved pte's for temporary kernel mappings */
	FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
    ...
	__end_of_fixed_addresses
};

变量kmap_pte表示可以FIX_KMAP_BEGIN的页表项地址,使用fix_to_virt(FIX_KMAP_BEGIN)调用获取并赋值给kmap_pte进行初始化。为了建立临时内核映射,内核调用**kmap_atomic()**函数实现,代码等价于:

void *kmap_atomic(struct page *page, enum km_type type){
   
    enum fixed_addresses idx;
    unsigned long vaddr;
    
    current_thread_info()->preempt_count++;//禁止调度、禁止强占
    if(!PageHighMem(page))//如果是低端内存,就直接返回映射address。
        return page_address(page);//上面的
    idx = type + KM_TYPE_NR*smp_processor_id();//通过type和cpu号 获取具体的index
    vaddr = fix_to_virt(FIX_KMAP_BEGIN + idx);
    set_pte(kmap_pte+idx,mk_pte(page, __pgprot(0x63)));//设置页表!!!
    __flush_tlb_single(vaddr);//刷新tlb项
    return (void *)vaddr;
}

内核使用kunmap_atomic(struct page *page)来撤销临时内核映射,在该函数中会preempt_count–,这是一种禁止调度方式,所以这里直接改preempt_count,就是禁止本cpu调度,这样的话整个kmap_atomickunmap_atomic之间就不能被强占,保证了当前没有其他的内核控制路径在使用同样的映射,就可以避免同一个临时内核映射页表项被多个进程使用!!

伙伴系统

Linux为了解决碎片的问题,可以有两种办法:

  1. 利用分页,把碎片页框映射到连续的线性地址上即可。
  2. 使用伙伴系统。

Linux是使用伙伴系统来解决碎片问题。Linux把所有空闲页分为11个块链表,每个块链表分别包含大小为1、2、4、8、16、32、64、128、256、512、1024个连续页框。对于1024个页框就是4MB。

使用到的数据结构

Linux的伙伴系统有三种,分别针对三个内存区域,每个伙伴系统都使用了以下数据结构:

  1. mem_map数组,每个伙伴系统所使用的页框都是mem_map的子集,伙伴系统的第一个页框以及页框个数使用zone_mem_map和size字段指定。
  2. 一个数组,包含了11个(对应前面的1、2、4…一共11个)类型为free_area的元素,每个free_area里包含了一个free_list,用于链接所有的同样大小的空闲块。

free_area数组的第k个元素表示了所有大小为2的k次方的空闲快,free_area的free_list字段是一个链接空闲块的双向循环链表的头,该链表的元素实际上就是空闲块首个页框的描述符(即page的lru字段,lru在页框空闲时用于此);free_area的nr_free表示当前链表中有多少个元素(即空闲块)。此外空闲块的第一个page的private字段放了k,用于表示它后面块个数为2的k次方。

分配快

分配快使用__rmqueue()函数,需要传入两个参数:管理去描述符zone和order(即需要快的大小为2的order次方)。如果分配成功则返回第一个页框的页描述符,否则返回NULL。该函数默认调用者以及禁止了本地中断和获取了zone->lock自旋锁。该函数大概内容如下:

...
	struct free_area *area;
	unsigned int current_order;
	for(current_order=order;current_order<11;current_order++){
   //不断地向上找,找到就跳转到block_found,找不到返回NULL
        area = zone->free_area + current_order;
        if(!list_empty(&area->free_list))
            goto block_found;
    }
	return NULL;
...

block_found代码如下:

block_found:
	page = list_entry(area->free_list.next,struct page, lru);//这是一个宏定义,用到了container_of宏
	list_del(&page->lru);
	ClearPagePrivate(page);
	page->private = 0;
	area->nr_free--;
	zone->free_pages -= 1UL << order;

此时如果current_order大于order的话就说明分配到了一个更大的块,所以需要讲剩余块插入到别的链表中去:

	size = 1 << current_order;
    while(current_order>order){
   
        area--;
        current_order--;
        size >>= 1;
        buddy = page + size;
        list_add(&buddy->lru, &area->free_list);
        area->nr_free++;
        buddy->private = current_order;
        SetPagePrivate(buddy);
    }

释放块

释放块用函数__free_pages_bulk(),它需要三个参数:zone、page、order,具体含义不解释了。该函数默认调用者以及禁止了本地中断和获取了zone->lock自旋锁。该函数大概内容如下:

{
   
    struct page *base = zone->zone_mem_map;
    unsigned long buddy_idx, page_idx = page - base;
    struct page *buddy, *coalesced;
    int order_size = 1 << order;
    
    zone->free_pages += order_size;//增加zone的page计数器
    
    while(order<10){
   //不断地去找伙伴
        buddy_idx = page_idx ^ (1<<order);//找到伙伴的page编号
        buddy = base + buddy_idx
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值