页框管理

内核对整个物理内存进行分页,每页大小为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号 获取具体的indexvaddr = 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_atomic和kunmap_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,找不到返回NULLarea = 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;if(!page_is_buddy(buddy,order))//检查这个buddy是不是我们要找的buddybreak;list_del(&buddy->lru);zone->free_area[order].nr_free--;ClearPagePrivate(buddy);buddy->private = 0;page_idx &= buddy_idx;//和伙伴合并,拿到合并后的低indexorder++;}coalesced = base + page_idx;coalesced->private = order;SeyPagePrivate(coalesced);list_add(&coalesced->lru, &zone->free_area[order].free_list);zone->free_area[order].nr_free++;
}

每CPU页框高速缓存

为了提升系统的性能,在每个内存管理区定义了一个“每CPU”页框高速缓存,“每CPU”页框高速缓存包含了一些预先分配的页框,用于满足本地CPU发出的单一内存请求。“每CPU”页框高速缓包括了两个高速缓冲区,分别是热高速缓冲区(大概率是存在CPU的硬件高速缓冲区中)、冷高速缓冲区。

实现“每CPU”页框高速缓存的主要数据结构是存放在内存管理区描述符的pageset字段中的一个per_cpu_pageset数组数据结构,该数组为每个cpu提供一个元素,该元素又分为两个per_cpu_pages描述符,分别是热高速缓冲区、冷高速缓冲区。

per_cpu_pages的相关字段:

int count ===> 高速缓冲区的叶匡个数;

int low =====> 下界,表示需要补充页框了!

int high ====> 上界,表示高速缓冲区用尽;

int batch ===> 在高速缓冲区需要补充或删去页框时,批量操作的页框个数

struct list_head list ==> 用于链表。

当count个数低于low时,内核通过伙伴系统分配batch个页框补充给对应的高速缓冲区,当count个数高于high时,内核通过伙伴系统释放batch个。

通过每CPU页框高速缓存分配页框

**buffered_rmqueue()函数用于在指定的内存管理区中分配页框。他使用“每CPU”页框高速缓存来处理单一页框请求,注意要请求多页框时该函数使用之前的__rmqueue()**函数去申请页框。源码如下:

/** Really, prep_compound_page() should be called from __rmqueue_bulk().  But* we cheat by calling it from here, in the order > 0 path.  Saves a branch* or two.*/
static struct page *buffered_rmqueue(struct zonelist *zonelist,struct zone *zone, int order, gfp_t gfp_flags)
{unsigned long flags;struct page *page;int cold = !!(gfp_flags & __GFP_COLD);int cpu;again:cpu  = get_cpu();//获取cpu编号if (likely(order == 0)) {//申请单页框struct per_cpu_pages *pcp;pcp = &zone_pcp(zone, cpu)->pcp[cold];//拿到对应的热缓冲区或冷缓冲区(取决于gfp_flags)local_irq_save(flags);//关中断if (!pcp->count) {//这里判断是否需要补充页框给缓冲区pcp->count = rmqueue_bulk(zone, 0,pcp->batch, &pcp->list);//通过__rmqueue去补充if (unlikely(!pcp->count))goto failed;}page = list_entry(pcp->list.next, struct page, lru);list_del(&page->lru);pcp->count--;} else {//多页框申请spin_lock_irqsave(&zone->lock, flags);page = __rmqueue(zone, order);spin_unlock(&zone->lock);if (!page)goto failed;}__count_zone_vm_events(PGALLOC, zone, 1 << order);zone_statistics(zonelist, zone);local_irq_restore(flags);put_cpu();VM_BUG_ON(bad_range(zone, page));if (prep_new_page(page, order, gfp_flags))goto again;return page;failed:local_irq_restore(flags);put_cpu();return NULL;
}static int rmqueue_bulk(struct zone *zone, unsigned int order, unsigned long count, struct list_head *list)
{int i;spin_lock(&zone->lock);for (i = 0; i < count; ++i) {struct page *page = __rmqueue(zone, order);if (unlikely(page == NULL))break;list_add_tail(&page->lru, list);}spin_unlock(&zone->lock);return i;
}

释放每CPU页框高速缓存页框

一般使用free_hot_pagefree_cold_page去释放页框。这两个函数底层都调用了**free_hot_cold_page()**来处理:

/** Free a 0-order page  //这个函数只释放一个页框*/
static void fastcall free_hot_cold_page(struct page *page, int cold)
{struct zone *zone = page_zone(page);//获取zonestruct per_cpu_pages *pcp;unsigned long flags;if (PageAnon(page))page->mapping = NULL;if (free_pages_check(page))return;if (!PageHighMem(page))debug_check_no_locks_freed(page_address(page), PAGE_SIZE);arch_free_page(page, 0);kernel_map_pages(page, 1, 0);pcp = &zone_pcp(zone, get_cpu())->pcp[cold];//获取per_cpu_pageslocal_irq_save(flags);__count_vm_event(PGFREE);list_add(&page->lru, &pcp->list);//加进链表pcp->count++;//计数器++if (pcp->count >= pcp->high) {free_pages_bulk(zone, pcp->batch, &pcp->list, 0);//释放batch个块pcp->count -= pcp->batch;}local_irq_restore(flags);put_cpu();
}

