内存管理
页(page)
内核中把物理页作为内存管理的基本单位,一般每个页的大小为4KB,例如在有1G的物理内存的机器上,物理内存会被划分为266144个页。内核中用struct page结构表示每个物理页,该结构体位于<linux/mm.types.h>中,事实上,最新Linux版本的struct page实现中大量用到union,也就是同一个元素在不同场景下有不同的意义。因为每个page frame都需要一个struct page来描述,一个Page frame占4KB,一个struct page占32字节,那所有的struct page需要消耗的内存占整个系统内存的32/4096,这一开销占30多MB。struct page描述和管理的是这4KB的物理内存,它并不关注这段内存中的数据变化。
struct page {
unsigned long flags;//存放页的状态:是否为脏页、是否被锁在内存中
atomic_t count; //页的引用计数,当为-1时说明内核没有引用该页,-1时使用page_count()返回0,当返 回count值为0时,该page frame可被free掉
atomic_t _mapcount;// 表示该page frame被映射的个数,也就是多少个page table entry中含有这个page frame的PFN(页面框架号(pfn))
struct list_head lru;//"least recently used"的缩写,根据page frame的活跃程度(使用频率), 一个可回收的page frame要么挂在active_list双向链表上,要么挂在 inactive_list双向链表上,以作为页面回收的选择依据,lru中包含的就是指向所在链表中前后节点的指针
struct address_space *mapping;//指向文件inode对应的address_space
unsigned long index;//index表示该page在文件内的offset(以page size为单位)
void *virtual; //页的虚拟地址
...
}
重要的问题**:那如何找一个page对应的物理地址呢?**(即如何实现每一个page页与物理页一一对应)
我们知道物理内存按照大小为 (1<<PAGE_SHIFT)分为很多个页,每个这样的页就对应一个struct page * page结构,这些页描述结构存放在一个称之为mem_map的数组里面,而且是严格按照物理内存的顺序来存放的,也就是物理上的第一个页描述结构,作为 mem_map数组的第一个元素,依次类推。所以,每个页描述结构(page)在数组mem_map里的位置在乘以页的大小,就可以得到该页的物理地址了。
page和物理页帧转换
define pfn_to_page(pfn) (mem_map + ((pfn) - PHYS_PFN_OFFSET))//pfn相当于该page在mem_map数组中的索引号,相当于mem_map[pfn]
define page_to_pfn(page) ((unsigned long)((page) - mem_map) + PHYS_PFN_OFFSET)//page-mem_map得到该数组的索引,索引号就是物理页帧号pfn
页的分配与释放
内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所以这些均以页为单位分配内存,定义于<linux/gfp.h>中
stalloc_page *alloc_page(gfp_t gfp_mask, unsigned int order)
该函数分配2^order(1<<order)个连续的物理页
返回一个指针,该指针指向于第一个页的page结构体
void * page_address(struct page *page)
该函数返回一个指针,指向给定物理页当前所在的逻辑地址。
unsigned long __get_free_pages(gfp_t gfp_mask,unsigned int order)
该函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。
区(zone)
对区(zone)的划分于硬件有关,对于不同的处理框架可能不一样。Zone是用于管理物理内存的,但zone与zone之间并没有任何的物理分割,它只是Linux为了便于管理进行的一种逻辑意义上的划分。可以通过"cat /proc/zoneinfo | grep Node"命令查看系统中包含的zones的种类
如上图,有防止内存碎片化的ZONE_MOVABLE和支持设备热插拔的ZONE_DEVICE。
zone_type定义在include/linux/mmzone.h(内核版本2.6.34)
enum zone_type
{
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES
};
linux主要使用四种区:
- ZONE_DMA——这个区的页能用来执行DMA操作
- ZONE_DMA——和ZONE_DMA类似,只不过只能被32位设备访问
- ZONE_NORMAL——这个区包含的都是能正常映射的页
- ZONE_HIGHEM——这个区包含“高端内存”
struct zone {
spinlock_t lock;//防止并发访问strcut zone
unsigned long spanned_pages;//zone含有的总的page frames数目
unsigned long present_pages;
unsigned long nr_reserved_highatomic;
atomic_long_t managed_pages;
struct free_area free_area[MAX_ORDER];
unsigned long _watermark[NR_WMARK];//watermark有min(mininum), low, high三种,可作为启动内存回收的判断标准
long lowmem_reserve[MAX_NR_ZONES];
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
unsigned long zone_start_pfn;//zone的起始物理页面号
struct pglist_data *zone_pgdat;//指向zone所属的node
struct page *zone_mem_map;
...
}
在X86结构中,linux内核虚拟地址空间划分03G为用户空间,34G为内核空间。内核虚拟空间(3~4G)又划分为三种类型的区:
ZONE_DMA 3G之后起始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~1G
由于内核的虚拟和物理地址只差一个偏移量:物理地址=逻辑地址-0xC0000000,所以如果1G内核空间完全用来线性映射,显然物理内也只能访问到1G区间,这显然是不合理的。
在 x86 32
位系统里,Linux 内核地址空间是指虚拟地址从 0xC0000000
开始到 0xFFFFFFFF
为止的高端内存地址空间,总计 1G
的容量, 包括了内核镜像、物理页面表、驱动程序等运行在内核空间 。
内核空间细分区域:
直接映射区
直接映射区Direct Memory Region
:从内核空间起始地址开始,最大896M的内核空间地址区间,为直接内存映射区。
直接映射区的896MB的[线性地址]直接与[物理地址]的前896MB进行映射,也就是说线性地址和分配的物理地址都是连续的。内核地址空间的线性地址0XC0000001所对应的物理地址为0x00000001,它们之间相差一个偏移量PAGE_OFFSET=0XC000000
该区域的线性地址和物理地址存在线性转换关系[线性地址=PAGE_OFFSET+物理地址],也用virt_to_phys()函数将内核虚拟空间中的线性地址转换为物理地址。
动态内存映射区
vmalloc Region
该区域由内核函数vmalloc来分配,特点是:线性空间连续,但是对应的物理地址空间不一定连续。vmalloc分配的线性地址所对应的物理页可能处于低端内存,也可能在高端内存。
永久内存映射区
Persistent Kernel Mapping Region
该区域可访问高端内存,访问方法是使用alloc_page(_GFP HIGHMEM)分配高端内存页或者使用kmap函数将分配待的高端内存映射到该区域。
固定映射区
Fixing kernel Mapping Region
该区域和 4G 的顶端只有 4k 的隔离带,其每个地址项都服务于特定的用途,如 ACPI_BASE
等。
FP HIGHMEM)分配高端内存页或者使用kmap函数将分配待的高端内存映射到该区域。