【推荐阅读】
浅析linux内核网络协议栈--linux bridge
深入理解SR-IOV和IO虚拟化
了解Docker 依赖的linux内核技术
浅谈linux 内核网络 sk_buff 之克隆与复制
深入linux内核架构--进程&线程
内核中最初勾引我好奇心的还是内存管理方面,我们平时编写应用程序时,一个进程所能拥有的内存大小几乎可以趋近于物理内存最大值或是超越这个值,虽然知道内核做内存方面的映射然后向我们的用户空间呈现出所谓的虚拟内存,但还是对其中实现疑惑甚多,而且一些关于内存的名词也是有许多,什么虚拟地址,内核线性地址,内核逻辑地址,balablabla...
屁话不讲了,我们直接来看内核最底层是如何来管理物理内存的。
struct page {
atomic_t _count; /* Usage count, see below. */
atomic_t _mapcount; /* Count of ptes mapped in mms,
* to show when page is mapped
* & limit reverse map searches.
*/
union {
struct {
unsigned long private; /* Mapping-private opaque data:
* usually used for buffer_heads
* if PagePrivate set; used for
* swp_entry_t if PageSwapCache;
* indicates order in the buddy
* system if PG_buddy is set.
*/
struct address_space *mapping; /* If low bit clear, points to
* inode address_space, or NULL.
* If page mapped as anonymous
* memory, low bit is set, and
* it points to anon_vma object:
* see PAGE_MAPPING_ANON below.
*/
};
struct kmem_cache *slab; /* SLUB: Pointer to slab */
struct page *first_page; /* Compound tail pages */
};
struct list_head lru; /* Pageout list, eg. active_list
* protected by zone->lru_lock !
*/
};
【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份 价值699的内核资料包(含视频教程、电子书、实战项目及代码)
内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料
学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂
内核将物理内存划分为一个个 4K or 8K 大小的小块(物理页),而这一个个小块就对应着这个page结构,它是内核管理内存的最小单元
上面的结构体只贴出了部分数据域,其注释内核也写得很清楚了
需要说得是,这个page结构描述的是某片物理页,而不是它包含的数据
不管是内核还是我们用户空间,分配内存时,底层都逃不掉这一个个的page,所以这个page可以作为:
1. 页缓存使用(mapping域指向address_space对象)
这个东西主要是用来对磁盘数据进行缓存,我们平时监控服务器时,经常会用top/free看到cached参数,这个参数其实就是页缓存(page cache),一般如果这个值很大,就说明内核缓冲了许多文件,读IO就会较小
2. 作为私有数据(由private域指向)
可以是作为块冲区中所用,也可以用作swap,当是空闲的page时,那么会被伙伴系统使用。
3. 作为进程页表中的映射
映射到进程页表后,我们用户空间的malloc才能获得这块内存
先来看一下内核中和page相关的一些常量:
include/asm-x86/page.h
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))
可以看出一个page所对应的物理块的大小(PAGE_SIZE)是4096
arch/x86/kernel/e820.c
#ifdef CONFIG_X86_32
# ifdef CONFIG_X86_PAE
# define MAX_ARCH_PFN (1ULL<<(36-PAGE_SHIFT))
# else
# define MAX_ARCH_PFN (1ULL<<(32-PAGE_SHIFT))
# endif
#else /* CONFIG_X86_32 */
# define MAX_ARCH_PFN MAXMEM>>PAGE_SHIFT
#endif
内核会将所有struct page* 放到一个全局数组(mem_map)中,而内核中我们常会看到pfn,说得就是页帧号,也就是数组的index,这里的MAX_ARCH_PFN就是系统的最大页帧号,但这个只是理论上的最大值,在start_kernel()时,setup_arch()函数会通过e820_end_of_ram_pfn()函数来获得实际物理内存并返回最终的max_pfn,可以看下e820_end_of_ram_pfn的实现(其内部直接调用e820_end_pfn函数)
/*
* Find the highest page frame number we have available
*/
static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type)
{
int i;
unsigned long last_pfn = 0;
unsigned long max_arch_pfn = MAX_ARCH_PFN;
for (i = 0; i < e820.nr_map; i++) {
struct e820entry *ei = &e820.map[i];
unsigned long start_pfn;
unsigned long end_pfn;
if (ei->type != type)
continue;
start_pfn = ei->addr >> PAGE_SHIFT;
end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;
if (start_pfn >= limit_pfn)
continue;
if (end_pfn > limit_pfn) {
last_pfn = limit_pfn;
break;
}
if (end_pfn > last_pfn)
last_pfn = end_pfn;
}
if (last_pfn > max_arch_pfn)
last_pfn = max_arch_pfn;
printk(KERN_INFO "last_pfn = %#lx max_arch_pfn = %#lx\n",
last_pfn, max_arch_pfn);
return last_pfn;
}
从上面的宏定义还可以看到
在x86_32时,内核会看是否启用PAE,PAE会比没有PAE所拥有的page更多(也即是说能访问更多的物理内存),PAE是一种物理地址扩展技术,让你在32位的系统中能访问超越4G的空间,其技术实现还是通过局部地址的映射,这里不展开说
接着来看下page结构的相关宏/函数:
pfn_to_page/page_to_pfn - 这两个底层使用 __pfn_to_page/__page_to_pfn宏,它们的作用是struct page* 和 前面提到的pfn页帧号之间的转换,看下实现
__pfn_to_page:(mem_map + ((pfn) - ARCH_PFN_OFFSET))
__page_to_pfn:((unsigned long)((page) - mem_map) + ARCH_PFN_OFFSET)
就是简单地和mem_map进行加减操作(最后那个OFFSET可以无视,默认0),由于mem_map也是struct page*类型,所以相加减就能得到对应的pfn(数组index)和对应的struct page*,如图
#define phys_to_page(phys) (pfn_to_page(phys >> PAGE_SHIFT))
#define page_to_phys(page) (page_to_pfn(page) << PAGE_SHIFT)
这两个宏的功能分别是将struct page*和物理地址之间进行转换
例如page_to_phys, 通过page_to_pfn宏取得相应的pfn后,还记得PAGE_SHIFT吗,假设pfn是1,左移12位,就是4096,也就是第二个对应的物理页的位置,这样就取得了物理地址(虽然内核在虚拟地址中是在高地址的,但是在物理地址中是从0开始的,所以这里也是从0开始)
#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)
#define page_to_virt(page) __va(page_to_pfn(page) << PAGE_SHIFT)
这两个宏的作用是在struct page*和内核逻辑/线性地址 之间做转换
这里要补几个概念性的问题 -
内核逻辑/线性地址:其实对于linux内核来说,这个地址等同于物理地址,只是它们之间有一个固定的偏移量,linux内核中常提到的逻辑地址和线性地址其实是同一个东西
内核虚拟地址:与上面的内核逻辑地址的区别在于,内核虚拟地址不一定是在硬件物理上是连续的,有可能是通过分页映射的不连续的物理地址
这里的virt指得就是逻辑/线性地址,而不是真正的virtual地址
继续看__pa和__va宏
#define __pa(x) ((unsigned long) (x) - PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long) (x) + PAGE_OFFSET))
可以看到它们只是做了一个偏移量(PAGE_OFFSET),在x86_32中,这个PAGE_OFFSET是0xC0000000,为什么是这个值呢,因为32位系统中,内核的虚拟地址只有1G,这个之后具体讲内存布局的时候再讨论
还有一个常用的宏/函数是page_address,它特殊的地方在于,以上的那些宏针对的或是返回的都是内核逻辑地址,也就是说是做简单的偏移加减,但是在32位系统中有个high_mem的概念 - 高端内存,它的作用让内核如何访问超出32位范围的内存,方法就是利用某一小块固定的内存做映射(这里的HighMem我个人认为就是前面提到的PAE技术的一种实现,以后讨论)
所以一个page对应的虚拟地址,有可能是直接做物理偏移的地址(也就是以上几个宏可以直接应用的),还有就是被高端内存映射的
针对后者,以上的几个宏是无法得到page的虚拟地址的,只有应用到page_address函数
我们看下page_address的实现:
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
if (!PageHighMem(page))
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) {
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
标红的地方会判断page是否是HighMem,如果不是,直接调用lowmem_page_address,这个函数内部实现就是page_to_virt,所以就是简单地做偏移了,关于HighMem的映射之后再讨论了
以上就是内核常用的几个page转换的宏/函数,最后咱们简单看下page的分配接口(释放的我懒得一一匹配写了)
返回page结构的:
struct page * alloc_pages(gfp_mask, order) // 分配 1<<order 个连续的物理页
struct page * alloc_page(gfp_mask) // 分配一个物理页
返回page对应的逻辑地址的:
__get_free_pages(gfp_mask, order) // 和alloc_pages一样,只不过返回的是第一个页的内核逻辑地址
__get_free_page(gfp_mask) // 返回一个页的逻辑地址