IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

linux内核研究笔记(一)内存管理 – page介绍

Yunjie Blog 2013-08-15 13:25:23 累计浏览 10,488 次
本机暂存
============ “不负责任”声明 begin ============
咳,首先我是一个平时工作在linux应用层的服务器程序员,对于内核的了解也是皮毛,仅是业余时间中的业余研究的一些笔记,文中的一些观点也许只是我对内核的粗浅认识,大家可千万不要轻易信以为真啊
PS:文中的内核代码默认都是2.6.27.62版本,且环境都按x86 32
============ “不负责任”声明 end ============
内核中最初勾引我好奇心的还是内存管理方面,我们平时编写应用程序时,一个进程所能拥有的内存大小几乎可以趋近于物理内存最大值或是超越这个值,虽然知道内核做内存方面的映射或是swap然后向我们的用户空间呈现出所谓的虚拟内存,但还是对其中实现疑惑甚多,一些关于内存的名词也是有许多,什么虚拟地址,内核线性地址,内核逻辑地址,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 !
                     */
};
内核将物理内存划分为一个个 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(我们现在用户空间用得许多不同的malloc实现,在分配大块内存时都会与4096对齐,其原因应该也是PAGE_SIZE,毕竟这是内核管理内存的最小单元)
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
",
             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;
}
其中会通过PageHighMem函数来判断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)                              // 返回一个页的逻辑地址
分配和释放都牵涉到底层的伙伴算法,那么也放到之后再讲吧~

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. Linux如何统计进程的CPU利用率 (累计阅读 16,308)
  2. 我的 RHCA 之路 (累计阅读 14,013)
  3. Linux内存点滴 用户进程内存空间 (累计阅读 13,230)
  4. 给程序员新手的一些建议 (累计阅读 13,089)
  5. Linux 性能监控、测试、优化工具 (累计阅读 13,011)
  6. 关于linux内存free的一些事情 (累计阅读 12,867)
  7. ps - 按进程消耗内存多少排序 (累计阅读 12,688)
  8. Google怎么用linux (累计阅读 12,581)
  9. Linux Used内存到底哪里去了? (累计阅读 11,867)
  10. find命令的一点注意事项 (累计阅读 11,866)