内存中,某些部分被永久的分配给内核,用来存放内核代码和静态内核数据结构。其余的部分叫做动态内存。本章描述了内核如何给自己分配动态内存。一般情况下,我们使用kmalloc分配小块内存,使用vmalloc分配大块内存。
1:页框管理
下面的图就显示了内存中,动态内存或者被保留的内存的分布情况。
1.1:页描述符
页框,也就是物理内存上的4K或者其它大小的物理页。内核必须记录每个页框的当前状态。这些状态信息保存在一个page结构体中,这个结构体叫做页描述符。
struct page { /** @rcu_head: You can use this to free a page by RCU. */ union { /* This union is 4 bytes in size. */ /* unsigned int active; /* SLAB */ /* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */ |
struct page *mem_map; |
page结构体的lru成员有以下用途:
1:当page是空闲的时候,lru链接在伙伴系统上:
zone -> free_area[order] ->free_list[migratetype] = page -> lru |
1.2:非一致性内存访问
非一致性内存访问模型(NUMA模型):在这种模型下,给定CPU对不同的内存单元的访问用时可能不同。系统的物理内存被划分成几个节点。同一CPU访问不同节点中的内存需要的时间不同;同样,不同CPU访问同一节点需要的时间也不一样。
不过只考虑X86系统,他是一致访问内存模型。因此,i386只有一个节点。这个节点使用全局变量描述:
struct pglist_data contig_page_data = { .bdata = &contig_bootmem_data }; |
所有的节点描述符会使用链表串联起来。当然,i386只有一个节点,因此:
struct pglist_data *pgdat_list; // pgdat_list=& contig_page_data |
节点的数据结构如下:
/* /* int nr_zones; /* number of populated zones in this node */ int kswapd_failures; /* Number of 'reclaimed == 0' runs */ /* Write-intensive fields used by page reclaim */ /* Fields commonly accessed by the page reclaim scanner */ /* unsigned long flags; ZONE_PADDING(_pad2_) /* Per-node vmstats */ |
1.3:内存管理区
Linux系统中包含的内存管理区可以查看/proc/zoneinfo。
每个内存管理区都有自己的描述符,见结构体zone:
/* zone watermarks, access with *_wmark_pages(zone) macros */ unsigned long nr_reserved_highatomic; /* /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */ /* const char *name; int initialized; /* Write-intensive fields used from the page allocator */ /* free areas of different sizes */ /* zone flags, see below */ /* Primarily protects free_area */ /* Write-intensive fields used by compaction and vmstats. */ /* bool contiguous; ZONE_PADDING(_pad3_) |
现在我们有了页框,内存节点,内存管理区的概念,还需要将页框和他们联系起来。使用函数
static inline struct zone *page_zone(const struct page *page) { return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)]; } |
/* Page flags: | [SECTION] | [NODE] | ZONE | [LAST_CPUPID] | ... | FLAGS | */ |
可以从页框描述符获得他所在的内存管理区的描述符。
1.4:保留的页框池
在请求内存的时候,如果有足够的空闲内存,请求就会立即满足;否则的话,将发出请求的内核控制路径阻塞,直到有内存被释放。
但是在某些请求中,无法阻塞内核控制路径(例如在中断或者在临界区代码中)。这种内存分配叫做原子内存分配请求。这时候页框分配会失败并且返回。但是,我们希望这种情况尽量少发生,尽量保证有内存可以分配。因此,我们引入保留的页框池,只有在内存不足的时候才能使用。
保留的内存数量使用参数min_free_kbytes表示,可以通过文件/proc/sys/vm/min_free_kbytes查看
每个内存区都将他们的一部分页框用于保留的页框池,在管理区描述符的_watermark[WMARK_MIN]字段,就表示了管理区内保留页框的数目。
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10); if (new_min_free_kbytes > user_min_free_kbytes) { //根据系统中总的动态内存(也就是不包含保留内存)计算得到min_free_kbytes。这个值可以通过/proc/sys/vm/min_free_kbytes查看 #ifdef CONFIG_NUMA khugepaged_min_free_kbytes_update(); return 0; |
每个内存管理区中,还有另一个参数值得关注,就是zone->lowmem_reserve数组
/* for_each_online_pgdat(pgdat) { for (j = i + 1; j < MAX_NR_ZONES; j++) { managed_pages += zone_managed_pages(upper_zone); if (clear) /* update totalreserve_pages */ |
通过上面的函数,可以知道,zone->lowmem_reserve是一个数组。假设我们的系统中有三个内存区,那么每个内存区的lowmem_reserve数组的内容如下所示(32和256的值来源于数组int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES-1] = { 256, 32 };):
0 |
1 |
2 | |
DMA->lowmem_reserve |
0 |
NORMAL->managed_pages/256 |
NORMAL->managed_pages/256+HIGHMEM->managed_pages/32 |
NORMAL->lowmem_reserve |
0 |
0 |
HIGHMEM->managed_pages/32 |
HIGHMEM->lowmem_reserve |
0 |
0 |
0 |
-
可以认为,这个数组中的内容是,当我们指定从NORMAL内存区中分配内存的时候,如果NORMAL中空闲内存不够,从DMA中去分配空闲内存的时候,不仅要满足一般的水线要求,还要求空闲内存要大于lowmem_reserve指定的内存。也就是说,当我们分配内存,我们尽量避免从低内存区借用。
1.5:分区页框分配器
被称为分区页框分配器的内核子系统用处理连续页框分配请求。
在请求分配的时候,分配器搜索一个包含能够满足要求的连续页框的管理区。在每个管理区中,使用伙伴系统处理页框。还有一小部分页框保留在高速缓存中,用于快速满足对单个页框的分配请求(也就是每cpu页框高速缓存)。
1.6:高端内存页框的内核映射(不适用于x86_64或arm64)
全局变量high_memory用于表示高端内存的物理地址(896M)。高端内存的页框并不会直接映射在内核线性地址空间中(虚拟地址3~4G,实际为3~3G+896M)。因此,内核不能直接访问高端内存。
在32位X86系统中,为了能够使用高端内存,需要做以下处理:
1:高端内存页框通过alloc_pages()或者alloc_page()分配。他们不返回页框的线性地址。因为高端内存的页框的线性地址不存在(不能确定,需要在映射的时候才能确定高端内存对应的线性地址;低端内存对应的线性地址一定是3G+physical_addr)。而是返回分配的页框的页描述符的线性地址。
2:因为高端内存的页框没有线性地址,不能被内核访问。所以,内核线性地址空间的最后128M中的一部分专门用于映射高端内存页框。不同的高端内存都会使用这128M的线性地址。
内核使用三种不同的机制将页框映射到高端内存,分别是永久内核映射,临时内核映射,非连续内存分配。
建立永久内核映射可能会阻塞当前进程,此时空闲页表项不存在。因此,不能在中断处理程序和可延迟函数中建立永久内核映射。
建立临时内核映射一定不会阻塞当前进程,并且他要保证当前没有其他的内核控制路径使用这个用于建立临时内核映射的页表项,以避免其他内核控制路径使用这个页表项映射其他的高端内存页。
上图也可很清晰的说明,内核的线性地址空间的使用情况。
1.6.1:永久内核映射
永久内核映射用于建立高端页框到内核地址空间的长期映射。建立永久内核映射的过程可能发生阻塞(空闲页表项不存在)。因此,不能在中断或者临界区中建立永久内核映射。
注意,i386中存在永久内核映射,但是其他架构中不存在这个东西。不要以为则会个特性是所有架构通用的。
永久内核映射建立的页表地址存放在全局变量pkmap_page_table中
|
|
此外,还使用数组
|
来描述永久内核映射页表项是否有效。
可以看到,永久内核映射对应的线性地址为PKMAP_BASE~FIXADDR_START,大小为4M。
通过页表,我们可以使用虚拟地址(线性地址),很方便的找到物理地址(也就是对应的页框)。但是,如果我们知道物理地址的话,如何去获得这个物理地址对应的线性地址呢?我们使用全局数组page_address_htable来辅助获得一个高端内存页框对应的线性地址。
|
这是一个有128个成员的数组。每个数组成员都有一个自旋锁,并且还是一个链表的头结点。这个链表链接的是具有相同哈希值的struct page_address_map结构体。这个结构体中,描述了物理页框(物理地址,由struct page *page描述)到线性地址(由void *virtual描述)的转换关系。
|
|
使用。
在系统中,这两个数据结构的组织结构如下图所示:
介绍一下,Linux是如何将一个映射过后的高端内存页框添加到这个哈希表中的。使用函数set_page_address
|
至此,我们明白了这两个数据结构的使用方法。
page_address()函数用于返回页框对应的线性地址。如果页框属于高端内存,并且没有被映射,就返回NULL。
|
介绍一下函数lowmem_page_address
|
|
要建立高端内存的永久内核映射,需要使用kmap()函数。
|
|
map_new_virtual此函数的主要操作是,检查pkmap_page_table中是否有空闲的页表项位置(此页表专门用于存储高端内存的映射关系)。如果有的话,那么就填入这个位置;如果没有的话,就将进程加入等待队列,直到另一个进程释放这个高端内存页表中的一个位置,然后唤醒等待队列中的进程。
|
通过这里建立页表项的过程,我们也能够得到高端页表的组织形式,如下图所示:
也就是说,页表项中并没有线性地址。线性地址是根据pte是高端内存页表的第几项决定的,里面只有20位物理地址,以及12位访问权限。
这里也是一个使用等待队列的典型例子。
|
|
|
因此,展开后,就是
|
同样,有建立虚拟地址和高端内存映射的接口,也有取消虚拟地址和高端内存映射的接口。通过函数kunmap可以实现这个功能。
|
如果确认是高端内存的页框,那么就会调用函数kunmap_high
|
1.6.2:临时内核映射
还是强调一下,永久内核映射和临时内核映射,还有后面的非连续内存映射,都是使用3G+896M~4G的线性地址空间来映射大于896M的物理地址空间。他们使用的线性地址空间如下所示:
使用临时内核映射的时候,考虑一个问题,两个进程使用了同一个临时内核映射(同样的线性地址A,映射到不同的物理地址B,C),这种情况会造成混乱。因此,要求使用临时内核映射的内核控制路径不能被阻塞抢占,不然就会有上面说说的那种混乱情况发生。
通过临时内核映射来建立内核到高端内存的页表项。并且由于他们不会阻塞的特性,可以用在中断处理程序和可延迟函数中。
临时内核映射使用的线性地址,是固定映射的线性地址的一部分。
|
通过函数kmap_atomic建立临时内核映射。
|
可见,如果之前已经建立了临时内核映射,这里会直接覆盖之前建立的映射。
1.7:伙伴系统算法
上面讲的是,内核建立映射的时候,线性地址是怎么来的。他们之间保持了
物理地址+PAGE_OFFSET=线性地址 |
的关系。
在后面,我们主要描述,如何给内核分配物理地址空间,也就是分配动态内存。
内核通过伙伴系统,为自己分配页框。要注意,内核为自己分配页框的时候,不会延后;内核为用户进程分配页框的时候,总是会延后分配。
页框的分配过程中,会出现外碎片的问题。外碎片就是,随着频繁的请求和释放一系列大小不同的页框,会导致在已分配的页框中分散了许多小块的空闲页框。因此,可能系统中剩余的空闲页框的大小大于要分配的页框大小,但是由于不连续,已经无法分配了。
为了解决外碎片的问题,通过伙伴算法这一类算法,避免外碎片的大量产生。
Linux使用伙伴系统算法来解决外碎片的问题。他将所有的空闲页框分组为11个链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续的页框,也就是4K~4M大小的连续页框。并且,如果一个页框是4M空闲页框的第一个页框,要求这个页框的物理地址必须按照4M对齐。
1.7.1:数据结构
每个管理区使用不同的伙伴系统。
伙伴系统需要用到下面的数据成员:
1:管理区包含的页框。每个管理区包含的页框都是mem_map的子集。struct page *zone_mem_map:表示了这个管理区包含的第一个页框描述符;size:表示了这个管理区包含的页框的个数。
2:管理区描述符中struct free_area free_area[MAX_ORDER]:每个元素对应的就是一种伙伴系统大小(order)。比如第k个元素,对应的就是包含1<<k个页框的空闲块。
struct free_area { |
他的free_list指向空闲块的第一个页描述符的lru。第一个页描述符的lru的prev指向free_list,next指向第二个空闲块的起始页的页描述符的lru的prev。这样就是用一个双向链表将所有大小为2^k的空闲内存块的起始页的页描述符联系了起来。
此外,一个2^k大小的空闲块的第一个页的页描述符,private字段存放了数值k。当这个内存块被释放的时候,就可以得到它的前后内存块的第一个页。根据他们是否空闲,就能实现伙伴系统的合并。
1.7.2:分配块
使用函数
static struct page *__rmqueue(struct zone *zone, unsigned int order) |
从管理区中分配一个空闲块。这个函数使用时,默认已经关中断,并且获得了内存管理区的锁。关中断是为了避免正常路径和中断路径上,分配内存的时候存在的可能的竞争条件。
/* if (!page && __rmqueue_fallback(zone, order, migratetype, //如果分配不出来,就先从migratetype的后备类型链表中,将部分页转移到指定的migratetype链表,然后retry |
我们首先看函数__rmqueue_smallest的实现
/* /* Find a page of the appropriate size in the preferred list */ return NULL; |
在这里分析一下函数expand。假设某一次high的值为8,low的值为2。也就是说,我们本来需要4个连续页框,但是没有这么小的连续页框了,只能从256个连续页框中分出4个页框出来。
/* //---------------------------------------------------------------------------------- low:2 high:8 area:对应了8的struct free_area结构体。也就是说,这个结构体链接的都是大小为1<<8个页框的空闲页框块 page:我们这次找到的,包含1<<8个页框的空闲页框块的第一个页框描述符 //---------------------------------------------------------------------------------- unsigned long size = 1 << high; while (high > low) { /* add_to_free_list(&page[size], zone, high, migratetype); //第一次循环的时候,我们将order=8的内存块分成了两个order=7的内存块,page[size]就是对应第二个内存块。将这个内存块放入free_area[high]->free_list[migratetype]链表中 |
上面描述的是当前migratetype中有满足要求的内存块的情况。当不满足时,可能从后备migratetype,也就是fallback的free_list中分配内存。fallbacks描述了每种migratetype的后备类型migrate
/* |
函数__rmqueue_fallback就是将后备type中的空闲内存块移动到需要的migratetype上。
/* /* /* /* goto do_steal; return false; find_smallest: /* do_steal: //如果找到了可以偷取的内存块,就将整个内存块移动到需要的migrate类型的free_list链表上 steal_suitable_fallback(zone, page, alloc_flags, start_migratetype, trace_mm_page_alloc_extfrag(page, order, current_order, return true; } |
1.7.3:释放块
使用函数__free_pages_ok释放块。
同样,假设我们要释放的是内存管理区ISA DMA(0~16M)中的页框块,是从8~10M的大小为2M的页框块,并且假设其他页框块都是空闲的。
static void __free_pages_ok(struct page *page, unsigned int order, if (!free_pages_prepare(page, order, true)) //这个函数中并没有做重要操作 migratetype = get_pfnblock_migratetype(page, pfn); |
static void free_one_page(struct zone *zone, struct page *page, unsigned long pfn, unsigned int order, int migratetype, fpi_t fpi_flags) { spin_lock(&zone->lock); if (unlikely(has_isolate_pageblock(zone) || is_migrate_isolate(migratetype))) { migratetype = get_pfnblock_migratetype(page, pfn); } __free_one_page(page, pfn, zone, order, migratetype, fpi_flags); spin_unlock(&zone->lock); } |
/* static inline void __free_one_page(struct page *page, max_order = min_t(unsigned int, MAX_ORDER - 1, pageblock_order); VM_BUG_ON(!zone_is_initialized(zone)); VM_BUG_ON(migratetype == -1); VM_BUG_ON_PAGE(pfn & ((1 << order) - 1), page); continue_merging: if (!pfn_valid_within(buddy_pfn)) max_order = order + 1; //我们将首页为page,大小为1<<order的内存块释放到伙伴系统中 done_merging: if (fpi_flags & FPI_TO_TAIL) if (to_tail) /* Notify page reporting subsystem of freed page */ |
1.8:每CPU页框高速缓存
内核经常请求或者释放单个页框。为此,每个内存管理区都定义了一个每CPU页框高速缓存。所有的每CPU高速缓存都包含一些预先分配的页框,用于处理CPU发出的单一内存请求。这样做的好处是,避免了从伙伴系统中分配的时候,需要多cpu之间进行同步的问题。也就是说,这和slub相似,每cpu页框缓存是伙伴系统的缓存。
还是要注意,每CPU高速缓存的页框是从伙伴系统中分出来的。
struct zone { …… struct per_cpu_pageset __percpu *pageset; //每个cpu都有一个pageset结构体 …… } |
struct per_cpu_pageset { struct per_cpu_pages pcp; …… #ifdef CONFIG_SMPs8 stat_threshold; s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS]; #endif }; |
struct per_cpu_pages { /* Lists of pages, one per migrate type stored on the pcp-lists */ |
总之,在系统运行过程中,每当分配1个page的时候,都会从每cpu页框缓存中分配。类似于slub,如果每cpu页框缓存是空的话,那么就先从伙伴系统中分batch个page给每cpu页框缓存。如果每cpu页框缓存多于high值,那么就会释放batch个页框到伙伴系统中。总的来说,每cpu页框缓存的思路和slub是一致的。
1.8.1:通过每CPU页框缓存分配页框
使用函数rmqueue_pcplist从每cpu页框缓存中分配页框。
/* Lock and remove page from the per-cpu list */ local_irq_save(flags); |
/* Remove page from the per-cpu list, caller must protect the list */ do { page = list_first_entry(list, struct page, lru); //有的话,我们就直接分配空闲页框出去 return page; |
分析内部函数rmqueue_bulk,我们观察它的参数,可知,他从内存管理区zone中,分batch次order=0的单个页框,将这些页框添加给每CPU高速缓存。这些页框在伙伴系统中看来,不是空闲的。但是在每cpu缓存中看来,他们是空闲的。
/* spin_lock(&zone->lock); if (unlikely(check_pcp_refill(page))) /* /* |
1.8.2:释放页框到每CPU页框高速缓存
函数free_unref_page用于将order=0的内存块释放到每cpu页框缓存。调用链为__free_pages->free_the_page->free_unref_page
/* if (!free_unref_page_prepare(page, pfn)) //这里同样不做关键操作 local_irq_save(flags); //关中断。同步是为了保护数据结构。中断和普通路径都可能走到这里,要避免他们之间产生竞争 |
static void free_unref_page_commit(struct page *page, unsigned long pfn) migratetype = get_pcppage_migratetype(page); /* pcp = &this_cpu_ptr(zone->pageset)->pcp; |
分析函数free_pcppages_bulk
/* * Frees a number of pages from the PCP lists * Assumes all pages on list are in same zone, and of same order. * count is the number of pages to free. * * If the zone was previously in an "all pages pinned" state then look to * see if this freeing clears that state. * * And clear the zone's pages_scanned counter, to hold off the "all pages are * pinned" detection logic. */ static void free_pcppages_bulk(struct zone *zone, int count, struct per_cpu_pages *pcp) { int migratetype = 0; int batch_free = 0; int prefetch_nr = 0; bool isolated_pageblocks; struct page *page, *tmp; LIST_HEAD(head); /* * Ensure proper count is passed which otherwise would stuck in the * below while (list_empty(list)) loop. */ count = min(pcp->count, count); while (count) { struct list_head *list; /* * Remove pages from lists in a round-robin fashion. A * batch_free count is maintained that is incremented when an * empty list is encountered. This is so more pages are freed * off fuller lists instead of spinning excessively around empty * lists */ do { batch_free++; if (++migratetype == MIGRATE_PCPTYPES) migratetype = 0; list = &pcp->lists[migratetype]; //这个循环再加上外面的大循环,能保证从不同的migratetype链表中都找到page释放 } while (list_empty(list)); /* This is the only non-empty list. Free them all. */ if (batch_free == MIGRATE_PCPTYPES) batch_free = count; do { page = list_last_entry(list, struct page, lru); /* must delete to avoid corrupting pcp list */ list_del(&page->lru); pcp->count--; if (bulkfree_pcp_prepare(page)) //此函数没有做有意义的操作 continue; list_add_tail(&page->lru, &head); //要释放的页链接在head链表上 /* * We are going to put the page back to the global * pool, prefetch its buddy to speed up later access * under zone->lock. It is believed the overhead of * an additional test and calculating buddy_pfn here * can be offset by reduced memory latency later. To * avoid excessive prefetching due to large count, only * prefetch buddy for the first pcp->batch nr of pages. */ if (prefetch_nr++ < pcp->batch) prefetch_buddy(page); } while (--count && --batch_free && !list_empty(list)); } spin_lock(&zone->lock); isolated_pageblocks = has_isolate_pageblock(zone); /* * Use safe version since after __free_one_page(), * page->lru.next will not point to original list. */ list_for_each_entry_safe(page, tmp, &head, lru) { int mt = get_pcppage_migratetype(page); …… __free_one_page(page, page_to_pfn(page), zone, 0, mt, FPI_NONE); //调用__free_one_page,将page释放到伙伴系统中,这个函数已经在1.7.3中描述过了 trace_mm_page_pcpu_drain(page, 0, mt); } spin_unlock(&zone->lock); }
/* /* /* This is the only non-empty list. Free them all. */ do { if (bulkfree_pcp_prepare(page)) //此函数没有做有意义的操作 list_add_tail(&page->lru, &head); //要释放的页链接在head链表上 /* spin_lock(&zone->lock); /* __free_one_page(page, page_to_pfn(page), zone, 0, mt, FPI_NONE); //调用__free_one_page,将page释放到伙伴系统中,这个函数已经在1.7.3中描述过了 |
1.9:管理区分配器
Linux5-10.110版本请参考第8章补第2节。
在上面的页框分配中,我们已经很好的分配了页框。但是,还有一个前提条件,就是,我们还需要找到一个合适的内存管理区,然后在这个内存管理区中分配页框。因此,这一部分就是如果找到合适的内存管理区。
管理区分配器要满足几个目标:1:他应当保护保留的页框池。2:当内存不足,并且允许阻塞当前进程的时候,他应该触发页框回收算法。一旦某些页框被释放,管理区分配器将再次尝试分配。3:如果可能,他应该保存珍贵的ZONE_DMA内存管理区。
对一组连续页框的每次请求实质上是通过执行alloc_pages宏来处理的。这个宏实际上调用了函数:
#define alloc_pages(gfp_mask, order) \ alloc_pages_node(numa_node_id(), gfp_mask, order) |
static inline struct page *alloc_pages_node(int nid, unsigned int gfp_mask, unsigned int order) { return __alloc_pages(gfp_mask, order, NODE_DATA(nid)->node_zonelists + (gfp_mask & GFP_ZONEMASK)); } |
struct page * fastcall __alloc_pages(unsigned int gfp_mask, unsigned int order, struct zonelist *zonelist) |
gfp_mask:内存分配的标志。这个标志能够表示从哪些内存管理区中分配,在分配过程中是否可以阻塞,分配不成功时的处理方式等 |
order:表示要分配2^order个页框 |
zonelist:指向zonelist的数据结构,该数据结构按照优先次序描述了适用于内存分配的内存管理区,他的数据结构如下 struct zone *zones[MAX_NUMNODES * MAX_NR_ZONES + 1]; 本次分配可以使用哪些内存管理区由分配参数gfp_mask决定 |
需要注意的是参数struct zonelist *zonelist,他表示了用于内存分配的内存管理区的优先次序。
首先要理解函数zone_watermark_ok。他的含义是,从zone中分配order幂的空闲页后,zone的空闲页依然高于水位要求。
bool zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, |
/* /* free_pages may go negative - that's OK */ if (alloc_flags & ALLOC_HIGH) if (unlikely(alloc_harder)) { //上面共同确定min的值 /* // 要求管理区剩下的页框数目必须大于min + z->lowmem_reserve[classzone_idx]。也就是虽然我们分配内存的时候,尽量避免从低级别的zone(也就是DMA/DMA32)中分配 /* If this is an order-0 request then the watermark is fine */ //并且要有一个连续内存区,能够满足分配需要。也就是z->free_area[o]不为空 /* For a high-order request, check at least one suitable page is free */ if (!area->nr_free) //每个area的free_list包含MIGRATE_TYPES中链表。只有在alloc_harder情况下,我们才能使用area->free_list[MIGRATE_HIGHATOMIC] for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) { ...... |
通过下面的函数__alloc_pages可以看到,只有当函数zone_watermark_ok返回1的时候,才能够分配页框。zone_watermark_ok会依据不同的参数确定一个最小值min。只有在以下情况,函数会返回1:
1:除了要分配的页框外,在内存管理区中至少还有min个空闲页框,并且这min个空闲页框不能包含为内存不足保留的页框(lowmem_reserve字段描述)。
2:必须是一段连续的空闲内存满足要求。
可以看到,上面的函数也是按照这样的要求编写的。
再看函数__alloc_pages,这个函数的逻辑就是,找到一个合适的内存管理区,然后在这个内存管理区中分配需要的页框。
struct page * fastcall __alloc_pages(unsigned int gfp_mask, unsigned int order, struct zonelist *zonelist) { const int wait = gfp_mask & __GFP_WAIT; // __GFP_WAIT:当前进程可以阻塞在这个函数中 struct zone **zones, *z; struct page *page; struct reclaim_state reclaim_state; struct task_struct *p = current; int i; int classzone_idx; int do_retry; int can_try_harder; int did_some_progress; might_sleep_if(wait); can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait; zones = zonelist->zones; //可以分配的内存管理区构成的数组的首地址 classzone_idx = zone_idx(zones[0]); //最希望使用classzone_idx这一管理区分配 restart: /* Go through the zonelist once, looking for a zone with enough free */ for (i = 0; (z = zones[i]) != NULL; i++) { //遍历zones内存管理区数组中,所有的内存管理区。这是这次分配可以使用的管理区 if (!zone_watermark_ok(z, order, z->pages_low, classzone_idx, 0, 0)) // pages_low回收页框使用的上界。这时第一次扫描,这时候can_try_harder和gfp_high被设置为0. continue; page = buffered_rmqueue(z, order, gfp_mask); //找到了合适的内存管理区,在这个管理区中,通过伙伴系统分配页框 if (page) goto got_pg; } for (i = 0; (z = zones[i]) != NULL; i++) //上面的操作没有分配出来页框,那么唤醒内核线程kswapd。这部分内容参见第十七章 wakeup_kswapd(z, order); for (i = 0; (z = zones[i]) != NULL; i++) { if (!zone_watermark_ok(z, order, z->pages_min, // pages_min保留页框的个数 classzone_idx, can_try_harder, gfp_mask & __GFP_HIGH)) //这时候使用了较低的阈值,最终反映到zone中要求保留的页框变少 continue; page = buffered_rmqueue(z, order, gfp_mask); if (page) goto got_pg; } //如果此次内核控制路径不是中断处理程序也不是可延迟函数,并且这个进程试图回收页框,那么这次会忽略内存不足的阈值,使用为内存不足预留的页框来进行分配 if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE))) && !in_interrupt()) { for (i = 0; (z = zones[i]) != NULL; i++) { page = buffered_rmqueue(z, order, gfp_mask); //直接分配页框,不需要为管理区保留。只有在这种情况下,才会使用保留的页框 if (page) goto got_pg; } goto nopage; } /* Atomic allocations - we can't balance anything */ if (!wait) //如果这个进程不能在这个函数中阻塞,那么打印错误信息后,返回空 goto nopage; rebalance: cond_resched(); //进程可以在这个函数中阻塞。如果没有分配到页框,那么这里检查是否需要调度。可能TIF_NEED_RESCHED已经被设置了,但是还没有到调度点,没有得到调度 p->flags |= PF_MEMALLOC; //设置PF_MEMALLOC,表示进程要进行内存回收 reclaim_state.reclaimed_slab = 0; p->reclaim_state = &reclaim_state; did_some_progress = try_to_free_pages(zones, gfp_mask, order); //紧急回收内存,具体内容参考第17章。注意,之前说过,设置了PF_MEMALLOC的进程,在请求页框的时候,会将内存保护区中的页框分配给这个进程。因此,这里是先设置进程的PF_MEMALLOC,然后让他回收页框 p->reclaim_state = NULL; p->flags &= ~PF_MEMALLOC; cond_resched(); if (likely(did_some_progress)) { //如果紧急回收页框回收到了一些页框,那么再尝试一次页框分配 for (i = 0; (z = zones[i]) != NULL; i++) { if (!zone_watermark_ok(z, order, z->pages_min, classzone_idx, can_try_harder, gfp_mask & __GFP_HIGH)) continue; page = buffered_rmqueue(z, order, gfp_mask); if (page) goto got_pg; } } else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) { //紧急回收页框不成功。再试一下 for (i = 0; (z = zones[i]) != NULL; i++) { if (!zone_watermark_ok(z, order, z->pages_high, classzone_idx, 0, 0)) continue; page = buffered_rmqueue(z, order, gfp_mask); if (page) goto got_pg; } out_of_memory(gfp_mask); //分不出来,进入OOM。杀死一个进程并释放内存,参考17章 goto restart; } 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) { blk_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(); } return NULL; got_pg: return page; } |
因此,做个总结。当使用函数alloc_pages的时候,首先去选择一个有足够空闲页框的内存管理区。然后在这个管理区中使用伙伴系统或者每CPU高速缓存(只需要一个页框的时候),分出需要的页框。
1.9.1:释放一组页框
释放页框的函数如下所示:
void free_pages(unsigned long addr, unsigned int order) { if (addr != 0) { VM_BUG_ON(!virt_addr_valid((void *)addr)); __free_pages(virt_to_page((void *)addr), order); } } |
void __free_pages(struct page *page, unsigned int order) { if (put_page_testzero(page)) //页框没有进程使用 free_the_page(page, order); else if (!PageHead(page)) while (order-- > 0) free_the_page(page + (1 << order), order); } |
static inline void free_the_page(struct page *page, unsigned int order) { if (order == 0) /* Via pcp? */ free_unref_page(page); //释放到每cpu缓存中 else __free_pages_ok(page, order, FPI_NONE); //释放到伙伴系统中 } |
释放到每cpu缓存和释放到伙伴系统中的函数我们都已经描述过了。
2:内存区管理
上面关注的都是按页框进行分配。但是,当我们请求小内存区的时候,如果按照页框进行分配,就会造成大量的浪费。
kmem_cache是Linux内核中用于分配小内存块的高速缓存机制。kmem_cache高速缓存通过预先分配一些内存块,并将其存储在一个特定大小的内存池中。这些与分配的内存块被组织成一个链表。当应用程序需要分配内存的时候,内核可以直接从这个链表中分配,而不是从通用的内存分配器中分配。同样,当内存块不再需要的时候,他们也不会立即释放,而是被放回高速缓存中备用。
kmem_cache的主要优点如下:
1:减少内存分配开销:直接从预分配的内存块中获取内存可以避免再每次分配的时候调用通用内存分配器,减少了分配操作的开销。
2:降低内存碎片:由于内存块被预先分配,并保持在池中,因此内存碎片变少了。
3:提高性能:避免了内存的频繁分配和释放。
2.1:slab分配器
slab分配器把对象,也就是一种数据结构,放入高速缓存kmem_cache。这个高速缓存就可以看做是这一类对象的专用储备。每个高速缓存都被划分成多个slab。每个slab由一个或者多个连续的页框组成,其中包含了已分配的对象(数据结构),也包含空闲的对象(数据结构)。
从上图中能够看出来,一个kmem_cache包含多个slab,一个slab包含多个页框,里面存放了分配的对象。
2.2:高速缓存描述符
每个高速缓存都通过kmem_cache_s结构体描述。所有的高速缓存通过双链表连接在一起,生成了高速缓存链表cache_chain。高速缓存描述符kmem_cache_t的内容如下:
struct kmem_cache_s { /* 1) per-cpu data, touched during every alloc/free */ struct array_cache *array[NR_CPUS]; unsigned int batchcount; unsigned int limit; /* 2) touched by every alloc & free from the backend */ struct kmem_list3 lists; //用于连接这个高速缓存中的slab描述符 unsigned int objsize; //高速缓存中每个对象的大小 unsigned int flags; /* constant flags */ unsigned int num; //一个slab中的对象的数目 unsigned int free_limit; /* upper limit of objects in the lists */ spinlock_t spinlock; /* 3) cache_grow/shrink */ /* order of pgs per slab (2^n) */ unsigned int gfporder; //一个单独的slab中包含的连续页框数目的对数 /* force GFP flags, e.g. GFP_DMA */ unsigned int gfpflags; size_t colour; /* cache colouring range */ unsigned int colour_off; //高速缓存中,slab结构体在slab页框中的偏移 unsigned int colour_next; /* cache colouring */ kmem_cache_t *slabp_cache; //一个kmem_cache指针。当slab结构体从外部分配的时候,slab结构体从这个kmem_cache中分配。当然,如果slab结构体从内部分配,这个值为NULL unsigned int slab_size; unsigned int dflags; /* dynamic flags */ /* 4) cache creation/removal */ const char *name; //高速缓存名字 struct list_head next; //用于连接下一个高速缓存描述符 }; |
其中,lists成员较为关键,他指示了高速缓存中已经用完的自己所有空闲对象的slab链表slabs_full,用了部分,还剩有部分空闲对象的slab链表slabs_partial,完全没使用的slab链表slabs_free。他的成员如下:
struct kmem_list3 { struct list_head slabs_partial; //指向部分使用的slab描述符 struct list_head slabs_full; //指向完全使用的slab描述符 struct list_head slabs_free; //指向完全没使用的 slab描述符 unsigned long free_objects; int free_touched; unsigned long next_reap; struct array_cache *shared; }; |
2.3:slab描述符
高速缓存中的每个slab都有自己的描述符
struct slab { struct list_head list; //用于连接高速缓存描述符的kmem_list3中的链表字段 unsigned long colouroff; //slab中第一个对象的偏移 void *s_mem; //slab中的第一个对象 unsigned int inuse; /* num of objs active in slab */ kmem_bufctl_t free; //slab中下一个空闲对象的下标 }; |
每个高速缓存的第一个slab的list的prev指向高速缓存描述符,next指向下一个与之相似的slab的slab的list.prev。slab描述符可以存在slab外部或者内部。
2.4:普通和专用高速缓存
kmem高速缓存分成两类,普通高速缓存和专用高速缓存。普通高速缓存是由slab分配器用于自己目的的高速缓存;内核其他组件使用的高速缓存是专用高速缓存;例如task结构体构成的高速缓存。普通高速缓存包含以下两种
1:cache_cache:高速缓存结构体kmem_cache自己作为一个对象,也需要高速缓存以及slab系统来存储他。存储存储高速缓存描述符的高速缓存是cache_cache。
static kmem_cache_t cache_cache = { .lists = LIST3_INIT(cache_cache.lists), //三个slab双向链表 .batchcount = 1, .limit = BOOT_CPUCACHE_ENTRIES, .objsize = sizeof(kmem_cache_t), //高速缓存存储的每个成员的大小(这里也可以看到,这个高速缓存是存储高速缓存对象的高速缓存) .flags = SLAB_NO_REAP, .spinlock = SPIN_LOCK_UNLOCKED, .name = "kmem_cache", }; |
由于他是存储高速缓存描述符的高速缓存,可想而知,他也是系统中第一个高速缓存。因此,在高速缓存描述符构成的链表cache_chain中,他也是第一个元素。
static struct list_head cache_chain; |
void __init kmem_cache_init(void) { …… list_add(&cache_cache.next, &cache_chain); //第一个被加入链表的元素 …… } |
2:另外还有一些高速缓存,这种高速缓存包含的内存大小分别是32字节,64字节……2M。由于每种大小的高速缓存都有两个,分别适用于ISA DMA分配和常规分配。所以总共有26个高速缓存,因此也对应了26个高速缓存描述符。
专用高速缓存是由kmem_cache_create创建的。在创建一个新的高速缓存的时候,会将新创建的高速缓存描述符加入到kmem_cache。并且还需要将高速缓存描述符加入cache_chain链表中。
接口kmem_cache_destory用于删除一个高速缓存,并且将高速缓存描述符从cache_chain链表上删除。要注意的是,必须在删除高速缓存之前,删除他所有的slab。
我们以文件系统中使用到的一个例子,来研究kmem_cache_create的行为。
示例: sizeof(struct radix_tree_node), 0, SLAB_PANIC, radix_tree_node_ctor, NULL); |
/** * kmem_cache_create - Create a cache. * @name: A string which is used in /proc/slabinfo to identify this cache. //name为radix_tree_node。我们可以通过/proc/slabinfo来观察这个kmem_cache * @size: The size of objects to be created in this cache. //kmem_cache中存储的是struct radix_tree_node结构体 * @align: The required alignment for the objects. * @flags: SLAB flags * @ctor: A constructor for the objects. //给slab分配新的页的时候,需要做的操作 * @dtor: A destructor for the objects. * //不能在中断中使用这个函数 * Returns a ptr to the cache on success, NULL on failure. * Cannot be called within a int, but can be interrupted. * The @ctor is run when new pages are allocated by the cache * and the @dtor is run before the pages are handed back. * * @name must be valid until the cache is destroyed. This implies that * the module calling this has to destroy the cache before getting * unloaded. * * The flags are * * %SLAB_POISON - Poison the slab with a known test pattern (a5a5a5a5) * to catch references to uninitialised memory. * * %SLAB_RED_ZONE - Insert `Red' zones around the allocated memory to check * for buffer overruns. * * %SLAB_NO_REAP - Don't automatically reap this cache when we're under * memory pressure. * * %SLAB_HWCACHE_ALIGN - Align the objects in this cache to a hardware * cacheline. This can be beneficial if you're counting cycles as closely * as davem. */ kmem_cache_t * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*, kmem_cache_t *, unsigned long), void (*dtor)(void*, kmem_cache_t *, unsigned long)) { size_t left_over, slab_size, ralign; kmem_cache_t *cachep = NULL; /* Check that size is in terms of words. This is needed to avoid * unaligned accesses for some archs when redzoning is used, and makes * sure any on-slab bufctl's are also correctly aligned. */ //---------------------------------------------------------------------------- 如果我们有一个值size,还有一个对齐值align。我们可以按照下面的方法,让size按照align向上或者向下对齐 向上对齐:size = (size + align - 1) & ~(align - 1); 向下对齐:size = size & ~(align - 1); //---------------------------------------------------------------------------- if (size & (BYTES_PER_WORD-1)) { size += (BYTES_PER_WORD-1); size &= ~(BYTES_PER_WORD-1); } /* calculate out the final buffer alignment: */ /* 1) arch recommendation: can be overridden for debug */ if (flags & SLAB_HWCACHE_ALIGN) { //SLAB_HWCACHE_ALIGN表示是否需要硬件缓存对齐 /* Default alignment: as specified by the arch code. * Except if an object is really small, then squeeze multiple * objects into one cacheline. */ ralign = cache_line_size(); while (size <= ralign/2) ralign /= 2; } else { ralign = BYTES_PER_WORD; } /* 2) arch mandated alignment: disables debug if necessary */ if (ralign < ARCH_SLAB_MINALIGN) { //ARCH_SLAB_MINALIGN表示架构要求的最小对齐值 ralign = ARCH_SLAB_MINALIGN; //--------------------------------------------------------------------------------- 如果ralign的值大于一个字,那么,这时候需要删除SLAB_RED_ZONE和SLAB_STORE_USER。 SLAB_RED_ZONE:这个标志用于在分配的内存块两侧插入Red区域,用于检测内存泄漏。 SLAB_STORE_USER:这个标志用于在缓存的对象中存储一些用户特定的信息 //--------------------------------------------------------------------------------- if (ralign > BYTES_PER_WORD) flags &= ~(SLAB_RED_ZONE|SLAB_STORE_USER); } /* 3) caller mandated alignment: disables debug if necessary */ if (ralign < align) { ralign = align; if (ralign > BYTES_PER_WORD) flags &= ~(SLAB_RED_ZONE|SLAB_STORE_USER); } /* 4) Store it. Note that the debug code below can reduce * the alignment to BYTES_PER_WORD. */ align = ralign; /* Get cache's description obj. */ //在这里创建了新的kmem_cache结构体。kmem_cache也是通过cache_cache这个高速缓存分配出来的 cachep = (kmem_cache_t *) kmem_cache_alloc(&cache_cache, SLAB_KERNEL); if (!cachep) goto opps; memset(cachep, 0, sizeof(kmem_cache_t)); /* Determine if the slab management is 'on' or 'off' slab. */ if (size >= (PAGE_SIZE>>3)) /* * Size is large, assume best to place the slab management obj * off-slab (should allow better packing of objs). */ flags |= CFLGS_OFF_SLAB; //CFLGS_OFF_SLAB:将slab结构体放在分配给slab存放对象的页框外部 size = ALIGN(size, align); if ((flags & SLAB_RECLAIM_ACCOUNT) && size <= PAGE_SIZE) { //-------------------------------------------------------------------------------------- SLAB_RECLAIM_ACCOUNT:表示分配给这个kmem_cache的页是可回收的。在内核内存紧张的时候,可以回收这个页面。 在这种时候,会将cachep->gfporder设置为0。这个值代表了,给这个kmem_cache的slab,从伙伴系统中分配页面的时候,每次分配1页 //-------------------------------------------------------------------------------------- /* * A VFS-reclaimable slab tends to have most allocations * as GFP_NOFS and we really don't want to have to be allocating * higher-order pages when we are unable to shrink dcache. */ cachep->gfporder = 0; cache_estimate(cachep->gfporder, size, align, flags, &left_over, &cachep->num); //-------------------------------------------------------------------------------------- //cache_estimate是一个相当重要的函数。他的参数解释如下: cachep->gfporder:入参,指定了一个slab占用了多少页 size:入参,指定了slab中每个对象的字节大小 align:入参,指定了每个对象应该按照什么样的方式对齐 flags:入参,控制了slab的分配方式 left_over:出参,表示了一个slab占有的页中,分配了对象之后,还剩下的内存字节数 cachep->num:出参,表示了一个slab占有的页可以分配的对象的数目 //-------------------------------------------------------------------------------------- } else { /* * Calculate size (in pages) of slabs, and the num of objs per * slab. This could be made much more intelligent. For now, * try to avoid using high page-orders for slabs. When the * gfp() funcs are more friendly towards high-order requests, * this should be changed. */ do { unsigned int break_flag = 0; cal_wastage: cache_estimate(cachep->gfporder, size, align, flags, &left_over, &cachep->num); if (break_flag) break; if (cachep->gfporder >= MAX_GFP_ORDER) break; if (!cachep->num) goto next; if (flags & CFLGS_OFF_SLAB && cachep->num > offslab_limit) { /* This num of objs will cause problems. */ cachep->gfporder--; break_flag++; goto cal_wastage; } /* * Large num of objs is good, but v. large slabs are * currently bad for the gfp()s. */ if (cachep->gfporder >= slab_break_gfp_order) break; if ((left_over*8) <= (PAGE_SIZE<<cachep->gfporder)) break; /* Acceptable internal fragmentation. */ next: cachep->gfporder++; } while (1); } if (!cachep->num) { //这个值就是通过cache_estimate函数计算出来的,一个slab中,能够存储对象的个数 printk("kmem_cache_create: couldn't create cache %s.\n", name); kmem_cache_free(&cache_cache, cachep); cachep = NULL; goto opps; } //slab_size就是slab结构体需要的字节大小。这里注意,不仅需要sizeof(struct slab),还需要cachep->num*sizeof(kmem_bufctl_t)。后者会在slab分配的时候使用 slab_size = ALIGN(cachep->num*sizeof(kmem_bufctl_t) + sizeof(struct slab), align); /* * If the slab has been placed off-slab, and we have enough space then * move it on-slab. This is at the expense of any extra colouring. */ //如果我们指定了CFLGS_OFF_SLAB,但是给slab分配的页中,有足够的空间来存放slab结构体。这时候,我们清楚CFLGS_OFF_SLAB标志。也就是说,这时候我们将slab结构体放在slab页中 if (flags & CFLGS_OFF_SLAB && left_over >= slab_size) { flags &= ~CFLGS_OFF_SLAB; left_over -= slab_size; } if (flags & CFLGS_OFF_SLAB) { /* really off slab. No need for manual alignment *///真的将slab放在外面。这时候不需要手动对齐 slab_size = cachep->num*sizeof(kmem_bufctl_t)+sizeof(struct slab); } cachep->colour_off = cache_line_size(); /* Offset must be a multiple of the alignment. */ if (cachep->colour_off < align) cachep->colour_off = align; //cachep->colour_off 用于着色。描述了每个着色的字节偏移 cachep->colour = left_over/cachep->colour_off; //left_over是所有剩下的内存,cachep->colour_off是每个着色的字节偏移。因此,cachep->colour描述了总共有多少个颜色 cachep->slab_size = slab_size; //每个slab结构体的大小。之后,创建新的slab的时候,不需要再通过函数cache_estimate进行计算 cachep->flags = flags; //描述了slab结构体在页面内还是页面外等信息 cachep->gfpflags = 0; if (flags & SLAB_CACHE_DMA) cachep->gfpflags |= GFP_DMA; spin_lock_init(&cachep->spinlock); cachep->objsize = size; //cachep->objsize的值就是slab中存放的每个对象的字节大小 /* NUMA */ INIT_LIST_HEAD(&cachep->lists.slabs_full); INIT_LIST_HEAD(&cachep->lists.slabs_partial); INIT_LIST_HEAD(&cachep->lists.slabs_free); if (flags & CFLGS_OFF_SLAB) cachep->slabp_cache = kmem_find_general_cachep(slab_size,0); //--------------------------------------------------- 还记得之前我们说的吗,通用高速缓存由slab系统内部使用。这里,根据slab_size去malloc_sizes这一全局数组中,寻找一个合适的通用高速缓存。 struct cache_sizes malloc_sizes[] = { #define CACHE(x) { .cs_size = (x) }, #include <linux/kmalloc_sizes.h> { 0, } #undef CACHE }; 这种代码已经在其他地方看过了。是一种比较方便的定义全局数组的方式。 函数kmem_find_general_cachep使用的时候,根据slab_size找到一个合适的struct cache_sizes结构体。然后,根据(gfpflags & GFP_DMA) ? csizep->cs_dmacachep : csizep->cs_cachep,选择一个合适的高速缓存 //--------------------------------------------------- cachep->ctor = ctor; cachep->dtor = dtor; cachep->name = name; if (g_cpucache_up == FULL) { enable_cpucache(cachep); //这部分内容,参考2.9节:空闲slab对象的本地高速缓存 } else { …… } cachep->lists.next_reap = jiffies + REAPTIMEOUT_LIST3 + ((unsigned long)cachep)%REAPTIMEOUT_LIST3; /* Need the semaphore to access the chain. */ //-------------------------------------------------------------- 在上面的过程中,我们已经完成了kmem_cache的创建。现在,我们要将这个新的高速缓存插入高速缓存链表中 //-------------------------------------------------------------- down(&cache_chain_sem); { struct list_head *p; mm_segment_t old_fs; old_fs = get_fs(); set_fs(KERNEL_DS); list_for_each(p, &cache_chain) { kmem_cache_t *pc = list_entry(p, kmem_cache_t, next); char tmp; /* This happens when the module gets unloaded and doesn't destroy its slab cache and noone else reuses the vmalloc area of the module. Print a warning. *///不懂 if (__get_user(tmp,pc->name)) { printk("SLAB: cache with size %d has lost its name\n", pc->objsize); continue; } if (!strcmp(pc->name,name)) { printk("kmem_cache_create: duplicate cache %s\n",name); up(&cache_chain_sem); unlock_cpu_hotplug(); BUG(); } } set_fs(old_fs); } /* cache setup completed, link it into the list */ list_add(&cachep->next, &cache_chain); up(&cache_chain_sem); opps: if (!cachep && (flags & SLAB_PANIC)) //SLAB_PANIC:没完成创建就panic panic("kmem_cache_create(): failed to create slab `%s'\n", name); return cachep; } |
我们观察函数cache_estimate的实现:
/* Cal the num objs, wastage, and bytes left over for a given slab size. */ static void cache_estimate (unsigned long gfporder, size_t size, size_t align, int flags, size_t *left_over, unsigned int *num) { int i; size_t wastage = PAGE_SIZE<<gfporder; size_t extra = 0; size_t base = 0; if (!(flags & CFLGS_OFF_SLAB)) { base = sizeof(struct slab); extra = sizeof(kmem_bufctl_t); } i = 0; while (i*size + ALIGN(base+i*extra, align) <= wastage) i++; if (i > 0) i--; if (i > SLAB_LIMIT) i = SLAB_LIMIT; *num = i; wastage -= i*size; wastage -= ALIGN(base+i*extra, align); *left_over = wastage; } |
上面的逻辑很清晰,就是获得了,指定了slab的页面数量后,一个slab中,能够容纳的对象的数量。
2.5:给高速缓存分配slab,以及删除一个slab
在上面的步骤中,我们创建了一个新的高速缓存kmem_cache。不过,这时候,我们还没有给这个kmem_cache创建slab。
当我们向一个高速缓存发出一个分配新对象的请求,并且高速缓存不包含任何空闲对象的时候,才会给高速缓存分配slab。通过下面的函数给一个kmem_cache创建一个新的slab。
static int cache_grow (kmem_cache_t * cachep, int flags, int nodeid) //对于一致性内存模型,nodeid的值为-1 //flags是分配对象时指定的flags { struct slab *slabp; void *objp; size_t offset; int local_flags; unsigned long ctor_flags; if (flags & SLAB_NO_GROW) return 0; ctor_flags = SLAB_CTOR_CONSTRUCTOR; local_flags = (flags & SLAB_LEVEL_MASK); if (!(local_flags & __GFP_WAIT)) ctor_flags |= SLAB_CTOR_ATOMIC; /* About to mess with non-constant members - lock. */ check_irq_off(); spin_lock(&cachep->spinlock); /* Get colour for the slab, and cal the next value. */ offset = cachep->colour_next; //这次创建的slab使用的颜色 cachep->colour_next++; if (cachep->colour_next >= cachep->colour) cachep->colour_next = 0; offset *= cachep->colour_off; //colour_off是每个颜色使用的字节大小 spin_unlock(&cachep->spinlock); if (local_flags & __GFP_WAIT) local_irq_enable(); /* Get mem for the objs. */ if (!(objp = kmem_getpages(cachep, flags, nodeid))) //此处通过伙伴系统完成页框分配,返回分配的页框块的第一个页框的线性地址。具体分析见下一小节 goto failed; /* Get slab management. */ if (!(slabp = alloc_slabmgmt(cachep, objp, offset, local_flags))) //在动态内存中分配slabp结构体。这里会根据cachep的属性,判断是在另一个专门存放slab描述符的高速缓存中分配,还是就在slabp的第一个页框中分配。 goto opps1; set_slab_attr(cachep, slabp, objp); //------------------------------------------------------------------------------ 刚刚分配了以objp为首的1<<order个页框。这个函数将这些页框的页框描述符中的lru字段置位 page->lru.next = cachep page->lru.prev = slabp 这样的目的是,通过页框描述符,可以快速找到它属于的高速缓存和slab。 当一个页框是空闲的时候,page->lru用于链接伙伴系统中,具有相同order的页框块 //------------------------------------------------------------------------------ cache_init_objs(cachep, slabp, ctor_flags); //我们已经说过,slab结构体后面,紧挨着cachep->num个kmem_bufctl_t数据。这个函数的作用就是,将这个数组中的元素,分别赋值为1,2……,BUFCTL_END。然后将slabp->free设置为0。 kmem_bufctl_t用于表示下一个空闲的对象块 if (local_flags & __GFP_WAIT) local_irq_disable(); check_irq_off(); spin_lock(&cachep->spinlock); /* Make slab active. */ list_add_tail(&slabp->list, &(list3_data(cachep)->slabs_free)); //将新分配的slab加入到高速缓存cachep的slabs_free链表中 list3_data(cachep)->free_objects += cachep->num; spin_unlock(&cachep->spinlock); return 1; opps1: kmem_freepages(cachep, objp); failed: if (local_flags & __GFP_WAIT) local_irq_disable(); return 0; } |
里面的函数alloc_slabmgmt需要分析。可以告诉我们什么叫做将slab结构体放在slab拥有的页框中,还是放在专门用于存放slab的高速缓存中。
static struct slab* alloc_slabmgmt (kmem_cache_t *cachep, void *objp, int colour_off, int local_flags) { struct slab *slabp;
if (OFF_SLAB(cachep)) { //外部slab描述符 /* Slab management obj is off-slab. */ slabp = kmem_cache_alloc(cachep->slabp_cache, local_flags); //如果slab在外部,那么,从cachep->slabp_cache这一通用高速缓存中分出slabp描述符 if (!slabp) return NULL; } else { //内部slab描述符 slabp = objp+colour_off; // objp是分给这个slab的连续页框的首地址,offset是slab描述符的偏移(用于slab着色) colour_off += cachep->slab_size; } slabp->inuse = 0; slabp->colouroff = colour_off; slabp->s_mem = objp+colour_off; //s_mem是slab中存放的第一个对象的线性地址 return slabp; } |
再次强调,当我们通过kmem_cache分配一个新的结构体,然后这个kmem_cache没有空闲对象的位置的时候,才会给这个kmem_cache创建新的slab。也就是说,函数cache_grow的调用关系如下:
void * kmem_cache_alloc (kmem_cache_t *cachep, int flags):适用于专用高速缓存 |
static inline void * __cache_alloc (kmem_cache_t *cachep, int flags) |
static void* cache_alloc_refill(kmem_cache_t* cachep, int flags) |
static int cache_grow (kmem_cache_t * cachep, int flags, int nodeid) |
使用函数slab_destroy删除一个slab。
2.6:slab分配器与分区页框分配器的接口
刚刚已经介绍了,如何给一个高速缓存中创建新的slab。这里介绍如何创建一个新的slab。创建新的slab的时候,我们首先创建了slab结构体,此外,还需要从伙伴系统中,分出一部分页框,供给这个slab使用。
必须使用页框分配器(也就是之前所说的,先通过内存区管理区确定内存管理区,再在此管理区中分配页框的函数)来获得一组连续的空闲页框。使用下面的函数完成新创建的slab的页框分配。
static void *kmem_getpages(kmem_cache_t *cachep, int flags, int nodeid) { struct page *page; void *addr; int i; flags |= cachep->gfpflags; if (likely(nodeid == -1)) { page = alloc_pages(flags, cachep->gfporder); //这里就是前面讲的页框分配器,通过伙伴系统,获得空闲页框,gfporder决定了一个slab包含的页框的数目 } else { page = alloc_pages_node(nodeid, flags, cachep->gfporder); } if (!page) return NULL; addr = page_address(page); //如果不是高端内存,那么就返回页框对应的虚拟地址;如果是高端内存,就返回页框的页框描述符的虚拟地址 i = (1 << cachep->gfporder); if (cachep->flags & SLAB_RECLAIM_ACCOUNT) atomic_add(i, &slab_reclaim_pages); //slab_reclaim_pages表示,当内核遇到很大的内存压力的情况下,可以释放的页的数目 add_page_state(nr_slab, i); while (i--) { SetPageSlab(page); //set_bit(PG_slab, &(page)->flags)。表示这个页用于slab page++; } return addr; } |
完成页框分配。它会返回分配的页框的线性地址。
使用函数kmem_freepages,将slab的页框返还给伙伴系统
static void kmem_freepages(kmem_cache_t *cachep, void *addr) //参数是高速缓存描述符而不是slab描述符。这是因为slab占有多少个页框是在高速缓存描述符中确定的 { unsigned long i = (1<<cachep->gfporder); struct page *page = virt_to_page(addr); const unsigned long nr_freed = i; while (i--) { if (!TestClearPageSlab(page)) //要求&(page)->flags的PG_slab必须被设置,也就是这个页属于slab BUG(); page++; } sub_page_state(nr_slab, nr_freed); if (current->reclaim_state) current->reclaim_state->reclaimed_slab += nr_freed; free_pages((unsigned long)addr, cachep->gfporder); //将页框释放到伙伴系统中 if (cachep->flags & SLAB_RECLAIM_ACCOUNT) atomic_sub(1<<cachep->gfporder, &slab_reclaim_pages); } |
完成slab的页框释放。
可见,伙伴系统实际上是页框分配的基础。不论是伙伴系统,每CPU高速缓存,还是kmem_cache和slab,最终都是建立在伙伴系统之上的。
2.7:对象描述符
存放在slab中的每个对象都有一个对象描述符
typedef unsigned short kmem_bufctl_t; |
从创建一个高速缓存的时候也知道,每个slab描述符后面就是跟着num个kmem_bufctl_t,num就是一个slab中包含的对象的个数。因此,kmem_bufctl_t和结构体是一一对应的关系。kmem_bufctl_t包含的是下一个空闲对象在slab中的下标。
从这个图中,我们可以看到这个字段是如何使用的。slab结构体中的free字段,表示了这个slab中第一个空闲的对象。同样,这个空闲对象对应了一个kmem_bufctl_t描述符,这个描述符中,存放的是下一个空闲的对象的下标。
2.8:slab着色
着色主要用于处理下面的问题:由于不同的内存块可能映射到相同的缓存行之上,在同一个kmem_cache的不同slab内,有相同偏移量的对象很可能映射到同一高速缓存行中。这样就可能发生cache的抖动。
之前讲过,一个高速缓存中一般有没有用完的字节。使用这些字节,让同一cache中不同的slab的对象结构体存放首地址不同。
例如,从下图中看,就是不同的颜色col,导致每个对象的位置不同。aln是每个颜色的偏移。
2.9:空闲slab对象的本地高速缓存
为了减少处理器对自旋锁的竞争,并且更好的利用硬件高速缓存,kmem_cache结构体中包含了一个被称作slab本地高速缓存的每CPU数据结构。
struct kmem_cache_s { /* 1) per-cpu data, touched during every alloc/free */ struct array_cache *array[NR_CPUS]; unsigned int batchcount; //本地高速缓存中,slab对象过多或者过少的时候,从kmem_cache的slab中拿走或者放回的slab对象的个数 unsigned int limit; //本地高速缓存中slab对象的最大数目 …… }; |
要理解这个成员的作用,就要清晰的了解我们从slab中分配对象或者释放对象到slab的过程。如上面所说,为了减少处理器对自旋锁的竞争,我们不是每次都从kmem_cache包含的所有slab中,分配一个对象或者释放一个对象。我们的操作是,先将一些slab中的对象分给一个本地高速缓存,然后,在分配或者释放对象的时候,从属于这个CPU的对象此中分配。
每个struct array_cache成员如下所示:
struct array_cache { unsigned int avail; unsigned int limit; unsigned int batchcount; unsigned int touched; }; |
这里有部分内容没有暴露出来。实际上,struct array_cache *array[NR_CPUS]并不是NR_CPUS个struct array_cache指针,它还包含了limit个void *指针,每个指针就是一个属于这个本地高速缓存的slab中的对象的地址。
kmem_cache_t * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*, kmem_cache_t *, unsigned long), void (*dtor)(void*, kmem_cache_t *, unsigned long)) { …… if (g_cpucache_up == FULL) { enable_cpucache(cachep); //通过这个函数使能cpucache } …… } |
static void enable_cpucache (kmem_cache_t *cachep) { int err; int limit, shared; /* The head array serves three purposes: * - create a LIFO ordering, i.e. return objects that are cache-warm * - reduce the number of spinlock operations. * - reduce the number of linked list operations on the slab and * bufctl chains: array operations are cheaper. * The numbers are guessed, we should auto-tune as described by * Bonwick. */ if (cachep->objsize > 131072) limit = 1; else if (cachep->objsize > PAGE_SIZE) limit = 8; else if (cachep->objsize > 1024) limit = 24; else if (cachep->objsize > 256) limit = 54; else limit = 120; /* Cpu bound tasks (e.g. network routing) can exhibit cpu bound * allocation behaviour: Most allocs on one cpu, most free operations * on another cpu. For these cases, an efficient object passing between * cpus is necessary. This is provided by a shared array. The array * replaces Bonwick's magazine layer. * On uniprocessor, it's functionally equivalent (but less efficient) * to a larger limit. Thus disabled by default. */ shared = 0; #ifdef CONFIG_SMP if (cachep->objsize <= PAGE_SIZE) shared = 8; #endif err = do_tune_cpucache(cachep, limit, (limit+1)/2, shared); //我们会使用这里的limit的值,设置array_cache中的limit的值 if (err) printk(KERN_ERR "enable_cpucache failed for %s, error %d.\n", cachep->name, -err); } |
static int do_tune_cpucache (kmem_cache_t* cachep, int limit, int batchcount, int shared) { struct ccupdate_struct new; struct array_cache *new_shared; int i; memset(&new.new,0,sizeof(new.new)); for (i = 0; i < NR_CPUS; i++) { if (cpu_online(i)) { //---------------------------------------------------------- //new.new[i]就是struct array_cache *new[i]。这里就是给这个指针分配内存。在这个函数中我们可以看到,分配的内存大小是sizeof(void*)*limit+sizeof(struct array_cache) //---------------------------------------------------------- new.new[i] = alloc_arraycache(i, limit, batchcount); if (!new.new[i]) { for (i--; i >= 0; i--) kfree(new.new[i]); return -ENOMEM; } } else { new.new[i] = NULL; } } new.cachep = cachep; //------------------------------------------- 这个函数先暂时不详细说明。反正就是,对所有的CPU都执行do_ccupdate_local函数。可以认为,它实现了下面的效果: 将新创建的struct array_cache *的值赋给struct kmem_cache_s->struct array_cache *array[NR_CPUS]。然后将struct kmem_cache_s->struct array_cache *的原有值赋给new.new。 //------------------------------------------- smp_call_function_all_cpus(do_ccupdate_local, (void *)&new); check_irq_on(); spin_lock_irq(&cachep->spinlock); cachep->batchcount = batchcount; cachep->limit = limit; cachep->free_limit = (1+num_online_cpus())*cachep->batchcount + cachep->num; spin_unlock_irq(&cachep->spinlock); for (i = 0; i < NR_CPUS; i++) { struct array_cache *ccold = new.new[i]; if (!ccold) continue; spin_lock_irq(&cachep->spinlock); //------------------------------------------- 在这里, ccold->avail的值为0,所以,这里不会释放任何块 //------------------------------------------- free_block(cachep, ac_entry(ccold), ccold->avail); spin_unlock_irq(&cachep->spinlock); kfree(ccold); } new_shared = alloc_arraycache(-1, batchcount*shared, 0xbaadf00d); if (new_shared) { struct array_cache *old; spin_lock_irq(&cachep->spinlock); old = cachep->lists.shared; cachep->lists.shared = new_shared; if (old) free_block(cachep, ac_entry(old), old->avail); spin_unlock_irq(&cachep->spinlock); kfree(old); } return 0; } |
因此,通过上面的函数,我们可以知道,他在struct kmem_cache_s中,建立了一个指针数组,每个指针都指向了上面的图所示的一段内存。不过,这时候这段内存中,void *区域还没有初始化,也就是说,这时候的cpucache中,还没有分配具体的slab对象。我们会在后面,将对象分配给一个CPU,然后,将这个对象的地址填入void *区域中。
2.10:分配slab对象
我们已经在上面描述了很多内容。实际上,他们都是被分配slab对象的函数调用的。例如,分配slab对象的时候,从cpucache中分配。这时候,就会给上面刚创建的,还是空状态的void *指针赋值,也就是将slab对象分配给cpucache。又或者,slab中已经没有空闲的对象。这时候,我们就要通过cache_grow,创建新的slab对象。我们将在这一节,串联起之前描述的所有内容。
通过函数kmem_cache_alloc()获得新对象,代码如下所示:
void * kmem_cache_alloc (kmem_cache_t *cachep, int flags) { return __cache_alloc(cachep, flags); } |
static inline void * __cache_alloc (kmem_cache_t *cachep, int flags) { unsigned long save_flags; void* objp; struct array_cache *ac; local_irq_save(save_flags); //--------------------------------------------------------------------------------- 我们已经说过,在分配slab对象的时候,为了降低处理器对自旋锁的竞争,以及更好的利用高速缓存,我们并不是从一个slab的所有对象池中分配的。我们是先将一些slab中的对象分配给cpucache。然后,每次从这个cpu的cpucache中分配对象 //--------------------------------------------------------------------------------- ac = ac_data(cachep); //获取属于此cpu的struct array_cache。里面包含了limit个void *指针,每个指针都指向了一个slab对象 if (likely(ac->avail)) { ac->touched = 1; objp = ac_entry(ac)[--ac->avail]; //从后往前分配 } else { objp = cache_alloc_refill(cachep, flags); //第一次在这个cpu上获取这种slab对象的时候,会走到这里。函数do_tune_cpucache在初始化array_cache的时候,将ac->avail设置为0 } local_irq_restore(save_flags); return objp; } |
因此,我们主要关注,在第一次在此cpu上,malloc这种类型的slab对象的时候,我们做了什么。按照之前的分析,我们应该将slab对象分配给这个cpu,也就是说,填充这个cpu的array_cache
static void* cache_alloc_refill(kmem_cache_t* cachep, int flags) { int batchcount; struct kmem_list3 *l3; struct array_cache *ac; check_irq_off(); ac = ac_data(cachep); //在这里,获取了此CPU使用的array_cache retry: batchcount = ac->batchcount; // batchcount:每次分配给此cpu的array_cache的slab对象的数目 if (!ac->touched && batchcount > BATCHREFILL_LIMIT) { //如果这个高速缓存被使用过,那么touched标志被设置为1 batchcount = BATCHREFILL_LIMIT; } l3 = list3_data(cachep); spin_lock(&cachep->spinlock); if (l3->shared) { struct array_cache *shared_array = l3->shared; if (shared_array->avail) { if (batchcount > shared_array->avail) batchcount = shared_array->avail; shared_array->avail -= batchcount; ac->avail = batchcount; memcpy(ac_entry(ac), &ac_entry(shared_array)[shared_array->avail], sizeof(void*)*batchcount); shared_array->touched = 1; goto alloc_done; } } while (batchcount > 0) { struct list_head *entry; struct slab *slabp; entry = l3->slabs_partial.next; //slabs_partial链接的是,部分对象被使用的slab if (entry == &l3->slabs_partial) { //考虑到链表结构,这个条件为真的话,说明slabs_partial中已经slab结构体了。也就是说,现在没有slab,他的对象是部分使用的 l3->free_touched = 1; entry = l3->slabs_free.next; //因此,我们到slabs_free链表中,找完全未使用的slab结构体 if (entry == &l3->slabs_free) goto must_grow; //slabs_free链表为空。也就是说,这时候也没有一个slab结构体,它的对象一个未使用。这时候,我们要创建新的slab } slabp = list_entry(entry, struct slab, list); //这里找到了一个slab结构体,他又未使用的对象 check_slabp(cachep, slabp); check_spinlock_acquired(cachep); while (slabp->inuse < cachep->num && batchcount--) { kmem_bufctl_t next; ac_entry(ac)[ac->avail++]=slabp->s_mem+ slabp->free * cachep->objsize; // s_mem:slab中第一个对象的地址。free:slab中空闲对象的下标。因此,ac_entry(ac)[ac->avail++]就是一个空闲对象的地址 slabp->inuse++; next = slab_bufctl(slabp)[slabp->free]; //这里访问了kmem_bufctl_t数组。回忆一下,slab中有多少个对象,kmem_bufctl_t数组就有多少个成员。每个成员都是一个下标,表示了这个成员的下一个空闲成员的下标。具体可以看2.7节对象描述符
slabp->free = next; } check_slabp(cachep, slabp); /* move slabp to correct slabp list: */ list_del(&slabp->list); if (slabp->free == BUFCTL_END) //分配完成后,还需要决定slab的去向。如果这个slab没有空闲对象了,就将他添加到slabs_full链表中。如果还有空闲对象,就加到slabs_partial链表中 list_add(&slabp->list, &l3->slabs_full); else list_add(&slabp->list, &l3->slabs_partial); } must_grow: l3->free_objects -= ac->avail; alloc_done: spin_unlock(&cachep->spinlock); if (unlikely(!ac->avail)) { int x; x = cache_grow(cachep, flags, -1);
// cache_grow can reenable interrupts, then ac could change. ac = ac_data(cachep); if (!x && ac->avail == 0) // no objects in sight? abort return NULL; if (!ac->avail) // objects refilled by interrupt? goto retry; } ac->touched = 1; return ac_entry(ac)[--ac->avail]; } |
因此,我们看到,通过函数cache_alloc_refill的作用,我们给array_cache分配了空闲的slab对象。这时候,array_cache->avail表示了array_cache中,void *数组的下标。我们是先分配array_cache中后面的空闲slab对象。
2.11:释放slab对象
上面讲述了如何分配slab对象的。现在我们描述如何释放slab对象。释放slab对象的代码如下:
void kmem_cache_free (kmem_cache_t *cachep, void *objp) { unsigned long flags; local_irq_save(flags); __cache_free(cachep, objp); local_irq_restore(flags); } |
static inline void __cache_free (kmem_cache_t *cachep, void* objp) { struct array_cache *ac = ac_data(cachep); check_irq_off(); if (likely(ac->avail < ac->limit)) { ac_entry(ac)[ac->avail++] = objp; return; } else { cache_flusharray(cachep, ac); ac_entry(ac)[ac->avail++] = objp; } } |
因此,我们这里要画图,说明分配和释放的过程。假设,我们给array_cache中,分配了8个slab对象。在某一次分配的时候,avail的值为3,也就是说,这时候,此cpu的array_cache中,还有3个空闲的对象。需要注意的是,这里存放的都是对象的指针:
然后,假设某一次释放的时候,我们先释放了*object5描述的对象。这时候,按照释放的逻辑,它会变成下面这样:
也就是说,这时候avail对应的是object5的指针。也不需要担心我们失去了object3的指针信息。它保存在我们分配的一个对象中。
如果本地高速缓存中,没有位置来存放空闲对象的指针,就进入函数
static void cache_flusharray (kmem_cache_t* cachep, struct array_cache *ac) { int batchcount; batchcount = ac->batchcount; check_irq_off(); spin_lock(&cachep->spinlock); if (cachep->lists.shared) { //先不考虑这种情况 struct array_cache *shared_array = cachep->lists.shared; int max = shared_array->limit-shared_array->avail; if (max) { if (batchcount > max) batchcount = max; memcpy(&ac_entry(shared_array)[shared_array->avail], &ac_entry(ac)[0], sizeof(void*)*batchcount); shared_array->avail += batchcount; goto free_done; } } free_block(cachep, &ac_entry(ac)[0], batchcount); free_done: spin_unlock(&cachep->spinlock); ac->avail -= batchcount; memmove(&ac_entry(ac)[0], &ac_entry(ac)[batchcount], sizeof(void*)*ac->avail); } |
上面函数的关键操作都在free_block中完成;
static void free_block(kmem_cache_t *cachep, void **objpp, int nr_objects) { int i; check_spinlock_acquired(cachep); cachep->lists.free_objects += nr_objects; for (i = 0; i < nr_objects; i++) { void *objp = objpp[i]; struct slab *slabp; unsigned int objnr; slabp = GET_PAGE_SLAB(virt_to_page(objp)); //struct page->lru.prev指向了这个页属于的slab结构体 list_del(&slabp->list); objnr = (objp - slabp->s_mem) / cachep->objsize; //这里计算出,这次释放的对象是这个slab中的第几个对象 check_slabp(cachep, slabp); slab_bufctl(slabp)[objnr] = slabp->free; //-------------------------------------------------------------------- 我们说过,free字段存放的是,这个slab中,第一个空闲对象的下标。这里我们设置了(kmem_bufctl_t *)(slabp+1)[objnr]的值为free slabp+1就是地址增加了sizeof(struct slab)。因此,我们拿到了kmem_bufctl_t数组的首地址。如前面所说,这个数组成员和这个slab包含的对象一一对应,表示下一个空闲对象的下标。这时候,我们设置了kmem_bufctl_t[objnr]=free,也就是说,设置了之前的第一个空闲对象,作为我们这次释放对象的下一个空闲对象 //-------------------------------------------------------------------- slabp->free = objnr; //现在slab中的第一个空闲对象,就是我们释放的对象 slabp->inuse--; check_slabp(cachep, slabp); /* fixup slab chains */ if (slabp->inuse == 0) { //如果slab没有一个对象被使用 if (cachep->lists.free_objects > cachep->free_limit) { cachep->lists.free_objects -= cachep->num; slab_destroy(cachep, slabp); //这种情况下删除slab } else { list_add(&slabp->list,&list3_data_ptr(cachep, objp)->slabs_free); } } else { list_add_tail(&slabp->list,&list3_data_ptr(cachep, objp)->slabs_partial); //将slab加入部分使用,部分为使用的链表中 } } } |
2.12:通用对象
如果是分配一些普通的结构体,就使用普通高速缓存来处理。
static inline void *kmalloc(size_t size, int flags) { return __kmalloc(size, flags); } |
void * __kmalloc (size_t size, int flags) { struct cache_sizes *csizep = malloc_sizes; //------------------------------------------------------------------------ malloc_sizes是一个全局数组,struct cache_sizes malloc_sizes的每个成员如下所示: struct cache_sizes { size_t cs_size; //对应不同的大小。每次从普通kmem_cache中分配一个对象的时候,要先找到满足条件的struct cache_sizes kmem_cache_t *cs_cachep; kmem_cache_t *cs_dmacachep; }; //------------------------------------------------------------------------ for (; csizep->cs_size; csizep++) { if (size > csizep->cs_size) //要满足的条件就是,分配的size<=csizep->cs_size continue; return __cache_alloc(flags & GFP_DMA ? csizep->cs_dmacachep : csizep->cs_cachep, flags); //还是调用函数__cache_alloc进行内存分配,但是不同的是,这时候指定的高速缓存是cs_dmacachep或者cs_cachep } return NULL; } |
2.13:内存池
内存池和kmem_cache具有相似之处,都是专门申请的,用于某种功能的内存。它主要用于在内存不足的情况下,给某种类型的内存分配提供一个预留分配内存。描述内存池的结构体如下所示:
typedef struct mempool_s { void *pool_data; |
通过函数mempool_create创建一个内存池。
/** * mempool_create - create a memory pool * @min_nr: the minimum number of elements guaranteed to be * allocated for this pool. * @alloc_fn: user-defined element-allocation function. * @free_fn: user-defined element-freeing function. * @pool_data: optional private data available to the user-defined functions. * * this function creates and allocates a guaranteed size, preallocated * memory pool. The pool can be used from the mempool_alloc() and mempool_free() * functions. This function might sleep. Both the alloc_fn() and the free_fn() * functions might sleep - as long as the mempool_alloc() function is not called * from IRQ contexts. * * Return: pointer to the created memory pool object or %NULL on error. */ mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data) { return mempool_create_node(min_nr,alloc_fn,free_fn, pool_data, GFP_KERNEL, NUMA_NO_NODE); } |
mempool_t *mempool_create_node(int min_nr, mempool_alloc_t *alloc_fn, pool = kzalloc_node(sizeof(*pool), gfp_mask, node_id); //分配mempool_t结构体 if (mempool_init_node(pool, min_nr, alloc_fn, free_fn, pool_data, return pool; |
int mempool_init_node(mempool_t *pool, int min_nr, mempool_alloc_t *alloc_fn, pool->elements = kmalloc_array_node(min_nr, sizeof(void *), //elements指针数组包含了mempool中所有的element地址 /* element = pool->alloc(gfp_mask, pool->pool_data); //其实就是kmalloc,kmem_cache_alloc,alloc_pages这三个函数,将内存预分配出来 return 0; |
当要从mempool中分配内存的时候,使用接口mempool_alloc。这个接口会首先利用普通方式分配内存。如果分不出来,再使用mempool中预留的内存。
/** VM_WARN_ON_ONCE(gfp_mask & __GFP_ZERO); gfp_mask |= __GFP_NOMEMALLOC; /* don't allocate emergency reserves */ gfp_temp = gfp_mask & ~(__GFP_DIRECT_RECLAIM|__GFP_IO); repeat_alloc: element = pool->alloc(gfp_temp, pool->pool_data); //通过普通的内存分配方式获取内存 spin_lock_irqsave(&pool->lock, flags); /* /* We must not sleep if !__GFP_DIRECT_RECLAIM */ /* Let's wait for someone else to return an element to @pool */ spin_unlock_irqrestore(&pool->lock, flags); /* finish_wait(&pool->wait, &wait); |
3:非连续内存区管理
将内存区映射到一组连续的页框是最好的选择,这样能够充分利用高速缓存并获得较低的访问时间。但是,如果对内存区的请求不是很频繁。那么,通过连续的线性地址来访问非连续的页框也会很有意义。这样的主要优点是避免外碎片,缺点是要打乱内核页表。
3.1:非连续内存区的线性地址
这张图可以看到内核的线性地址布局。物理内存映射部分是对0~896M内存进行映射的线性地址(也就是说线性地址是3G~3G+896M)。FIXADDR_START~4G是包含固定映射的线性地址(固定映射的线性地址),PKMAP_BASE~FIXADDR_START用于高端内存页框的永久内核映射(高端内存页框的内核映射)。
最终,剩下的,从VMALLOC_START开始,到VMALLOC_END结束的线性地址用于非连续内存区映射(也就是vmalloc使用)。这个区域的大小可以参考linux/Documentation/x86/x86_64/mm.rst。
3.2:非连续内存区的描述符
每个非连续内存区都对应着一个类型为vm_struct的描述符。
struct vm_struct { void *addr; //这个内存区的虚拟地址首地址 unsigned long size; //内存区大小+4096(安全区大小) unsigned long flags; //表示了非连续区映射的内存的类型,VM_ALLOC表示使用vmalloc得到的线性区;VM_MAP表示使用vmap映射已经分配的页框;VM_IOREMAP表示使用ioremap映射的硬件设备的板上内存 struct page **pages; //指向了这个内存区对应的所有的物理页页框组成的数组 unsigned int nr_pages; unsigned long phys_addr; struct vm_struct *next; }; |
struct vm_struct *vmlist; |
不同的vm_struct结构体通过next连接成一个链表。第一个vm_struct结构体地址存放在全局变量vmlist中。
3.2.1:Linux-2.6.11
通过函数get_vm_area,查找一个空闲的线性地址(当然,线性地址是位于VMALLOC_START和VMALLOC_END之间的)。一定要注意的是,struct vm_struct是用于表示内核的非连续内存映射,struct vm_area_struct是用于表示进程的线性区。
/** * get_vm_area - reserve a contingous kernel virtual area * * @size: size of the area * @flags: %VM_IOREMAP for I/O mappings or VM_ALLOC * * Search an area of @size in the kernel virtual mapping area, * and reserved it for out purposes. Returns the area descriptor * on success or %NULL on failure. */ { return __get_vm_area(size, flags, VMALLOC_START, VMALLOC_END); } |
struct vm_struct *__get_vm_area(unsigned long size, unsigned long flags, unsigned long start, unsigned long end) { struct vm_struct **p, *tmp, *area; unsigned long align = 1; unsigned long addr; if (flags & VM_IOREMAP) { // VM_IOREMAP:使用ioremap()映射的硬件设备的板上内存。VM_ALLOC:使用vmalloc()得到的页。VM_MAP:表示使用vmap()映射的已经被分配的页。 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); area = kmalloc(sizeof(*area), GFP_KERNEL); //使用slab,从内核内存区域中malloc出来一个结构体 if (unlikely(!area)) return NULL; /* * We always allocate a guard page. */ size += PAGE_SIZE; //在分配的时候,额外多分配4K作为保护页 write_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) goto found; addr = ALIGN(tmp->size + (unsigned long)tmp->addr, align); if (addr > end - size) goto out; } //这段逻辑可以在纸上画图得到,反正最终结果,就是获得了一个满足我们大小要求的,位于VMALLOC_START~VMALLOC_END之间的线性区 found: area->next = *p; *p = area; //这两句组成将一个元素插入链表的操作 area->flags = flags; area->addr = (void *)addr; //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; } |
现在,我们有了一段足够长的空闲的线性地址了,接下来,我们还要找一段足够长的,空闲的物理页框(不连续的),将他们和这段线性地址对应起来。
3.2.2:Linux-5.10.110
使用函数__get_vm_area_node来找到一段合适的内核虚拟地址。
//start和end还是使用宏VMALLOC_START和VMALLOC_END来表示 BUG_ON(in_interrupt()); //不能在中断环境中使用vmalloc if (flags & VM_IOREMAP) area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node); //分配一个struct vm_struct结构体 if (!(flags & VM_NO_GUARD)) va = alloc_vmap_area(size, align, start, end, node, gfp_mask); //分配一个struct vmap_struct结构体。在给进程分配线性区的时候,我们使用了一个红黑树来表示进程拥有的线性区。这里也是一样,通过一个红黑树,描述内核使用的非连续线性区。这个函数找到一段足够的虚拟内存 kasan_unpoison_vmalloc((void *)va->va_start, requested_size); setup_vmalloc_vm(area, va, flags, caller); return area; |
因此,我们分析函数alloc_vmap_area
/* BUG_ON(!size); if (unlikely(!vmap_initialized)) might_sleep(); va = kmem_cache_alloc_node(vmap_area_cachep, gfp_mask, node);//从kmem_cache中分配struct vmap_area结构体内存 /* retry: if (!this_cpu_read(ne_fit_preload_node)) spin_lock(&free_vmap_area_lock); if (pva && __this_cpu_cmpxchg(ne_fit_preload_node, NULL, pva)) //如果this_cpu_read(ne_fit_preload_node)为空,那么就额外分配一个struct vmap_area,存放在这个每cpu变量中 /* if (unlikely(addr == vend)) va->va_start = addr; //将va_start设置为我们找到的虚拟地址
BUG_ON(!IS_ALIGNED(va->va_start, align)); ret = kasan_populate_vmalloc(addr, size); return va; overflow: if (gfpflags_allow_blocking(gfp_mask)) { if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) kmem_cache_free(vmap_area_cachep, va); |
函数 __alloc_vmap_area的作用是找到一段满足我们条件的虚拟地址空间。内核将从VMALLOC_START和VMALLOC_END的所有空闲虚拟地址段组织成一个红黑树free_vmap_area_root。因此,这个函数遍历红黑树free_vmap_area_root,找到一个合适的虚拟地址空间。并且,如果被分割的虚拟地址空间有空余的话,还要讲剩余的虚拟地址空间对应的struct vmap_area在插入红黑树中。
3.3:分配非连续内存区
使用函数vmalloc函数,分配一个虚拟地址连续,但是虚拟地址对应的物理地址不连续的非连续内存区,这个函数最终返回连续的虚拟地址的首地址。提示,kmalloc的作用是在普通slab系统中分出一个结构体的内存。
3.3.1:Linux-2.6.11
Linux-2.6.11中,vmalloc的实现如下:
void *vmalloc(unsigned long size) { return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL); } |
void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot) { struct vm_struct *area; struct page **pages; unsigned int nr_pages, array_size, i; size = PAGE_ALIGN(size); if (!size || (size >> PAGE_SHIFT) > num_physpages) return NULL; area = get_vm_area(size, VM_ALLOC); //如上所述,这里获得一个连续的线性地址空间的首地址,VM_ALLOC表示,是函数__vmalloc调用的get_vm_area if (!area) return NULL; //------------------------------------------------------------------------------- 在这里有一个点需要注意。在get_vm_area中,我们分配的虚拟地址空间的size增加了一个page,但是我们在这里使用的size,是没有增加page的。因此,后面在建立页表的时候,虚拟地址空间的最后一个page实际上没有建立映射。因此,访问最后一个page会出错。也就是说,我们的保护页是通过这样的方式实现的 //------------------------------------------------------------------------------- nr_pages = size >> PAGE_SHIFT; array_size = (nr_pages * sizeof(struct page *)); area->nr_pages = nr_pages; /* Please note that the recursion is strictly bounded. */ if (array_size > PAGE_SIZE) pages = __vmalloc(array_size, gfp_mask, PAGE_KERNEL); else pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM)); //先默认函数是走下面的流程,这里通过kmalloc,从伙伴系统以及slab中获得pages数组。pages包含了nr_pages个page指针。最终,会将每个page指针赋值为,此次分配出来的空闲页框的页框描述符指针。 area->pages = pages; //area->pages是一个成员类型为struct page *的数组。也就是说,这个数组中每个成员都是一个页描述符的地址。它描述了area对应的物理页框,也表示了area对应的物理地址 if (!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; } memset(area->pages, 0, array_size); for (i = 0; i < area->nr_pages; i++) { area->pages[i] = alloc_page(gfp_mask); //这里给area分配页框 if (unlikely(!area->pages[i])) { /* Successfully allocated i pages, free them in __vunmap() */ area->nr_pages = i; goto fail; } }
if (map_vm_area(area, prot, &pages)) //在这里对分配的页框和分配的线性地址做映射 goto fail; return area->addr; fail: vfree(area->addr); return NULL; } |
int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages) //已经获得了连续的虚拟地址空间,以及非连续的物理页框(代表了物理地址),现在将他们映射起来 { unsigned long address = (unsigned long) area->addr; unsigned long end = address + (area->size-PAGE_SIZE); //这里减去PAGE_SIZE的原因是,area->size中包含了一个保护页 unsigned long next; pgd_t *pgd; int err = 0; int i; //------------------------------------------------------------------------------ 在这里补充i386的架构信息。在这种架构中,如果使用的是3级页表,那么虚拟地址总共分成4部分 [31~30]:一级页表指数:pgd_index。从4项一级页表中选择一项 [29~21]:二级页表指数:pmd_index。从512项二级页表中选择一项 [20~12]:三级页表指数:pte_index。从512项三级页表中选择一项 [11~0]:偏移量offset //------------------------------------------------------------------------------ pgd = pgd_offset_k(address); //内核页表是init_mm,这里就是获得((&init_mm)->pgd+pgd_index(address)),获得这个address对应的一级页表项。按照i386的架构信息,pgd_index就是拿到address的前两位 spin_lock(&init_mm.page_table_lock); for (i = pgd_index(address); i <= pgd_index(end-1); i++) { pud_t *pud = pud_alloc(&init_mm, pgd, address); //如果不是4级页表的话,我们没有pud(页上级目录),这时候,pud=pgd。 if (!pud) { err = -ENOMEM; break; } next = (address + PGDIR_SIZE) & PGDIR_MASK; if (next < address || next > end) next = end; if (map_area_pud(pud, address, next, prot, pages)) { //在二级页表中建立映射关系 err = -ENOMEM; break; } address = next; pgd++; } spin_unlock(&init_mm.page_table_lock); flush_cache_vmap((unsigned long) area->addr, end); return err; } |
static int map_area_pud(pud_t *pud, unsigned long address, unsigned long end, pgprot_t prot, struct page ***pages) { do { pmd_t *pmd = pmd_alloc(&init_mm, pud, address); //这里实际上是返回((pmd_t *)(*(pud)) + pmd_index(address))/我们已经说过,pud=pgd,因此,*(pud)就是二级页表的首地址,pmd_index(address)实际上获得了address的[29~21]位。因此,这时候的pmd(页中级目录)就是address对应的二级页表项的地址 //------------------------------------------------------------------- 在这里再补充一个信息。对于i386而言,他的三级页表,也就是pte,总是4K对齐的(也就是说,每个三级页表都占据一页的内存空间)。因此,*pmd指向了三级页表的地址,只有前20位有效。因此,我们可以使用后12位存储额外的信息,例如,这个pmd二级页表项是否有效等。 //------------------------------------------------------------------- if (!pmd) return -ENOMEM; if (map_area_pmd(pmd, address, end - address, prot, pages)) //在这个函数中,首先判断pmd有效,也就是它是否指向了一个三级页表(也就是这个三级页表以及被分配出来了)。如果有效的话,通过address找到对应的三级页表项,通过接口set_pte(pte, mk_pte(page, prot))设置这个三级页表项 return -ENOMEM; address = (address + PUD_SIZE) & PUD_MASK; pud++; } while (address && address < end); return 0; } |
static int map_area_pmd(pmd_t *pmd, unsigned long address, unsigned long size, pgprot_t prot, struct page ***pages) { unsigned long base, end; base = address & PUD_MASK; //PUD_MASK的值为2M address &= ~PUD_MASK; end = address + size; if (end > PUD_SIZE) end = PUD_SIZE; do { pte_t * pte = pte_alloc_kernel(&init_mm, pmd, base + address); //这个函数的功能是,通过pmd的后12位包含的额外信息(_PAGE_PRESENT位是否被设置),判断这个pmd是否对应了一个有效的三级页表。如果没有有效的三级页表,需要从内存中分一个页用作三级页表。最终,返回base + 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); return 0; } |
static int map_area_pte(pte_t *pte, unsigned long address, unsigned long size, pgprot_t prot, struct page ***pages) //在这个函数中,pte表示了要更新的三级页表项的地址,address是要映射的虚拟地址,size是要映射的大小,prot是映射权限,page表示了要映射的物理地址 { unsigned long end; address &= ~PMD_MASK; end = address + size; if (end > PMD_SIZE) end = PMD_SIZE; do { struct page *page = **pages; WARN_ON(!pte_none(*pte)); if (!page) return -ENOMEM; set_pte(pte, mk_pte(page, prot)); //设置三级页表项 address += PAGE_SIZE; pte++; (*pages)++; } while (address < end); return 0; } |
注意,一个进程进入内核态后,或者内核线程,通过函数map_vm_area建立非连续内存区的映射的时候,并不会修改当前用户进程的页表,而是修改的主内核页表。当用户进程进入内核态的时候,可能访问属于内核态的线性地址空间。这时候,他才有可能访问到非连续映射的内存区。
这时,缺页处理程序起作用了。他会检查这个缺页地址是否在内核页表中。如果他在,就把这个页表项拷贝到用户进程的页表项中,然后恢复执行。
一定要知道,用户进程的页表,和内核进程的页表,在3~4G的线性地址空间上,使用的是同一份表。pgd都是指向同样的内存区域。
一些情况下,我们已经有了包含空闲页框的页框描述符数组,这时候我们只需要给他分配连续的线性地址即可。这种情况下,使用函数vmap来映射页框。
3.3.2:Linux-5.10.110
Linux-5.10.110中,vmalloc的实现如下:
/** * vmalloc - allocate virtually contiguous memory * @size: allocation size * * Allocate enough pages to cover @size from the page level * allocator and map them into contiguous kernel virtual space. * * For tight control over page level allocator and protection flags * use __vmalloc() instead. * * Return: pointer to the allocated memory or %NULL on error */ void *vmalloc(unsigned long size) { return __vmalloc_node(size, 1, GFP_KERNEL, NUMA_NO_NODE, __builtin_return_address(0)); } |
/** * __vmalloc_node - allocate virtually contiguous memory * @size: allocation size * @align: desired alignment * @gfp_mask: flags for the page level allocator * @node: node to use for allocation or NUMA_NO_NODE * @caller: caller's return address * * Allocate enough pages to cover @size from the page level allocator with * @gfp_mask flags. Map them into contiguous kernel virtual space. * * Reclaim modifiers in @gfp_mask - __GFP_NORETRY, __GFP_RETRY_MAYFAIL * and __GFP_NOFAIL are not supported * * Any use of gfp flags outside of GFP_KERNEL should be consulted * with mm people. * * Return: pointer to the allocated memory or %NULL on error */ void *__vmalloc_node(unsigned long size, unsigned long align, gfp_t gfp_mask, int node, const void *caller) { return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END, gfp_mask, PAGE_KERNEL, 0, node, caller); } |
size = PAGE_ALIGN(size); area = __get_vm_area_node(real_size, align, VM_ALLOC | VM_UNINITIALIZED | addr = __vmalloc_area_node(area, gfp_mask, prot, node); //在这里完成了物理页面的分配,已经映射关系的建立 /* kmemleak_vmalloc(area, size, gfp_mask); return addr; fail: |
gfp_mask |= __GFP_NOWARN; /* Please note that the recursion is strictly bounded. */ if (!pages) { area->pages = pages; for (i = 0; i < area->nr_pages; i++) { if (node == NUMA_NO_NODE) //我们分配物理页,将所有分配的物理页指针放入数组page中 if (map_kernel_range((unsigned long)area->addr, get_vm_area_size(area), //我们在这个函数中,将连续的虚拟地址和非连续的物理地址之间做映射 return area->addr; fail: |
3.4:释放非连续内存区
使用函数vfree释放vmalloc()创建的非连续内存区。
void vfree(const void *addr) { BUG_ON(in_nmi()); kmemleak_free(addr); might_sleep_if(!in_interrupt()); if (!addr) return; __vfree(addr); } |
static void __vfree(const void *addr) { if (unlikely(in_interrupt())) __vfree_deferred(addr); else __vunmap(addr, 1); } |
static void __vunmap(const void *addr, int deallocate_pages) if (!addr) if (WARN(!PAGE_ALIGNED(addr), "Trying to vfree() bad address (%p)\n", area = find_vm_area(addr); //在红黑树vmap_area_root中找addr对应的vmap_area,返回vmap_area->vm_struct vm_remove_mappings(area, deallocate_pages); //调用了vm_remove_mappings,删除了addr对应的线性区,并且清除了内核页表中,这个线性区对应的映射关系。这个函数在下面展开解析 if (deallocate_pages) { //这个变量为1,表示还需要释放物理内存到伙伴系统中 for (i = 0; i < area->nr_pages; i++) { BUG_ON(!page); kvfree(area->pages); //释放数组占用的页 kfree(area); //释放线性区结构体占用的空间 //可以看到,这里并没有释放vmap_area结构体。这个结构体会在要释放的vmap_area足够多的时候,才会释放 |
/** might_sleep(); spin_lock(&vmap_area_lock); va->vm = NULL; kasan_free_shadow(vm); return vm; spin_unlock(&vmap_area_lock); |
函数free_unmap_vmap_area的实现如下所示
static void free_unmap_vmap_area(struct vmap_area *va) { flush_cache_vunmap(va->va_start, va->va_end); unmap_kernel_range_noflush(va->va_start, va->va_end - va->va_start); //在这个函数中,解除内核页表中相应地址的映射 if (debug_pagealloc_enabled_static()) flush_tlb_kernel_range(va->va_start, va->va_end); free_vmap_area_noflush(va); //将vmap_area从使用中的红黑树vmap_area_root中摘除,但是添加到vmap_purge_list链表中。只有vmap_purge_list链表中的vmap_area占据的虚拟地址空间足够大的时候,才会将这些虚拟地址空间释放到空闲的红黑树free_vmap_area_root中 } |
这部分的内容和建立映射关系刚好是反过来的。通过address找到三级页表项pte,然后设置pte无效。