8.1:页框管理
8.1.1:页描述符
页框,也就是物理内存上的4K或者其它大小的物理页。内核必须记录每个页框的当前状态。这些状态信息保存在一个page结构体中,这个结构体叫做页描述符。
struct page { page_flags_t flags; //每个二进制位都描述了一个页的状态 atomic_t _count; //页的引用计数器,如果这个值为-1,表示这个页框空闲;count+1表示这个页框的使用者数目 atomic_t _mapcount; unsigned long private; //伙伴系统中,一个大小为2^n的空闲页框块的第一个页描述符的private存储值n。当一个空闲页框块释放的时候,这个值存储n,会以此判断相邻大小为2^n的页框块是否空闲。如果空闲,就合并空闲页框块 struct address_space *mapping; pgoff_t index; struct list_head lru; //伙伴系统中,一个大小为2^n的空闲页框块的第一个页描述符的lru链接着free_area中的链表头。当然,如果这个页不是空闲页框的第一个页,这个成员存储的内容表示其他含义。 #if defined(WANT_PAGE_VIRTUAL) void *virtual; #endif /* WANT_PAGE_VIRTUAL */ }; |
所有的页描述符存放在mem_map数组中。
struct page *mem_map; |
8.1.2:非一致性内存访问
非一致性内存访问模型:在这种模型下,给定CPU对不同的内存单元的访问用时可能不同。系统的物理内存被划分成几个物理节点,同时,每个节点又被分成数个管理区。在一个节点内,任意给定CPU访问页面需要的时间都是相等的。在x86系统,IBM兼容PC中,所有的物理内存被组织成一个节点,然后使用链表头链接起来。
struct pglist_data *pgdat_list; |
节点的数据结构如下:
typedef struct pglist_data { struct zone node_zones[MAX_NR_ZONES]; //节点中管理区描述符数组 struct zonelist node_zonelists[GFP_ZONETYPES]; //用于表示分配页框时首选的管理区 int nr_zones; struct page *node_mem_map; struct bootmem_data *bdata; unsigned long node_start_pfn; unsigned long node_present_pages; /* total number of physical pages */ unsigned long node_spanned_pages; /* total size of physical page range, including holes */ int node_id; struct pglist_data *pgdat_next; wait_queue_head_t kswapd_wait; struct task_struct *kswapd; int kswapd_max_order; } pg_data_t; |
8.1.3:内存管理区
每个内存节点被分成三个内存管理区。在x86结构中,分别是:
ZONE_DMA:包含低于16M的页框;
ZONE_NORMAL:包含高于16M,低于896M的页框;
ZONE_HIGHMEM:包含高于896M的页框;
ZONE_DMA 的划分原因是,ISA总线的直接内存存取(DMA)处理器只能对内存的前16M进行寻址。ZONE_DMA和ZONE_NORMAL这些物理页框直接映射到线性地址空间的第4个GB,可以被内核直接使用。但是,ZONE_HIGHMEM不能由内核直接访问,事实上,高于896M的物理内存必须使用高端内存页框的内核映射方法进行映射。
每个内存管理区都有自己的描述符,见结构体zone:
struct zone { /* Fields commonly accessed by the page allocator */ unsigned long free_pages; unsigned long pages_min, pages_low, pages_high; // pages_min存储了管理区中保留页框的数目,pages_low和pages_high分别是回收页框使用的下界或者上界,也在管理区分配器中作为阈值使用 unsigned long lowmem_reserve[MAX_NR_ZONES]; //在内存不足的情况下,每个管理区必须保留的页框数目 struct per_cpu_pageset pageset[NR_CPUS]; spinlock_t lock; struct free_area free_area[MAX_ORDER]; ZONE_PADDING(_pad1_) spinlock_t lru_lock; struct list_head active_list; struct list_head inactive_list; unsigned long nr_scan_active; unsigned long nr_scan_inactive; unsigned long nr_active; unsigned long nr_inactive; unsigned long pages_scanned; /* since last reclaim */ int all_unreclaimable; /* All pages pinned */ int temp_priority; int prev_priority; ZONE_PADDING(_pad2_) wait_queue_head_t * wait_table; unsigned long wait_table_size; unsigned long wait_table_bits; struct pglist_data *zone_pgdat; struct page *zone_mem_map; unsigned long zone_start_pfn; unsigned long spanned_pages; /* total size, including holes */ unsigned long present_pages; /* amount of memory (excluding holes) */ char *name; } ____cacheline_maxaligned_in_smp; |
此外,还需要建立页描述符到内存节点和管理区的链接。在这里不直接使用指针的方式,目的是为了节省空间。而是将相应的内存节点和管理区编码后放在page结构体的flags字段中。
8.1.4:保留的页框池
在请求内存的时候,如果有足够的空闲内存,请求就会立即满足;否则的话,将发出请求的内核控制路径阻塞,直到有内存被释放。
但是在某些请求中,无法阻塞内存控制路径(例如在中断或者在临界区代码中)。这时候页框分配会失败并且返回。但是,我们希望这种情况尽量少发生,尽量保证有内存可以分配。因此,我们引入保留的页框池。使用参数min_free_kbytes表示:
int min_free_kbytes = 1024; //1024/4=256,因此总共有25个页框用作保留的页框池 |
ZONE_DMA和ZONE_NORMAL将他们的一部分页框用于保留的页框池,只有在不能阻塞的内核控制路径中,会被使用到。注意,保留的页框池将会全部被直接映射到内核线性地址空间中,因为只有在内核控制路径上,进行页框分配的时候会使用到这个页框池。
在管理区描述符的page_min字段,就表示了管理区内保留页框的数目。
8.1.5:分区页框分配器
被称为分区页框分配器的内核子系统用处理连续页框分配请求。
在请求分配的时候,分配器搜索一个包含能够满足要求的连续页框的管理区。在每个管理区中,使用伙伴系统处理页框。还有一小部分页框保留在高速缓存中,用于快速满足对单个页框的分配请求。
8.1.5.1:请求和释放页框
使用多个函数分配或者释放页框。这些函数将在后面分析。
8.1.6:高端内存页框的内核映射
全局变量high_memory用于表示直接映射的物理内存末端,高端内存的始端。高端内存的页框并不会直接映射在内核线性地址空间中(虚拟地址3~4G,实际为3~3G+896M)。因此,内核不能直接访问高端内存。
在32位X86系统中,为了能够使用高端内存,需要做以下处理:
1:高端内存页框通过alloc_pages()或者alloc_page()分配。他们不返回页框的线性地址。因为高端内存的页框的线性地址不存在。而是返回分配的页框的页描述符的线性地址。
2:因为高端内存的页框没有线性地址,不能被内核访问。所以,内核线性地址空间的最后128M专门用于映射高端内存页框。不同的高端内存都会使用这128M的线性地址。
内核使用三种不同的机制将页框映射到高端内存,分别是永久内核映射,临时内核映射,非连续内存分配。
8.1.6.1:永久内核映射
永久内核映射用于建立高端页框到内核地址空间的长期映射。建立永久内核映射的过程可能发生阻塞。因此,不能在中断或者临界区中建立永久内核映射。
永久内核映射建立的页表地址存放在全局变量pkmap_page_table中
pte_t *pkmap_page_table; //总共是LAST_PKMAP(512或者1024)个页表项 |
此外,还使用数组
static int pkmap_count[LAST_PKMAP]; //和pkmap_page_table中的成员是一一对应关系,数值用于描述pkmap_page_table每个pte的状态。0表示这个pte是空闲的,没有映射高端内存;1表示这个pte没有映射高端内存,但是他是不能使用的,因为其对应的TLB还没有刷新;n表示这个pte映射了高端内存,并且有n-1个内核路径在使用这个pte。 |
来辅助描述永久内核映射页表。
除此之外,还是用全局数组page_address_htable来描述已经被映射的高端内存。在系统中的组织结构如下图所示:
page_address()函数用于返回页框对应的线性地址。由于高端内存的页框没有对应的线性地址,因此会返回高端内存页框对应的页描述符的线性地址。
void *page_address(struct page *page) { unsigned long flags; void *ret; struct page_address_slot *pas; if (!PageHighMem(page)) //检查page描述符结构体的成员flags是否设置了PG_highmem位 return lowmem_page_address(page); pas = page_slot(page); ret = NULL; spin_lock_irqsave(&pas->lock, flags); if (!list_empty(&pas->lh)) { struct page_address_map *pam; list_for_each_entry(pam, &pas->lh, list) { //如果寻找的页框是高端内存,并且没有被映射,那么这个页框不会被加到全局变量page_address_htable中,也就不会在链表操作中被找到,就会返回NULL。如果找到,就说明这个高端内存页框已经被映射,那么就返回这个高端页框映射的虚拟地址 if (pam->page == page) { ret = pam->virtual; goto done; } } } done: spin_unlock_irqrestore(&pas->lock, flags); return ret; } |
介绍一下函数lowmem_page_address
static inline void *lowmem_page_address(struct page *page) { return __va(page_to_pfn(page) << PAGE_SHIFT); //<< PAGE_SHIFT就是乘4096 } |
#define page_to_pfn(page) ((unsigned long)((page) - mem_map)) //两个指针相减,得到的结果是这两个指针之间数组元素的个数 |
要建立高端内存的永久内核映射,需要使用kmap()函数。
void *kmap(struct page *page) { if (!PageHighMem(page)) return page_address(page); //如果这个页框不属于高端内存,那么就直接返回这个页框的虚拟地址 return kmap_high(page); } |
void fastcall *kmap_high(struct page *page) { unsigned long vaddr; spin_lock(&kmap_lock); //这里要加锁,避免多个CPU都修改高端内存页框对应的页表。但是这里不需要禁止中断。因为限制中断中不能使用kmap函数,因为这个函数是可阻塞的。 vaddr = (unsigned long)page_address(page); if (!vaddr) //判断这个页框是否已经被映射过了 vaddr = map_new_virtual(page); //页框没有被映射过 pkmap_count[PKMAP_NR(vaddr)]++; spin_unlock(&kmap_lock); return (void*) vaddr; } |
map_new_virtual此函数的主要操作是,检查pkmap_page_table中是否有空闲的页表项位置(此页表专门用于存储高端内存的映射关系)。如果有的话,那么就填入这个位置;如果没有的话,就将进程加入等待队列,直到另一个进程释放这个高端内存页表中的一个位置,然后换新等待队列中的进程。
static inline unsigned long map_new_virtual(struct page *page) { unsigned long vaddr; int count; start: count = LAST_PKMAP; //1024 /* Find an empty entry */ for (;;) { last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;//这个位操作的作用是,当last_pkmap_nr<512时,保持他的值;等于512时,变成0。 if (!last_pkmap_nr) { //如果last_pkmap_nr的值变成0的时候,这时候应该将高端内存对应的页表项中,无效的项刷出去。 flush_all_zero_pkmaps(); count = LAST_PKMAP; } if (!pkmap_count[last_pkmap_nr]) //找到了空闲位置。之前说过,pkmap_count数组中某一项为0表示高端内存页表中对应的页表项没有使用 break; if (--count) continue { //如果没有找到空闲的页表项,那么将当前任务加入等待队列 DECLARE_WAITQUEUE(wait, current); __set_current_state(TASK_UNINTERRUPTIBLE); add_wait_queue(&pkmap_map_wait, &wait); //将当前任务挂在pkmap_map_wait等待队列上 spin_unlock(&kmap_lock); schedule(); remove_wait_queue(&pkmap_map_wait, &wait); //当有任务释放了高端页表中的页表项的时候,会唤醒挂在这个等待队列上的任务 spin_lock(&kmap_lock); /* Somebody else might have mapped it while we slept 可能有其他进程已经映射了*/ if (page_address(page)) return (unsigned long)page_address(page); /* Re-start */ goto start; } } //找到位置的情况,将其加入高端内存页表中,此时获得的vaddr是高端内存页表中,对应页表项的虚拟地址 vaddr = PKMAP_ADDR(last_pkmap_nr); //这里是找到序号为last_pkmap_nr的页表项对应的虚拟地址,为(PKMAP_BASE + ((nr) << PAGE_SHIFT)),其中PKMAP_BASE是高端内存页表开始映射的虚拟地址首地址。高端内存页表最多映射4M内存。 set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); // mk_pte(page, kmap_prot)的作用是作出高端页表项 pkmap_count[last_pkmap_nr] = 1; set_page_address(page, (void *)vaddr); //这个高端内存页框已经完成映射,将他加到哈希表中。比较复杂,但是一眼能看懂,就不列出了。 return vaddr; } |
这里也是一个使用等待队列的典型例子。
DECLARE_WAITQUEUE(wait, current); |
#define DECLARE_WAITQUEUE(name, tsk) \ wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk) |
#define __WAITQUEUE_INITIALIZER(name, tsk) { \ .task = tsk, \ .func = default_wake_function, \ .task_list = { NULL, NULL } } |
因此,展开后,就是
wait_queue_t wait = {.task = current, .func = default_wake_function, .task_list = {NULL, NULL} } |
同样,有建立虚拟地址和高端内存映射的接口,也有取消虚拟地址和高端内存映射的接口。通过函数kunmap可以实现这个功能。
void kunmap(struct page *page) { if (!PageHighMem(page)) return; kunmap_high(page); //必须是属于高端内存的页框才能进入这个函数 } |
如果确认是高端内存的页框,那么就会调用函数kunmap_high
void fastcall kunmap_high(struct page *page) { unsigned long vaddr; unsigned long nr; int need_wakeup; spin_lock(&kmap_lock); vaddr = (unsigned long)page_address(page); nr = PKMAP_NR(vaddr); //通过这个高端内存的虚拟地址,得到他是对应高端内存页表中的第几个页表项。这里需要注意的是,由于高端内存页框的映射方式,可知道不同的高端内存页框的虚拟地址是不连续的。 /* * A count must never go down to zero * without a TLB flush! */ need_wakeup = 0; switch (--pkmap_count[nr]) { //获得这个高端内存页框的页表项的使用个数 case 0: BUG(); case 1: need_wakeup = waitqueue_active(&pkmap_map_wait); } spin_unlock(&kmap_lock); /* do wake-up, if needed, race-free outside of the spin lock */ if (need_wakeup) wake_up(&pkmap_map_wait); //唤醒挂在这个等待队列上的进程 } |
8.1.6.2:临时内核映射
还可以通过临时内核映射来建立内核到高端内存的页表项。并且由于他们不会阻塞的特性,可以用在中断处理程序和可延迟函数中。
通过函数kmap_atomic建立临时内核映射。
void *kmap_atomic(struct page *page, enum km_type type) { enum fixed_addresses idx; unsigned long vaddr; inc_preempt_count(); if (!PageHighMem(page)) return page_address(page); idx = type + KM_TYPE_NR*smp_processor_id(); vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); set_pte(kmap_pte-idx, mk_pte(page, kmap_prot)); __flush_tlb_one(vaddr); return (void*) vaddr; } |
8.1.7:伙伴系统算法
页框的分配过程中,会出现外碎片的问题。外碎片就是,随着频繁的请求和释放一系列大小不同的页框,会导致在已分配的页框中分散了许多小块的空闲页框。因此,可能系统中剩余的空闲页框的大小大于要分配的页框大小,但是由于不连续,已经无法分配了。
为了解决外碎片的问题,有两种方法,一是分配不连续的空闲页框,将他们映射到连续的线性地址空间,一次满足要求;而是通过伙伴算法这一类算法,避免外碎片的大量产生。
Linux使用伙伴系统算法来解决外碎片的问题。他将所有的空闲页框分组为11个链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续的页框,也就是4K~4M大小的连续页框。
8.1.7.1:数据结构
每个管理区使用不同的伙伴系统。因此对于x86而言,有三个内存管理区,因此也就有三个伙伴系统。分别是适用于ISA DMA(0~16M)的伙伴系统;适用于常规页框(16M~896M)的伙伴系统;适用于高端内存的伙伴系统(896M~4G)。每个伙伴系统使用的数据结构如下所示(都包含在管理区描述符中):
struct zone { unsigned long free_pages; unsigned long pages_min, pages_low, pages_high; unsigned long lowmem_reserve[MAX_NR_ZONES]; struct per_cpu_pageset pageset[NR_CPUS]; //用于实现每CPU高速缓存 spinlock_t lock; struct free_area free_area[MAX_ORDER]; //每个元素都是一个链表头,链表链接了不同大小的空闲页框组成的块。第n个元素,就是大小为2^n的空闲页框块链表的链表头 ZONE_PADDING(_pad1_) spinlock_t lru_lock; struct list_head active_list; struct list_head inactive_list; unsigned long nr_scan_active; unsigned long nr_scan_inactive; unsigned long nr_active; unsigned long nr_inactive; unsigned long pages_scanned; /* since last reclaim */ int all_unreclaimable; /* All pages pinned */ int temp_priority; int prev_priority; ZONE_PADDING(_pad2_) wait_queue_head_t * wait_table; unsigned long wait_table_size; unsigned long wait_table_bits; struct pglist_data *zone_pgdat; struct page *zone_mem_map; /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */ unsigned long zone_start_pfn; unsigned long spanned_pages; /* total size, including holes */ unsigned long present_pages; /* amount of memory (excluding holes) */ char *name; } ____cacheline_maxaligned_in_smp; |
1:管理区把包含的页框。每个管理区包含的页框都是mem_map的子集。struct page *zone_mem_map:表示了这个管理区包含的第一个页框描述符;
2:管理区描述符中struct free_area free_area[MAX_ORDER]:每个元素对应的就是一种块大小。比如第k个元素,对应的就是包含2^k个页框的空闲块。
struct free_area { struct list_head free_list; //连接了空闲页框块的第一个页描述符的lru成员 unsigned long nr_free; //指定了这个链表头链接的链表节点个数,其实就是,这个大小的空闲页框块的个数 }; |
他的free_list指向空闲块的第一个页描述符的lru。第一个页描述符的lru的prev指向free_list,next指向第二个空闲块的起始页的页描述符的lru的prev。这样就是用一个双向链表将所有大小为2^k的空闲内存块的起始页的页描述符联系了起来。
8.1.7.2:分配块
使用函数
static struct page *__rmqueue(struct zone *zone, unsigned int order) |
从管理区中分配一个空闲块。这个函数使用时,默认已经关中断,并且获得了内存管理区的锁。
static struct page *__rmqueue(struct zone *zone, unsigned int order) { struct free_area * area; unsigned int current_order; struct page *page; for (current_order = order; current_order < MAX_ORDER; ++current_order) { area = zone->free_area + current_order; //拿到order对应的数组成员,这个数组成员是struct free_area类型的结构体 if (list_empty(&area->free_list)) continue; page = list_entry(area->free_list.next, struct page, lru); //刚才已经说了,freee_list链表链接的是每个页描述符的lru。因此,找到满足要求的空闲页框块的第一个页的页描述符 list_del(&page->lru); rmv_page_order(page); //其实就是将页描述符的private成员设置为0 area->nr_free--; zone->free_pages -= 1UL << order; return expand(zone, page, order, current_order, area);//这个函数的作用是,如果是从更大的空闲内存块分成数个小的空闲内存块,那么要将小的内存块加入对应的空闲内存块链表中 } return NULL; } |
在这里分析一下函数expand。假设某一次high的值为8,low的值为2。也就是说,我们本来需要4个连续页框,但是没有这么小的连续页框了,只能从256个连续页框中分出4个页框出来。
static inline struct page *expand(struct zone *zone, struct page *page, int low, int high, struct free_area *area) // low:这次分配的2^n个页框的这个n值;high:这次是从2^n个空闲页框中分出来的,这个n的值 { unsigned long size = 1 << high; //size=256 while (high > low) { area--; //area是数组地址,最开始是第7个,后面依次变为6,5…… high--; size >>= 1; list_add(&page[size].lru, &area->free_list); //第一次size=256,因此会将page+256表示的页描述符放入area中,从此可知,每次分出去的页框都是在前面的页框 area->nr_free++; set_page_order(&page[size], high); //设置空闲页框块的第一个页描述符的private字段为order } return page; } |
8.1.7.3:释放块
使用函数
static inline void __free_pages_bulk (struct page *page, struct page *base, struct zone *zone, unsigned int order) |
释放块。
同样,假设我们要释放的是内存管理区ISA DMA(0~16M)中的页框块,是从8~10M的大小为2M的页框块,并且假设其他页框块都是空闲的。
static inline void __free_pages_bulk (struct page *page, struct page *base, struct zone *zone, unsigned int order) //page是被释放页框块的第一个页描述符,zone是管理区描述符地址,order是块大小,base是这个管理区的第一个页框的页框描述符 { unsigned long page_idx; struct page *coalesced; int order_size = 1 << order; //order_size=512 page_idx = page - base; // page_idx表明这个页框是这个管理区的第几个页框,page_index=2048 zone->free_pages += order_size; while (order < MAX_ORDER-1) { struct free_area *area; struct page *buddy; int buddy_idx; buddy_idx = (page_idx ^ (1 << order)); //使用(1<<order)异或page_idx,就是将page_index的order位设置为当前的相反值。因此,如果这个位原先是0,那么buddy_idx=page_index+order_size;否则, buddy_idx=page_index-order_size buddy = base + buddy_idx; if (bad_range(zone, buddy)) //如果伙伴页框不在这个管理区中,那么跳出 break; if (!page_is_buddy(buddy, order)) //如果这个页不能当做伙伴页,那么跳出 break; /* Move the buddy up one level. */ list_del(&buddy->lru); //将伙伴页框从空闲页框块链表中删除,因为伙伴页框要和我们释放的页框一起,作为一个更大的页框,添加到页框链表中了 area = zone->free_area + order; area->nr_free--; rmv_page_order(buddy); page_idx &= buddy_idx; order++; } coalesced = base + page_idx; //将组成后的大页添加到页框链表中 set_page_order(coalesced, order); list_add(&coalesced->lru, &zone->free_area[order].free_list); zone->free_area[order].nr_free++; } |
8.1.8:每CPU页框高速缓存
内核经常请求或者释放单个页框。为此,每个内存管理区都定义了一个每CPU页框高速缓存。所有的每CPU高速缓存都包含一些预先分配的页框,用于处理CPU发出的单一内存请求。
还是要注意,每CPU高速缓存的页框是从伙伴系统中分出来的。
struct zone { …… struct per_cpu_pageset pageset[NR_CPUS]; …… } |
struct per_cpu_pageset { struct per_cpu_pages pcp[2]; //0: hot。1: cold,分别表示热高速缓存和冷高速缓存 } ____cacheline_aligned_in_smp; |
struct per_cpu_pages { int count; /* 高速缓存中页框个数 */ int low; /* 下界,表示高速缓存需要补充 */ int high; /* 上界,表示高速缓存需要释放*/ int batch; /*每次添加或者删除页框的数目*/ struct list_head list; //链表头,链表链接的是所有属于此高速缓存的页的页描述符,依然是连接到页描述符的lru上。 }; |
在冷热高速缓存中,如果页框个数低于low,那么内核通过伙伴系统分配batch个页框到高速缓存中;如果页框个数高于high,那么内核从高速缓存中释放batch个页框到伙伴系统中。
8.1.8.1:通过每CPU高速缓存分配页框
使用函数
struct page *buffered_rmqueue(struct zone *zone, int order, int gfp_flags) |
来分配页框。这个函数是伙伴系统的核心,它既能从每CPU高速缓存中分配页框,也可以从高速缓存中分配页框。
static struct page *buffered_rmqueue(struct zone *zone, int order, int gfp_flags) { unsigned long flags; struct page *page = NULL; int cold = !!(gfp_flags & __GFP_COLD); // gfp_flags是分配标志 if (order == 0) { //只有在分配一个页框的时候,使用每CPU高速缓存 struct per_cpu_pages *pcp; pcp = &zone->pageset[get_cpu()].pcp[cold];//这里确定是从冷还是热高速缓存中分配页框 local_irq_save(flags); if (pcp->count <= pcp->low) //如果现在高速缓存中的页数小于下界,那么需要向高速缓存中添加页 pcp->count += rmqueue_bulk(zone, 0, pcp->batch, &pcp->list); //利用伙伴系统,从内存管理区中找到batch页添加到高速缓存中 if (pcp->count) { page = list_entry(pcp->list.next, struct page, lru); //这里从高速缓存中拿到一页 list_del(&page->lru); pcp->count--; } local_irq_restore(flags); put_cpu(); //开抢占,为什么开抢占不清楚 } if (page == NULL) {//如果要分配的页数不止1页,或者是1页,但是之前的操作没有完成分配的时候,直接在伙伴系统中分配 spin_lock_irqsave(&zone->lock, flags); page = __rmqueue(zone, order);//使用伙伴系统进行页框的分配 spin_unlock_irqrestore(&zone->lock, flags); } if (page != NULL) { //这时候已经分配成功 mod_page_state_zone(zone, pgalloc, 1 << order); prep_new_page(page, order); if (gfp_flags & __GFP_ZERO) prep_zero_page(page, order, gfp_flags); if (order && (gfp_flags & __GFP_COMP)) prep_compound_page(page, order); } return page; } |
分析内部函数rmqueue_bulk,它的目的是从伙伴系统中,分出batch个页框给每CPU高速缓存。
static int rmqueue_bulk(struct zone *zone, unsigned int order,unsigned long count, struct list_head *list) { unsigned long flags; int i; int allocated = 0; struct page *page;
spin_lock_irqsave(&zone->lock, flags); for (i = 0; i < count; ++i) { //每次分配一页,分配count,也就是batch次 page = __rmqueue(zone, order); if (page == NULL) break; allocated++; list_add_tail(&page->lru, list); //将分配出来的页添加到每CPU高速缓存链表中,list=zone->pageset[cpu_id].pcp[cold]->list } spin_unlock_irqrestore(&zone->lock, flags); return allocated; } |
8.1.8.2:释放页框到每CPU页框高速缓存
static void fastcall free_hot_cold_page(struct page *page, int cold) { struct zone *zone = page_zone(page);//获得相应的内存管理区描述符地址 struct per_cpu_pages *pcp; unsigned long flags; arch_free_page(page, 0); kernel_map_pages(page, 1, 0); inc_page_state(pgfree); if (PageAnon(page)) page->mapping = NULL; free_pages_check(__FUNCTION__, page); pcp = &zone->pageset[get_cpu()].pcp[cold]; local_irq_save(flags); if (pcp->count >= pcp->high) pcp->count -= free_pages_bulk(zone, pcp->batch, &pcp->list, 0); //释放每CPU高速缓存中的多余页,将之添加进伙伴系统中 list_add(&page->lru, &pcp->list);//将本次要释放的页添加到每CPU高速缓存中 pcp->count++; local_irq_restore(flags); put_cpu(); } |
8.1.9:管理区分配器
管理区分配器是内核页框分配器的前端。也就是说,在分配页框的时候,首先要找到一个适合的管理区,然后再在此管理区中,通过页框管理器分配页框。
对一组连续页框的每次请求实质上是通过执行alloc_pages宏来处理的。这个宏实际上调用了函数:
struct page * fastcall __alloc_pages(unsigned int gfp_mask, unsigned int order, struct zonelist *zonelist) |
gfp_mask:内存分配的标志,包含是否在冷热CPU高速缓存页框中分配的标志位 |
order:表示要分配2^order个页框 |
zonelist:指向zonelist的数据结构,该数据结构按照优先次序描述了适用于内存分配的内存管理区,他的数据结构如下 struct zone *zones[MAX_NUMNODES * MAX_NR_ZONES + 1]; |
需要注意的是参数struct zonelist *zonelist,他表示了用于内存分配的内存管理区的优先次序。
首先要理解函数zone_watermark_ok
int zone_watermark_ok(struct zone *z, int order, unsigned long mark, int classzone_idx, int can_try_harder, int gfp_high) { long min = mark, free_pages = z->free_pages - (1 << order) + 1; int o; if (gfp_high) min -= min / 2; if (can_try_harder) min -= min / 4; //上面共同确定min的值 // 要求管理区剩下的页框数目必须大于min+保留的页框数 if (free_pages <= min + z->lowmem_reserve[classzone_idx]) return 0; //除了被分配的页框,在order为k的块中,管理区至少有min/2^k个空闲页框。这段代码很容易看懂,但不是很好描述 for (o = 0; o < order; o++) { /* 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; } |
通过下面的函数__alloc_pages可以看到,只有当函数zone_watermark_ok返回1的时候,才能够分配页框。zone_watermark_ok会依据不同的参数确定一个最小值min。只有在以下情况,函数会返回1:
1:除了要分配的页框外,在内存管理区中至少还有min个空闲页框,并且这min个空闲页框不能包含为内存不足保留的页框(lowmem_reserve字段描述)。
2:除了被分配的页框,在order为k的块中,管理区至少有min/2^k个空闲页框。
可以看到,上面的函数也是按照这样的要求编写的。
再看函数__alloc_pages,这个函数的逻辑就是,找到一个合适的内存管理区,然后在这个内存管理区中分配需要的页框。
struct page * fastcall __alloc_pages(unsigned int gfp_mask, unsigned int order, struct zonelist *zonelist) { const int wait = gfp_mask & __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]); 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)) 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)) 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()) { /* go through the zonelist yet again, ignoring mins */ 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); //杀死一个进程并释放内存,参考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: zone_statistics(zonelist, z); return page; } |
8.1.9.2:释放一组页框
释放页框的函数如下所示:
fastcall void __free_pages(struct page *page, unsigned int order) { if (!PageReserved(page) && put_page_testzero(page)) { //检查页框是否是动态页框(PG_reserved位表示),并且页框没有被其他进程使用(count=0) if (order == 0) free_hot_page(page); //只有一页的话,释放到热每CPU高速缓存 else __free_pages_ok(page, order); //通过伙伴系统释放 } } |
8.2:内存区管理
上面关注的都是按页框进行分配。但是,当我们请求小内存区的时候,如果按照页框进行分配,就会造成大量的浪费。
8.2.1:slab分配器
slab分配器把对象,也就是一种数据结构,放入高速缓存。这个高速缓存就可以看做是这一类对象的专用储备。每个高速缓存都被划分成多个slab。每个slab由一个或者多个连续的页框组成,其中包含了已分配的对象(数据结构),也包含空闲的对象(数据结构)。
8.2.2:高速缓存描述符
每个高速缓存都通过kmem_cache结构体描述。所有的高速缓存通过双链表连接在一起,生成了高速缓存链表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; /* # of objs per 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; /* force GFP flags, e.g. GFP_DMA */ unsigned int gfpflags; size_t colour; /* cache colouring range */ unsigned int colour_off; /* colour offset */ unsigned int colour_next; /* cache colouring */ kmem_cache_t *slabp_cache; 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; }; |
8.2.3:slab描述符
高速缓存中的每个slab都有自己的描述符
struct slab { struct list_head list; //用于连接高速缓存描述符的kmem_list3中的链表字段 unsigned long colouroff; void *s_mem; /* including colour offset */ unsigned int inuse; /* num of objs active in slab */ kmem_bufctl_t free; }; |
每个高速缓存的第一个slab的list的prev指向高速缓存描述符,next指向下一个与之相似的slab的slab的list.prev。slab描述符可以存在slab外部或者内部。
8.2.4:普通和专用高速缓存
slab高速缓存分成两类,普通高速缓存和专用高速缓存。普通高速缓存包含以下两种
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的代码实现如下:
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为cache中存储的每个结构体的大小 { size_t left_over, slab_size, ralign; kmem_cache_t *cachep = NULL; if ((!name) || in_interrupt() || (size < BYTES_PER_WORD) || (size > (1<<MAX_OBJ_ORDER)*PAGE_SIZE) || (dtor && !ctor)) { printk(KERN_ERR "%s: Early error in slab %s\n", __FUNCTION__, name); BUG(); } if (size & (BYTES_PER_WORD-1)) { //对象大小向上对齐处理器字长 size += (BYTES_PER_WORD-1); size &= ~(BYTES_PER_WORD-1); } if (flags & SLAB_HWCACHE_ALIGN) { //这里是为了让cache中的每个对象都按照一定的方式对齐,这样能够保证不同的结构体对象在同一个cache_line上,加快访问效率 ralign = cache_line_size(); while (size <= ralign/2) ralign /= 2; } else { ralign = BYTES_PER_WORD; } if (ralign < ARCH_SLAB_MINALIGN) { ralign = ARCH_SLAB_MINALIGN; if (ralign > BYTES_PER_WORD) flags &= ~(SLAB_RED_ZONE|SLAB_STORE_USER); } if (ralign < align) { ralign = align; if (ralign > BYTES_PER_WORD) flags &= ~(SLAB_RED_ZONE|SLAB_STORE_USER); } align = ralign; /* Get cache's description obj. */ 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大于一页的1/8的时候,就将slab结构体放在高速缓存空间内 flags |= CFLGS_OFF_SLAB; size = ALIGN(size, align); if ((flags & SLAB_RECLAIM_ACCOUNT) && size <= PAGE_SIZE) { //如果一个对象的size小于一页,那么就只需要分配1页,给这个高速缓存 cachep->gfporder = 0; //只分配一页 cache_estimate(cachep->gfporder, size, align, flags, &left_over, &cachep->num); //这个函数的作用是,计算得到cachep->num,这个成员代表的含义是,这高速缓存中可以存储多少个对象 } else { 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) { printk("kmem_cache_create: couldn't create cache %s.\n", name); kmem_cache_free(&cache_cache, cachep); cachep = NULL; goto opps; } slab_size = ALIGN(cachep->num*sizeof(kmem_bufctl_t) + sizeof(struct slab), align); //这里获得slab的尺寸,可以看到,包含了结构体struct slab,还包含了num个kmem_bufctl_t if (flags & CFLGS_OFF_SLAB && left_over >= slab_size) { flags &= ~CFLGS_OFF_SLAB; left_over -= slab_size; } if (flags & CFLGS_OFF_SLAB) { slab_size = cachep->num*sizeof(kmem_bufctl_t)+sizeof(struct slab); } cachep->colour_off = cache_line_size(); if (cachep->colour_off < align) cachep->colour_off = align; cachep->colour = left_over/cachep->colour_off; cachep->slab_size = slab_size; cachep->flags = flags; cachep->gfpflags = 0; if (flags & SLAB_CACHE_DMA) cachep->gfpflags |= GFP_DMA; spin_lock_init(&cachep->spinlock); cachep->objsize = size; /* NUMA */ INIT_LIST_HEAD(&cachep->lists.slabs_full); INIT_LIST_HEAD(&cachep->lists.slabs_partial); INIT_LIST_HEAD(&cachep->lists.slabs_free); //初始化cache的三个链表 if (flags & CFLGS_OFF_SLAB) cachep->slabp_cache = kmem_find_general_cachep(slab_size,0); cachep->ctor = ctor; cachep->dtor = dtor; cachep->name = name; /* Don't let CPUs to come and go */ lock_cpu_hotplug(); if (g_cpucache_up == FULL) { enable_cpucache(cachep); } else { if (g_cpucache_up == NONE) { cachep->array[smp_processor_id()] = &initarray_generic.cache; g_cpucache_up = PARTIAL; } else { cachep->array[smp_processor_id()] = kmalloc(sizeof(struct arraycache_init),GFP_KERNEL); } BUG_ON(!ac_data(cachep)); ac_data(cachep)->avail = 0; ac_data(cachep)->limit = BOOT_CPUCACHE_ENTRIES; ac_data(cachep)->batchcount = 1; ac_data(cachep)->touched = 0; cachep->batchcount = 1; cachep->limit = BOOT_CPUCACHE_ENTRIES; cachep->free_limit = (1+num_online_cpus())*cachep->batchcount + cachep->num; } cachep->lists.next_reap = jiffies + REAPTIMEOUT_LIST3 + ((unsigned long)cachep)%REAPTIMEOUT_LIST3; /* Need the semaphore to access the chain. */ 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; 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); unlock_cpu_hotplug(); opps: if (!cachep && (flags & SLAB_PANIC)) panic("kmem_cache_create(): failed to create slab `%s'\n", name); return cachep; } |
这里单独说明一下函数
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; //这个是高速缓存cache的order对应的size,单位为字节。每次对cache进行扩容的时候,都是再给他分配1<<order个页。 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++; //这里是计算出这么大的size总共可以存下多少个对象。这里也要注意,每一个对象都对应的一个kmem_bufctl_t结构体 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; } |
8.2.5:给高速缓存分配slab,以及删除一个slab
一个新创建的高速缓存,不包含任何slab。只有当,向一个高速缓存发出一个分配新对象的请求,并且高速缓存不包含任何空闲对象的时候,才会给高速缓存分配slab。
static int cache_grow (kmem_cache_t * cachep, int flags, int nodeid) |
使用此函数向高速缓存中分配一个slab。
首先进入kmem_getpages为slab分配页框。
static int cache_grow (kmem_cache_t * cachep, int flags, int nodeid) { struct slab *slabp; void *objp; size_t offset; int local_flags; unsigned long ctor_flags; if (flags & ~(SLAB_DMA|SLAB_LEVEL_MASK|SLAB_NO_GROW)) BUG(); 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; cachep->colour_next++; if (cachep->colour_next >= cachep->colour) cachep->colour_next = 0; offset *= cachep->colour_off; spin_unlock(&cachep->spinlock); if (local_flags & __GFP_WAIT) local_irq_enable(); kmem_flagcheck(cachep, flags); /* 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 = objp; cache_init_objs(cachep, slabp, ctor_flags); 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; } |
使用函数slab_destroy删除一个slab。
8.2.6:slab分配器与分区页框分配器的接口
刚刚已经介绍了,如何给一个高速缓存中创建新的slab。这里介绍如何创建一个新的slab。创建新的slab的时候,必须使用页框分配器(也就是之前所说的,先通过内存区管理区确定内存管理区,再在此管理区中分配页框的函数)来获得一组连续的空闲页框。使用函数kmem_getpages
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); //这里就是前面讲的页框分配器,通过伙伴系统,获得空闲页框 } 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); add_page_state(nr_slab, i); while (i--) { //将所有分配的页的PG_slab置位 SetPageSlab(page); page++; } return addr; } |
完成页框分配。它会返回分配的页框的线性地址。
使用函数kmem_freepages,将slab的页框返还给伙伴系统
static void kmem_freepages(kmem_cache_t *cachep, void *addr) { unsigned long i = (1<<cachep->gfporder); struct page *page = virt_to_page(addr); const unsigned long nr_freed = i; while (i--) { if (!TestClearPageSlab(page)) 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的页框释放。
8.2.7:分配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); ac = ac_data(cachep); if (likely(ac->avail)) { ac->touched = 1; objp = ac_entry(ac)[--ac->avail]; //objp=((void **)(ac+1))[--ac->avail],得到一个对象的地址 } else { objp = cache_alloc_refill(cachep, flags); } local_irq_restore(save_flags); return objp; } |
8.2.8:对象描述符
存放在slab中的每个对象都有一个对象描述符
typedef unsigned short kmem_bufctl_t; |
从创建一个高速缓存的时候也知道,每个slab描述符后面就是跟着num个kmem_bufctl_t,num就是一个slab中包含的对象的个数。因此,kmem_bufctl_t和结构体是一一对应的关系。kmem_bufctl_t包含的是下一个空闲对象在slab中的下标。
8.2.9:slab着色
着色主要用于处理下面的问题:由于不同的内存块可能映射到相同的缓存行之上,在同一个cache的不同slab内,有相同偏移量的对象很可能映射到同一高速缓存行中。这样就可能发生cache的抖动。
之前讲过,一个高速缓存中一般有没有用完的字节。使用这些字节,让同一cache中不同的slab的对象结构体存放首地址不同。
例如,从下图中看,就是不同的颜色col,导致每个对象的位置不同。
8.2.10:空闲slab对象的本地缓存
高速缓存结构体中的成员
struct array_cache *array[NR_CPUS]; |
有着重要的含义:
struct array_cache { unsigned int avail; unsigned int limit; unsigned int batchcount; unsigned int touched; }; |
array本质上是一个二级数组。第一级是NR_CPUS个struct array_cache *结构体。因此,第二级是一个指针。在初始化的时候,struct array_cache *分配的内存大小并不是struct array_cache的大小,而是
struct arraycache_init { struct array_cache cache; void * entries[BOOT_CPUCACHE_ENTRIES]; }; |
cachep->array[smp_processor_id()] = kmalloc(sizeof(struct arraycache_init),GFP_KERNEL); |
的大小。entries同样是一个数组,每个数组成员都是一个地址,这个地址就是空闲的对象结构体的首地址。因此,在代码中,我们会经常看到,使用
ac_entry(ac)[--ac->avail]; |
来分配一个结构体。
通过函数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); ac = ac_data(cachep); if (likely(ac->avail)) { //要记得,在我们第一次创建cache的时候,我们将avail这个成员设置为0。 ac->touched = 1; objp = ac_entry(ac)[--ac->avail]; } else { objp = cache_alloc_refill(cachep, flags); } local_irq_restore(save_flags); return objp; } |
因此,在第一次malloc的时候,主要要看看函数cache_alloc_refill做了些什么
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); retry: batchcount = ac->batchcount; // batchcount:本地高速缓存重新填充或者腾空时使用的块大小 if (!ac->touched && batchcount > BATCHREFILL_LIMIT) { //如果这个高速缓存最近被使用过,那么touched标志被设置为1 batchcount = BATCHREFILL_LIMIT; } l3 = list3_data(cachep); spin_lock(&cachep->spinlock); if (l3->shared) { //这里我们假设shared为0 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; //这里是拿到部分使用的slab if (entry == &l3->slabs_partial) { //链表操作,如果已经遍历了一遍,但是还是没有找到合适的slab的话,就要进入新建一个slab的流程 l3->free_touched = 1; entry = l3->slabs_free.next; if (entry == &l3->slabs_free) goto must_grow; } 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中第一个对象的地址,因此,ac_entry(ac)[ac->avail++]就是一个空闲对象的地址 slabp->inuse++; next = slab_bufctl(slabp)[slabp->free]; slabp->free = next; //free:slab中下一个空闲对象的下标 } check_slabp(cachep, slabp); /* move slabp to correct slabp list: */ list_del(&slabp->list); if (slabp->free == BUFCTL_END) 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]; } |
8.2.11:释放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; } } |
如果本地高速缓存中,没有位置来存放空闲对象的指针,就进入函数
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)); //slab包含的页框的页描述符的lru.prev指向相应的slab描述符 list_del(&slabp->list); objnr = (objp - slabp->s_mem) / cachep->objsize; check_slabp(cachep, slabp); slab_bufctl(slabp)[objnr] = slabp->free; slabp->free = objnr; //在分配对象的时候会用到free字段。下次分配的时候,会优先分配这次释放的对象占据的内存空间 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加入部分使用,部分为使用的链表中 } } } |
8.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; for (; csizep->cs_size; csizep++) { if (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; } |
8.3:非连续内存区管理
将内存区映射到一组连续的页框是最好的选择,这样能够充分利用高速缓存并获得较低的访问时间。但是,如果对内存区的请求不是很频繁。那么,通过连续的线性地址来访问非连续的页框也会很有意义。这样的主要优点是避免外碎片,缺点是要打乱内核页表。
8.3.1:非连续内存区的线性地址
这张图可以看到内核的线性地址空间分配。物理内存映射部分是对0~896M内存进行映射的线性地址。FIXADDR_START~4G是包含固定映射的线性地址(固定映射的线性地址),PKMAP_BASE开始是用于高端内存页框的永久内核映射的线性地址(高端内存页框的内核映射)。
最终,剩下的,从VMALLOC_START开始,到VMALLOC_END结束的线性地址用于非连续内存区映射。