内核采用 struct page 来表示一个物理页,在其中记载了诸多物理页的属性,比如 物理页被几个线程使用(如若没有则表示该页可以释放),页对应的虚拟地址。
首先需要知道的是,分配物理页可以分为两个步骤 :
1) 寻找内核中空闲 处于3-4G 4K对齐的虚拟地址。
i)虚拟页是否空闲 由页表项的属性决定,里面会有标志位记录。
ii)虚拟页需要4K对齐是强制性的,因为页目录项的低12 bit是用来记录页目录属性的,高20 bit才是记录页目录项的物理地址。
2) 寻找内核中空闲的物理页。
3) 通过改变页表项来修改映射关系,如何修改的请看 我的另一篇博客 : https://www.cnblogs.com/vizdl/p/11925205.html。
1 // 物理页表示 2 struct page { 3 unsigned 1ong flags ; 4 atomic_t _count ; 5 atomic_t mapcount; 6 unsigned 1ong private; 7 struct address_space * mapping; 8 pgoff_t index; 9 struct list_ head lru; 10 void *virtual; 11 };
由物理地址进行分区 : 在系列文章2中也讲了分区的一些缘由,详情可看系列博客2中内核空间分区部分 https://www.cnblogs.com/vizdl/p/12233623.html
内核空间一般被分为三个区 : ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM,直接提供的接口可以建议内核从特定区域申请物理页。
struct zone { unsigned long watermark[NR_ WMARK]; unsigned 1ong lowmem_ reserve[MAX_ NR_ ZONES]; struct per_cpu_pageset pageset[NR_CPUS]; spinlock_t lock; struct free_ area free_area[MAX_ ORDER] spinlock_t lru_1ock; struct zone_1ru { struct list_head list; unsigned 1ong nr_saved_scan; } lru[NR_ LRU_ LISTS]; struct zone_reclaim_ stat reclaim_ stat; unsigned long pages_scanned; unsigned long flags; atomic_1ong_t vm_stat[NR_ _VM_ ZONE STAT_ ITEMS]; int prev_priority; unsigned int inactive_ratio; wait_queue_head_t *wait_ table; unsigned 1ong wait_ table_ hash_ nr_ entries; unsigned 1ong wait_ table_ bits; struct pglist_ data *zone_ pgdat; unsigned long zone_ start_ pfn; unsigned long spanned_ pages; unsigned 1ong present_ pages; const char *name; };
对物理页操作接口: 物理页采用buddy算法来进行分配,单位最小为1页。只能申请2^order页,这也是buddy算法的特性。
特别说明 :
1) gfp_t gfp_mask,这个参数就是用来建议内核在具体哪个区域得到物理页。
2) 如若已经无可用物理页了,就会采用一系列 页面置换算法, 将物理页上的内容保存到磁盘中的swap sapce(交换空间)中去。
// 分配 2^order个连续的物理页(使用buddy); struct page *al1oc_ pages(gfp_t gfp_mask, unsigned int order); // 利用page中的 virtual属性 得到对应的虚拟地址。 void *page_address(struct page *page); // 分配 2^order个连续的物理页,且直接返回逻辑地址 unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order); // 分配一页物理内存 struct page *alloc_page(gfp_t pfp_mask); // 分配一页物理内存 并返回逻辑地址 struct page *__get_free_page(gfp_t pfp_mask); // 获取填充为0的页 unsigned long get_zeroed_page(unsigned int gfp_mask);
物理页的释放函数 :
// 释放物理地址 void __free_pages(struct page *page, uns igned int order); void free_pages(unsigned long addr, unsigned int order); void free_page(unsigned 1ong addr);
对于小块内存分配,内核也提供了Slab分配器来进行分配,这个分配器可以提供一个缓冲,不会将申请的物理解除绑定虚拟地址,理由是一些小块的内存(特别是结构体)会在内核中频繁地申请与释放。
Slab层的一些接口 :
// 字节为单位的内存分配(slab层) // kmalloc()在<linux/slab.h>中声明: // 分配物理地址连续的地址, 出错返回NULL void *kmalloc(size_t size, gfp_t flags); // 物理不一定连续,虚拟连续。 void *vmalloc(unsigned long size); // 释放 void kfree(const void *ptr); void vfree(const void *addr);
slab层的好处 :
1) 避免了频繁地寻找空闲虚拟页和空闲物理页,提高效率。
2) 避免了因频繁修改物理页与虚拟页映射关系而导致的物理页无法有大块平坦(连续)物理空间。
slab本质其实就是一个内存池。 而内存池之后我可能会总结malloc的内存分配算法,那个算法完全可以用于slab。不过一个是堆内存池,一个是内核内存池。
slab与Buddy的关系:
由上述 : 目前对于内核空间的内存分配已经有一个简单的策略认识了, 即 小块内存 用slab分配器分配,大块内存(最小单位为页)用 Buddy系统 直接分配。而slab层内存池的内存也是Buddy分配出来的。只不过Slab在此做了一个缓冲罢了。