注意:在当前2.6版本的内核,是没有页框释放到冷高速缓存中的,这是因为内核总是假设释放的叶匡的热的!

管理区分配器

管理区分配器是内核页框分配器的前端,它的核心函数为**__alloc_pages()**,之前的alloc_pages宏就是调用它来分配页框。该函数需要三个参数:gfp_mask标志、order、zonelist(每个内存管理区的zone的链表)

/** This is the 'heart' of the zoned buddy allocator.*/
struct page * fastcall
__alloc_pages(gfp_t gfp_mask, unsigned int order,struct zonelist *zonelist)
{const gfp_t wait = gfp_mask & __GFP_WAIT;struct zone **z;struct page *page;struct reclaim_state reclaim_state;struct task_struct *p = current;int do_retry;int alloc_flags;int did_some_progress;might_sleep_if(wait);if (should_fail_alloc_page(gfp_mask, order))return NULL;restart:z = zonelist->zones;  /* the list of zones suitable for gfp_mask */if (unlikely(*z == NULL)) {/* Should this ever happen?? */return NULL;}//第一次扫描各个区,这个时候设定的阈值为z->pages_low,如果分配成功就goto got_pgpage = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,zonelist, ALLOC_WMARK_LOW|ALLOC_CPUSET);//关键函数,他就是扫描所有的zonelist中的zone,找到合适的块返回块的第一个page,见下if (page)goto got_pg;/** GFP_THISNODE (meaning __GFP_THISNODE, __GFP_NORETRY and* __GFP_NOWARN set) should not cause reclaim since the subsystem* (f.e. slab) using GFP_THISNODE may choose to trigger reclaim* using a larger set of nodes after it has established that the* allowed per node queues are empty and that nodes are* over allocated.*/if (NUMA_BUILD && (gfp_mask & GFP_THISNODE) == GFP_THISNODE)goto nopage;//如果第一次扫描各个区域不成功,那就唤醒kswapd内核线程来异步执行页框回收!!for (z = zonelist->zones; *z; z++)wakeup_kswapd(*z, order);/** OK, we're below the kswapd watermark and have kicked background* reclaim. Now things get more complex, so set up alloc_flags according* to how we want to proceed.** The caller may dip into page reserves a bit more if the caller* cannot run direct reclaim, or if the caller has realtime scheduling* policy or is asking for __GFP_HIGH memory.  GFP_ATOMIC requests will* set both ALLOC_HARDER (!wait) and ALLOC_HIGH (__GFP_HIGH).*///设置alloc_flagsalloc_flags = ALLOC_WMARK_MIN;if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait)alloc_flags |= ALLOC_HARDER;if (gfp_mask & __GFP_HIGH)alloc_flags |= ALLOC_HIGH;if (wait)alloc_flags |= ALLOC_CPUSET;/** Go through the zonelist again. Let __GFP_HIGH and allocations* coming from realtime tasks go deeper into reserves.** This is the last chance, in general, before the goto nopage.* Ignore cpuset if GFP_ATOMIC (!wait) rather than fail alloc.* See also cpuset_zone_allowed() comment in kernel/cpuset.c.*///第二次扫描,这里扫描和第一次不同的是改变了阈值大小,这次使用了较低的阈值了!也就是要求变低了!page = get_page_from_freelist(gfp_mask, order, zonelist, alloc_flags);if (page)goto got_pg;/* This allocation should allow future memory freeing. */rebalance:if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE)))&& !in_interrupt()) {if (!(gfp_mask & __GFP_NOMEMALLOC)) {nofail_alloc://第三次扫描了,这一次使用了ALLOC_NO_WATERMARKS,也就是不调用zone_watermark_ok去检查阈值,只有这种情况内核才会去使用“保存的页框”/* go through the zonelist yet again, ignoring mins */page = get_page_from_freelist(gfp_mask, order,zonelist, ALLOC_NO_WATERMARKS);if (page)goto got_pg;if (gfp_mask & __GFP_NOFAIL) {congestion_wait(WRITE, HZ/50);goto nofail_alloc;}}goto nopage;}/* Atomic allocations - we can't balance anything */if (!wait)goto nopage;//检查是否还有别的进程需要CPUcond_resched();/* We now go into synchronous reclaim */cpuset_memory_pressure_bump();p->flags |= PF_MEMALLOC;reclaim_state.reclaimed_slab = 0;p->reclaim_state = &reclaim_state;//这里就开始调用try_to_free_pages寻找一些页框来回收,可能会阻塞当前进程。did_some_progress = try_to_free_pages(zonelist->zones, gfp_mask);p->reclaim_state = NULL;p->flags &= ~PF_MEMALLOC;//检查是否还有别的进程需要CPUcond_resched();if (likely(did_some_progress)) {//如果上面try_to_free_pages有释放一些页框,那就再尝试以此获取页框page = get_page_from_freelist(gfp_mask, order,zonelist, alloc_flags);if (page)goto got_pg;} else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {//如果上面try_to_free_pages没有释放一些页框,那就麻烦大了,空闲叶匡已经少到危险的地步了。/** Go through the zonelist yet one more time, keep* very high watermark here, this is only to catch* a parallel oom killing, we must fail if we're still* under heavy pressure.*///再去尝试获取一次page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,zonelist, ALLOC_WMARK_HIGH|ALLOC_CPUSET);if (page)goto got_pg;//通过out_of_memory杀死一个进程开始释放一些内存out_of_memory(zonelist, gfp_mask, order);//跳回第一步!goto restart;}/** Don't let big-order allocations loop unless the caller explicitly* requests that.  Wait for some write requests to complete then retry.** In this implementation, __GFP_REPEAT means __GFP_NOFAIL for order* <= 3, but that may not be true in other implementations.*/do_retry = 0;if (!(gfp_mask & __GFP_NORETRY)) {if ((order <= 3) || (gfp_mask & __GFP_REPEAT))do_retry = 1;if (gfp_mask & __GFP_NOFAIL)do_retry = 1;}if (do_retry) {congestion_wait(WRITE, HZ/50);goto rebalance;}nopage:if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {printk(KERN_WARNING "%s: page allocation failure."" order:%d, mode:0x%x\n",p->comm, order, gfp_mask);dump_stack();show_mem();}
got_pg:return page;
}

get_page_from_freelist函数的作用就是扫描各个内存管理区,看看各个管理区是否低于阈值,如果低于就不适用该区域,如果高于那就调用buffered_rmqueue分配页框,如下:

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order,struct zonelist *zonelist, int alloc_flags)
{struct zone **z;struct page *page = NULL;int classzone_idx = zone_idx(zonelist->zones[0]);struct zone *zone;nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */int zlc_active = 0;       /* set if using zonelist_cache */int did_zlc_setup = 0;        /* just call zlc_setup() one time */zonelist_scan:/** Scan zonelist, looking for a zone with enough free.* See also cpuset_zone_allowed() comment in kernel/cpuset.c.*/z = zonelist->zones;do {if (NUMA_BUILD && zlc_active &&!zlc_zone_worth_trying(zonelist, z, allowednodes))continue;zone = *z;if (unlikely(NUMA_BUILD && (gfp_mask & __GFP_THISNODE) &&zone->zone_pgdat != zonelist->zones[0]->zone_pgdat))break;if ((alloc_flags & ALLOC_CPUSET) &&!cpuset_zone_allowed_softwall(zone, gfp_mask))goto try_next_zone;if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {//ALLOC_NO_WATERMARKS就是不去检查各个zone的阈值unsigned long mark;if (alloc_flags & ALLOC_WMARK_MIN)mark = zone->pages_min;else if (alloc_flags & ALLOC_WMARK_LOW)mark = zone->pages_low;elsemark = zone->pages_high;if (!zone_watermark_ok(zone, order, mark,classzone_idx, alloc_flags)) {//zone_watermark_ok里面确定min阈值,然后返回该zone是否有足够的内存使用,见下if (!zone_reclaim_mode ||!zone_reclaim(zone, gfp_mask, order))goto this_zone_full;//该内存管理区满了}}page = buffered_rmqueue(zonelist, zone, order, gfp_mask);//从高速缓冲区获取,这个函数见上一小节if (page)break;
this_zone_full://内存管理区满了if (NUMA_BUILD)zlc_mark_zone_full(zonelist, z);
try_next_zone:if (NUMA_BUILD && !did_zlc_setup) {/* we do zlc_setup after the first zone is tried */allowednodes = zlc_setup(zonelist, alloc_flags);zlc_active = 1;did_zlc_setup = 1;}} while (*(++z) != NULL);if (unlikely(NUMA_BUILD && page == NULL && zlc_active)) {/* Disable zlc cache for second zonelist scan */zlc_active = 0;goto zonelist_scan;}return page;
}

zone_watermark_ok函数用来确定阈值并检查区域可用空间是否高于阈值:

int zone_watermark_ok(struct zone *z, int order, unsigned long mark,int classzone_idx, int alloc_flags)
{/* free_pages my go negative - that's OK */long min = mark;long free_pages = zone_page_state(z, NR_FREE_PAGES) - (1 << order) + 1;int o;if (alloc_flags & ALLOC_HIGH)min -= min / 2;if (alloc_flags & ALLOC_HARDER)min -= min / 4;if (free_pages <= min + z->lowmem_reserve[classzone_idx])return 0;for (o = 0; o < order; o++) {//检查大于order的块链表,小于order的不需要检查了/* At the next order, this order's pages become unavailable */free_pages -= z->free_area[o].nr_free << o;/* Require fewer higher order pages to be free */min >>= 1;if (free_pages <= min)return 0;}return 1;
}

连续内存区管理

本节针对连续物理地址的内存区域管理,主要介绍slab分配机制。

slab分配器介绍

高速缓存的主内存区被分为多个slab,每个slab由一个或多个连续的页框组成,这些页框被分为多个大小相等且连续的对象,后面分配也就是这些个对象。如图所示:

接下来介绍几个关键的结构体

高速缓存描述符

高速缓存描述符结构体为kmem_cache_t类型:

类型 名称 说明
struct array_cache * [] array 每CPU指针数组的本地高速缓存,后面会详细介绍
unsigned int batchcount 要转移进本地高速缓存或从本地高速缓存转出的对象数
unsigned int obj_size 本高速缓存管理下的对象大小(固定值)
struct kmem_list3 lists 高速缓存中的slab链表(有三个链表,见后面)
unsigned int gfporder 一个单独的slab中包含的连续页框数目的对数。
其余字段省略。。。

其中lists是一个结构体kmem_list3

类型 名称 说明
struct list_head slabs_partial 包含空闲和非空闲对象的slab描述符
struct list_head slabs_full 不包含空闲对象的slab。。
struct list_head slabs_free 只包含空闲对象的slab。。
unsigned long free_objects 高速缓冲区空闲对象的个数

slab描述符

类型 名称 说明
struct list_head list 双向链表!
unsigned long colouroff 第一个对象的偏移量(slab着色)
void * s_mem slab中第一个对象的偏移
unsigned int inuse 当前正在被使用的对象个数
unsigned int free slab中下一个空闲对象的下标

slab描述符可以存放在两个地方:

  1. 外部slab描述符:存在于slab的外部。
  2. 内部slab描述符:存放于slab的内部。

高速缓存描述符和slab描述符之间的关系:

普通和专用高速缓存

高速缓存被分为两大类:普通和专用,普通高速缓存只由slab分配器用于自己的目的,专用高速缓存由内核的其余部分使用。

普通高速缓存是:

  1. 第一个高速缓存叫做kmem_cache,包含由内核使用的其余高速缓存的高速缓存描述符!
  2. 另外有13个不同对象大小的高速缓存,一个叫做malloc_sizes的表分别指向26个高速缓存描述符,对应对象大小为32、64、128…131072字节的dma区域分配和常规分配的高速缓存。

slab分配器与分区页框分配器的接口

当slab分配器创建新的slab时,它依靠分区页框分配器来获取一组连续的空闲页框。为了完成这一操作,系统会调用kmem_getpages函数,如下:

void * kmem_getpages(kmem_cache_t *cachep, int flags){struct page *page;int i;flags |= cachep->gfpflags;page = alloc_pages(flags, cachep->gfporder);if(!page) return NULL;i = (1 << cachep->gfporder);if(cachep->flags & SLAB_RECLAIM_ACCOUNT)atomic_add(i,&slab_reclaim_pages);while(i--){SetPageSlab(page++);}return page_address(page);
}

相反通过调用kmem_freepages函数释放slab的页框:

void kmem_freepages(kmem_cache_t *cachep, void * addr){unsigned long i = (1<<cachep->gfporder);struct page *page = virt_to_page(addr);if(current->reclaim_state)current->reclaim_state->reclaim_slab += i;while(i--)ClearPageSlab(page++);free_pages((unsigned long)addr, cachep->gfporder);if(cachep->flags & SLAB_RECLAIM_ACCOUNT)atomic_sub(1<<cachep->gfporder,&slab_reclaim_pages);
}

给高速缓存分配slab

当创建一个高速缓存没有包含任何slab和对象,当发出分配对象的请求后且高速缓存不包含任何空闲对象时,才会分配slab。此时,slab分配器会调用cache_grow()给高速缓存分配一个新的slab。这个函数首先调用kmem_getpages函数从分区页框分配器获得一组页框来存放一个单独的slab。然后调用alloc_slabmgmt()获得一个新的slab描述符,此时会看CFLGS_OFF_SLAB标志,从而实现外部slab描述符/内部slab描述符,如果是内部的,就在第一个页框中分配slab描述符。

当我们给定一个页框,内核必须确定它属于哪个高速缓存和哪个slab,所以cache_grow会扫描给新的slab分配的所有页框,把他们的lru字段的next和pre分别置为高速缓存描述符和slab描述符!相反的,当给定一个slab,也需要确定哪些页框属于它,系统通过slab描述符的s_mem字段(起始位置)和高速缓存描述符的gfporder字段(大小)来确定。

然后cache_grow调用cache_init_objs(),初始化slab的所有对象。

最后,调用list_add_tail(),把slab描述符添加到高速缓存中的全空链表上。

从高速缓存中释放slab

调用slab_destory()来撤销一个slab:

void slab_destory(kmem_cache_t *cachep, slab_t *slabp){if(cachep->dtor){int i;for(i = 0; i<cachep->num; i++){void *objp = slabp->s_mem + cachep->objsize*i;  //找到对象(cachep->dtor)(objp, cachep, 0);               //析构对象}}kmem_freepages(cachep, slab->s_mem - slabp->colouroff); //释放页框,这里slabp->colouroff是slab着色相关,见后面if(cachep->flags & CFLGS_OFF_SLAB)  //如果是外部slab,就要手动释放描述符。kmem_cache_free(cachep->slabp_cache, slabp);
}

对象描述符

每个对象都有类型为kmem_bufctl_t的描述符,但其实他就是一个unsigned int而已,对象描述符存放在一个数组中,位于相应的slab描述符之后,因此对象描述符数组也分为外部和内部。对象描述符只不过就是一个无符号整数,只有在对象空闲的时候才有意义,它包含的是下一个空闲对象的下标,因此实现了slab内部空闲对象的简单链表。

对齐内存中的对象

对象在slab中一般存放是对齐的,存放他们的内存单元的起始物理地址是一个给定常量的倍数,通常是2的倍数,这个常量就是对齐因子。

slab着色

当不同的slab内具有相同偏移量的对象最终很可能映射在同一个高速缓存行中。高速缓存的硬件可能因此而花费内存周期在同一行高速缓存行与RAM内存单元之间来来往往传送两个对象,而其他高速缓存行没有充分利用!linux通过slab着色来解决这一问题:随机分配一个颜色(随机整数)给slab,从而让它后面的对象都错开。

如图所示:

颜色值col,那么整个slab就偏移col*aln,最终把col*aln+dsize的值存放在slab->colouroff中。

空闲slab对象的本地高速缓存

slab分配器为每个高速缓存都安排了一个每CPU数据结构,该结构由一个指向被释放对象的小指针数组组成。高速缓存描述符中的array字段是一组指向array_cache数据结构的指针,系统中每个CPU对应一个元素,array_cache是空闲对象的本地高速缓存的一个描述符:

类型 名称 说明
unsigned int avail 本地高速缓存中可用的空闲对象个数
unsigned int limit 本地高速缓存中对象个数的最大值
unsigned int batchcount 本地高速缓存重新填充/腾空时操作的对象个数
unsigned int touched 标记最近有没有被使用过

分配slab对象

通过调用函数**kmem_cache_alloc()**函数获得新对象:

void *kmem_cache_alloc(kmem_cache_t *cachep, int flags){unsigned long save_flags;void *objp;struct array_cache *ac;local_irq_save(save_flags);ac = cachep->array[smp_processor_id()];if(ac->avail){//如果本地高速缓存还有的多。。。ac->touched = 1;objp = ((void **)(ac+1))[--ac->avail];}else      //否则调用cache_alloc_refillobjp = cache_alloc_refill(cachep, flags);local_irq_restore(save_flags);return objp;
}

**cache_alloc_refill()**函数用来从slab中把空闲的对象加载到本地高速缓存,具体步骤:

  1. 将本地高速缓存描述符存放在局部变量中:

​ ac = cachep->array[smp_processor_id()];

  1. 获得cachep->spinlock。

  2. 如果slab高速缓存包含本地高速缓存,并且该共享本地高速缓存包含一些空闲对象,函数就通过从共享本地高速缓存中移动ac->batchcount个指针来填充CPU的本地高速缓存。然后跳转到第6步。

  3. 函数试图填充本地高速缓存,填充数目为ac->batchcount个指针:

    1. 查看高速缓存 描述符中slabs_partial和slabs_free链表,并获得slab描述符slabp,slabp指向的slab为部分为空或全空。如果不存在slabp,跳转到第5步.

    2. 对于slab中的每个空闲对象,函数将他们插入到本地高速缓存,增加slab描述符的inuse字段,并更新free字段使得free指向下一个空闲对象的下标:

      ​ slabp->inuse ++;

      ​ ((void**)(ac + 1))[ac->avail++] = slabp->s_mem + slabp->free * cachep->objsize;

      ​ slabp->free = ((kmem_bufctl_t *)(slabp+1))[slabp->free];

    3. 将slabp插入适当的链表,可以实slabs_full或slabs_partial。

  4. 在这一步,被添加到本地高速缓存的指针个数被存放在ac->avail中,函数递减同样数量的kmem_list3结构的free_objects字段。

  5. 释放cachep->spinlock;

  6. 如果现在ac->avail大于0(相当于填充了一些指针了),函数将ac->touched设为1,并返回最后插入到本地高速缓存的空闲指针:

    ​ return ((void **)(ac + 1))[–ac->avail];

  7. 否则,上面没有发生填充操作,调用cache_grow()获取一个新的slab,如果成功再跳转到第1步,否则返回NULL。

释放slab对象

调用**kmem_cache_free()**函数释放一个对象:

void kmem_cache_free(kmem_cache_t *cachep, void *objp){unsigned long flags;struct array_cache *ac;local_irq_save(flags);ac = cachep->array[smp_processor_id()];if(ac->avail == ac->limit)//空闲的满了,要去释放了!cache_flusharray(cachep, ac);((void **)(ac+1))[ac->avail++] = objp;local_irq_restore(flags);
}

空闲的满了,要去释放就调用cache_flusharray

  1. 获得cachep->spinlock。

  2. 如果slab高速缓存包含一个共享的本地高速缓存,并且该共享的本地缓存还没有满,函数就会通过从本CPU的本地高速缓存移动ac->batchcount个指针来填充共享的本地高速缓存。

  3. 调用free_block()函数将当前包含在本地高速缓存中的ac->batchcount个指针归还给相应的slab。对于每一个对象objp,执行如下操作:

    1. 增加高速缓存描述符的lists->free_objcts字段。

    2. 确定objp属于哪个slab(根据lru.prev确定):

      ​ slabp = (struct slab *)(virt_to_page(objp)->lru.prev);

    3. 把它从链表上摘下来。

    4. 计算slab内对象的下标:

      ​ objnr = (objp - slabp->s_mem) / cachep->objsize;

    5. 更新slabp->free的值,从而形成空闲对象链表:

      ​ ((kmem_bufctl_t *)(slabp + 1))[objnr] = slabp->free;

      ​ slabp->free = objnr;

    6. 递减slabp->inuse字段。

    7. 当slabp->inuse等于0时,整个slab就是完全空闲的了。此时如果整个高速缓存的空闲对象个数大于规定值,就去释放这个slab:

      ​ if(cachep->lists.free_objects > cachep->free_limit){

      ​ cachep->lists.free_objects -= cachep->num;

      ​ slab_destory(cachep, slabp);

      ​ }

    8. 将slab插入当适当的链表。

  4. 释放cachep->spinlock。

  5. 更新ac->avail;

通用对象

本小节介绍kmalloc和kfree函数:

void *kmalloc(size_t size, int flags){struct cache_sizes *csizep = malloc_sizes;kmem_cache_t * cachep;for(; csizep->cs_size; csizep++){if(size > csizep->cs_size) continue;if(flags & __GFP_DMA)cachep = csizep->cs_dmacachep;elsecachep = csizep->cs_cachep;return kmem_cache_alloc(cachep, flags);}return NULL;
}void kfree(const void *objp){kmem_cache_t * c;unsigned long flags;if(!objp)return;local_irq_save(flags);c = (kmem_cache_t *)(virt_to_page(objp)->lru.next);kmem_cache_free(c, (void *)objp);local_irq_restore(flags);
}

非连续内存区管理

在前面一节中,内存区映射成一组连续的页框,可以充分利用高速缓存并获得较低的平均访问时间。但是如果对内存的访问不是很频繁,通过连续的线性地址来访问非连续的页框就很有意义!非连续内存区管理的优点是可以避免外碎片,但是缺点是必须打乱内核页表。非连续内存区的大小必须是4096的倍数。

非连续内存区的线性地址

要找到线性地址的一个空闲区域,可以从PAGE_OFFSET开始查找(也就是第四个GB的起始),如图所示:

  1. 内存区的开始部分是前896MB的物理内存的线形映射,也就是图中的物理内存映射。
  2. 内存区的结尾部分是固定映射的线性地址。
  3. 从PLMAP_BASE开始是用于高端内存页框的永久内核映射的线性地址。
  4. 其他的地址空间可以用于非连续内存区。在物理内存映射之后留有8MB的安全区,这是为了捕获内存越界访问。同样的原因,在每一个vmalloc区域之间都插入了4KB的安全区域。

非连续内存区保留的线性地址空间的起始地址有VMALLOC_START宏定义保存,末尾地址由VMALLOC_END宏定义保存。

非连续内存区的描述符

每个非连续内存区都对应着类型为vm_struct的描述符:

类型 名称 说明
void * addr 内存区内第一个内存单元的线性地址。
unsigned long size 内存区的大小+4096(安全区大小)
unsigned long flags 非连续内存区映射的内存类型
struct page ** pages 指向nr_pages数组的指针,该数组由指向页描述符的指针组成
unsigned int nr_pages 内存区填充页的个数
unsigned long phys_addr 该字段设为0,除非内存已被创建来映射一个硬件设备的io共享内存
struct vm_struct * next 指向下一个vm_struct结构的指针

next字段可以形成一个链表,其链表的第一个元素存放在vmlist变量中,对这个链表的访问依靠vmlist_lock读写自旋锁来保护。flags字段标识了内存类型:VM_ALLOC表示使用了vmalloc函数获取到页,VM_MAP表示vmap函数获取的页,VM_IOREMAP表示ioremap映射的物理设备。

**get_vm_area()**函数在线性地址VMALLOC_START和VMALLOC_END之间查找一个空闲区域,接收两个参数:size和flag,源码如下:

static struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long flags,unsigned long start, unsigned long end,int node, gfp_t gfp_mask)
{struct vm_struct **p, *tmp, *area;unsigned long align = 1;unsigned long addr;BUG_ON(in_interrupt());if (flags & VM_IOREMAP) {int bit = fls(size);if (bit > IOREMAP_MAX_ORDER)bit = IOREMAP_MAX_ORDER;else if (bit < PAGE_SHIFT)bit = PAGE_SHIFT;align = 1ul << bit;}addr = ALIGN(start, align);size = PAGE_ALIGN(size);if (unlikely(!size))return NULL;area = kmalloc_node(sizeof(*area), gfp_mask & GFP_LEVEL_MASK, node);//kmalloc创建一个vm_struct结构体if (unlikely(!area))return NULL;/** We always allocate a guard page.*/size += PAGE_SIZE;//加上安全区4096write_lock(&vmlist_lock);//上锁for (p = &vmlist; (tmp = *p) != NULL ;p = &tmp->next) {if ((unsigned long)tmp->addr < addr) {if((unsigned long)tmp->addr + tmp->size >= addr)addr = ALIGN(tmp->size + (unsigned long)tmp->addr, align);continue;}if ((size + addr) < addr)goto out;if (size + addr <= (unsigned long)tmp->addr)//找到能容纳下size大小的区域了goto found;addr = ALIGN(tmp->size + (unsigned long)tmp->addr, align);if (addr > end - size)goto out;}found:area->next = *p;*p = area;//这里注意前面声明是**p,所以这样就可以直接插入链表了!area->flags = flags;area->addr = (void *)addr;area->size = size;area->pages = NULL;area->nr_pages = 0;area->phys_addr = 0;write_unlock(&vmlist_lock);return area;out:write_unlock(&vmlist_lock);kfree(area);if (printk_ratelimit())printk(KERN_WARNING "allocation failed: out of vmalloc space - use vmalloc=<size> to increase size.\n");return NULL;
}

分配非连续内存区

**vmalloc()**函数给内核分配一个非连续内存区:

void * vmalloc(unsigned long size){struct vm_struct *area;struct page **pages;unsigned int array_size, i;size = (size + PAGE_SIZE - 1) & PAGE_MASK;    //4096的整数倍area = get_vm_area(size, VM_ALLOC);if(!area) return NULL;area->nr_pages = size >> PAGE_SHIFT;array_size = (area->nr_pages * sizeof(struct page *));area->pages = pages = kmalloc(array_size, GFP_KERNEL);//用于存放struct page **if(area->pages){remove_vm_area(area->next);kfree(area);return NULL;}memset(area->pages,0,array_size);for(i=0; i<area->nr_pages; i++){area->pages[i] = alloc_page(GFP_KERNEL | __GFP_HIGHMEM);  //从高端内存申请if(!area->pages[i]){area->nr_pages = i;
fail:       vfree(area->addr);return NULL;}}if(map_vm_area(area, __pgprot(0x63), &pages))    //物理地址 转化为 线性地址goto fail;return area->addr;
}

其中最重要的是map_vm_area函数,这个函数用来修改内核页表!

需要传入三个参数:

  1. area:指向内存区的vm_struct描述符指针。
  2. prot:已分配页框的保护位,它总被置为0x63。
  3. pages:物理页框指针数组,类型为struct page ***。

首先函数把内存区的开始和末尾的线性地址分别分配给局部变量address和end:

address = area->addr;
end = address + (area->size - PAGE_SIZE);

然后使用pgd_offset_k宏定义得到主内核页全局目录中的目录项(详细见第二张分页部分),然后获取内核页表自旋锁:

pgd = pgd_offset_k(address);
spin_lock(&init_mm.page_table_lock);

然后执行下列循环:

int ret = 0;
for(i = pgd_index(address);i<pgd_index(end-1);i++){pud_t *pud = pud_alloc(&init_mm, pgd, address);   //分配一个页上级目录,并写到pgd的位置上。ret = -ENOMEM;if(!pud)break;next = (address + PGDIR_SIZE) & PGDIR_MASK;if(next < address || next > end)next = end;if(map_area_pub(pub,address,next,prot,pages)) //去处理页上级目录break;address = next;pgd ++;ret = 0;
}
spin_unlock(&init_mm.page_table_lock);
flush_cache_vmp((unsigned long)area->addr,end);
return ret;

循环结束后所有的非连续内存的页表项全被建立!

map_area_pub函数用来处理页上级目录!

do{pmd_t *pmd = pmd_alloc(&init_mm,pud,address);if(!pmd)return -ENOMEM;if(map_area_pmd(pmd,address,end-address.prot,pages))return -ENOMEM;address = (address + PUD_SIZE)&PUB_MASK;pud++;
}while(address<end);

同样的map_area_pmd去处理页中间目录:

do{pte_t *pte = pte_alloc_kernel(&init_mm,pmd,address);if(!pte)return -ENOMEM;if(map_area_pte(pte,address,end-address.prot,pages))return -ENOMEM;address = (address + PMD_SIZE)&PMD_MASK;pmd++;
}while(address<end);

最后由map_area_pte函数处理最后的页框:

do{struct page *page = **pages;set_pte(pte,mk_pte(page,prot));address += PAGE_SIZE;pte++;(*pages)++;
}while(address<end);

释放非连续内存

**vfree()用来释放vmalloc或vmalloc_32创建的非连续内存区,而vunmap()用来释放vmap创建的内存区,这两个函数都调用了__vunmap()**函数来处理。

**__vunmap()**接收两个参数:addr(起始线形地址)、deallocate_pages(标志)。

**__vunmap()**主要做了以下内容:

  1. 调用remove_vm_area()函数得到vm_struct,并清楚相应的页表项。
  2. 如果deallocate_pages被置位,函数扫描指向页描述符的area->pages指针数组;对于数组的每一个元素,调用__free_page函数释放页框。并调用kfree(area->pages)释放数组本身。
  3. 调用kfree(area)释放vm_struct描述符。

remove_vm_area执行如下:

write_lock(&vmlist_lock);
for(p = &vmlist;(tmp=*p);p=&tmp->next){if(tmp->addr = addr){unmap_vm_area(tmp);*p = tmp->next;break;}
}
write_unlock(&vmlist_lock);
return tmp;

内存区通过unmap_vm_area函数来释放,其执行的是map_vm_area的反向操作:

address = area->addr;
end = address + area->size;
pgd = pgd_offset_k(address);
for(i=pgd_index(address);i<=pgd_index(end);i++){next = (address + PGDIR_SIZE)&PGDIR_MASK;if(next<=address||next>end)next = end;unmap_area_pud(pgd,address,next-address);address = next;pgd++;
}

unmap_area_pud如下:

do{unmap_area_pmd(pub,address,end-address);address = (address + PUD_SIZE)&PUD_MASK;pud++;
}while(address&&(address<end));

unmap_area_pmd如下:

do{unmap_area_pte(pmd,address,end-address);address = (address + PMD_SIZE)&PMD_MASK;pmd++;
}while(address<end);

最后,unmap_area_pte如下:

do{pte_t page = ptep_get_and_clear(pte);address += PAGE_SIZE;pte++;if(!pte_none(page)&&!pte_present(page))printk("...");
}while(address<end);

ptep_get_and_clear宏将pte指向的页表项设为0。

深入理解Linux内核-内存管理相关推荐

  1. 深入理解Linux内核---内存管理zone

    转载:https://blog.csdn.net/gatieme/article/details/52384529 https://blog.csdn.net/gatieme/article/deta ...

  2. Linux内核内存管理(3):kmemcheck介绍

    Linux内核内存管理 kmemcheck介绍 rtoax 2021年3月 在英文原文基础上,针对中文译文增加5.10.13内核源码相关内容. 5.10.13不存在kmemcheck的概念,取代的是k ...

  3. Linux内核内存管理(1):内存块 - memblock

    Linux内核内存管理 内存块 - memblock rtoax 2021年3月 在英文原文基础上,针对中文译文增加5.10.13内核源码相关内容. 1. 简介 内存管理是操作系统内核中最复杂的部分之 ...

  4. Linux内核内存管理(2):固定映射地址(fixmap)和输入输出重映射(ioremap)

    Linux内核内存管理 固定映射地址(fixmap)和输入输出重映射(ioremap) rtoax 2021年3月 在英文原文基础上,针对中文译文增加5.10.13内核源码相关内容. Print ke ...

  5. 【Linux 内核 内存管理】虚拟地址空间布局架构 ③ ( 内存描述符 mm_struct 结构体成员分析 | mmap | mm_rb | task_size | pgd | mm_users )

    文章目录 一.mm_struct 结构体成员分析 1.mmap 成员 2.mm_rb 成员 3.get_unmapped_area 函数指针 4.task_size 成员 5.pgd 成员 6.mm_ ...

  6. 【Linux 内核 内存管理】内存管理架构 ④ ( 内存分配系统调用过程 | 用户层 malloc free | 系统调用层 brk mmap | 内核层 kmalloc | 内存管理流程 )

    文章目录 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) 二.内存管理流程 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) " 堆内存 " ...

  7. 【Linux 内核 内存管理】内存管理架构 ② ( 用户空间内存管理 | malloc | ptmalloc | 内核空间内存管理 | sys_brk | sys_mmap | sys_munmap)

    文章目录 一.用户空间内存管理 ( malloc / free / ptmalloc / jemalloc / tcmalloc ) 二.内核空间内存管理 1.内核内存管理系统调用 ( sys_brk ...

  8. 【Linux 内核 内存管理】优化内存屏障 ③ ( 编译器屏障 | 禁止 / 开启内核抢占 与 方法保护临界区 | preempt_disable 禁止内核抢占源码 | 开启内核抢占源码 )

    文章目录 一.禁止 / 开启内核抢占 与 方法保护临界区 二.编译器优化屏障 三.preempt_disable 禁止内核抢占 源码 四.preempt_enable 开启内核抢占 源码 一.禁止 / ...

  9. 【Linux 内核 内存管理】RCU 机制 ② ( RCU 机制适用场景 | RCU 机制特点 | 使用 RCU 机制保护链表 )

    文章目录 一.RCU 机制适用场景 二.RCU 机制特点 三.使用 RCU 机制保护链表 一.RCU 机制适用场景 在上一篇博客 [Linux 内核 内存管理]RCU 机制 ① ( RCU 机制简介 ...

最新文章

  1. 安装完python后、还需要安装什么-安装python后
  2. 初识mysql学习笔记
  3. HDU 3530 Subsequence
  4. apache2+支持php7,Ubuntu14.04下配置PHP7.0+Apache2+Mysql5.7
  5. oracle数据库内核,深入内核:Oracle数据库里SELECT操作Hang解析
  6. ubuntu 20.04 DNS 设置
  7. 火山引擎 veStack 在企业办公场景的落地实践
  8. 微软欲打造开发者联盟!
  9. 基于OWIN WebAPI 使用OAUTH2授权服务【授权码模式(Authorization Code)】
  10. No package ‘dconf‘ found
  11. Nginx面试题整理
  12. 我们要不要和to B“霸王龙”企业交朋友?
  13. 张量基础2(张量乘法和对称)
  14. asp.net基于net的小美果蔬批发网-蔬菜商城系统-计算机毕业设计
  15. 关于SQL server 2000 在安装过程中遇到文件挂起的解决办法
  16. macOS VirtualBox 安装步骤
  17. 关于虚拟机检测技术的研究
  18. java入门之控制台输入人数成绩计算及格率(将成绩存入数组)与打印九九乘法表
  19. python文件对象
  20. 网络安全工程师需要考什么证吗?

热门文章

  1. 光纤激光切割机功率下降的原因分析
  2. MOS管的作用及原理介绍
  3. 售饭机系统无法连接服务器,广东食堂售饭机方案
  4. RouterOS流量控制
  5. java多线程run方法传参
  6. 【项目案例】配置小型网络WLAN基本业务示例
  7. 精美html个人主页
  8. 招商银行专业版,交易记录下载到本地是乱码怎么办
  9. 05-初识并发问题(抢火车票)
  10. 学习笔记-Metasploit