Linux内存管理中的术语、变量和小函数

来源:互联网 发布:lol限定皮肤淘宝店铺 编辑:程序博客网 时间:2024/05/20 11:22

最近学了点内存管理基本概念,仅以此文做个记录。

术语

PGD, PUD, PMD, PTE

PGD: Page Global Directory
PUD: Page Upper Directory
PMD: Page Middle-level Directory
PTE: Page Table Entry

这么看其实非常枯燥,请看下文中pgd_index, pud_index这小节。

变量/宏

__START_KERNEL_map,内核虚拟地址的起始地址

定义很简单,但这就是内核虚拟地址的启示位子。

#define __START_KERNEL_map  _AC(0xffffffff80000000, UL)

好,我们来看看为什么。

先看链接脚本: arch/x86/kernel/vmlinux.lds.S

#define __START_KERNEL_map  _AC(0xffffffff80000000, UL)#define __START_KERNEL      (__START_KERNEL_map + __PHYSICAL_START)SECTIONS{    . = __START_KERNEL;    phys_startup_64 = ABSOLUTE(startup_64 - LOAD_OFFSET);    /* Text and read-only data */    .text :  AT(ADDR(.text) - LOAD_OFFSET) {        _text = .;

这里只看x86_64的。可以看到 .text, _text的虚拟地址就是__START_KERNEL,如果__PHYSICAL_START默认为0x1000000, 那么 这个 地址 就是
__START_KERNEL_map + 0x1000000
= 0xffffffff81000000。

ok,那怎么证明呢? 有几个证据。

证明一, readelf

第一个就是看elf的program header。

readelf -l vmlinux

Elf file type is EXEC (Executable file)Entry point 0x1000000There are 5 program headers, starting at offset 64Program Headers:  Type           Offset             VirtAddr           PhysAddr                 FileSiz            MemSiz              Flags  Align  LOAD           0x0000000000200000 0xffffffff81000000 0x0000000001000000                 0x0000000000da0000 0x0000000000da0000  R E    200000  LOAD           0x0000000001000000 0xffffffff81e00000 0x0000000001e00000                 0x0000000000143000 0x0000000000143000  RW     200000  LOAD           0x0000000001200000 0x0000000000000000 0x0000000001f43000                 0x0000000000019018 0x0000000000019018  RW     200000  LOAD           0x000000000135d000 0xffffffff81f5d000 0x0000000001f5d000                 0x000000000016c000 0x00000000002ef000  RWE    200000  NOTE           0x0000000000a38e08 0xffffffff81838e08 0x0000000001838e08                 0x0000000000000204 0x0000000000000204         4

看第一个program header的虚拟地址是0xffffffff81000000, 是不是就是它了呢。

证明二,打印符号 _text

第二个证明,干脆在内核中打印_text这个符号的地址呗。

恩,请自行打印,就不在这里写了。

证明三, 计算 level2_kernel_pgt的index

第三个证明, 查看页表。

在head_64.S设置的页表 和 尝试打印页表 中, 我们可以看到 level2_kernel_pgt 是 从 pgd[511] 和 pud[510] 指过来的。 好了, 那我们分析一下 __START_KERNEL_map。

  __START_KERNEL_map= 0x ffff       ffff                 8000             0000= 0x ffff       ffff                 8            000 0000= 0x ffff  (1111 1111     1111 1111 1000)b        000 0000= 0x ffff  (1111 11111)b  (111 1111 10)b  (00)b   000 0000= 0x ffff   511            510            (00)b   000 0000

写得有点丑,大家将就看一下。低48bit中的高9位是pgd的index,接下来的9位是pud的index。上面把这两个域截出来了,正好是 511 和 510。完美~

__PAGE_OFFSET,整个虚拟地址的起始地址

恩,这个是我猜的,暂时还不确认哈

/* * Set __PAGE_OFFSET to the most negative possible address + * PGDIR_SIZE*16 (pgd slot 272).  The gap is to allow a space for a * hypervisor to fit.  Choosing 16 slots here is arbitrary, but it's * what Xen requires. */#define __PAGE_OFFSET           _AC(0xffff880000000000, UL)

这高级玩意还真有点难懂,理解要是不对,欢迎大家拍砖。

  0x ffff 0000 0000 0000       A  0x 0000 8000 0000 0000       B+ 0x 0000 0800 0000 0000       C= 0x ffff 8800 0000 0000       __PAGE_OFFSET

A 是 现在页表支持最大的内存空间。因为现在我看,一个页表最多也就能表示48位的地址空间了。(init_level4_pgt就是这种页表。)

B 是 most nagetive possible address。 这个想了一会儿发现还真的是的。这个使用补码表示,最高位留作符号位。最高位取1, 其余为0时,就是这么多位能表示的最小的负数。

C 是 注释中说的那个16个PGDIR_SIZE。 因为 (C >> 39) = 16。

看到这里有点没有想明白,为啥这个地址还是个有符号数呢?

小函数

pgd_index(), pud_index(), pmd_index()

这几个函数或者宏是用来遍历页表的

/* * the pgd page can be thought of an array like this: pgd_t[PTRS_PER_PGD] * * this macro returns the index of the entry in the pgd page which would * control the given virtual address */#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))/* to find an entry in a page-table-directory. */static inline unsigned long pud_index(unsigned long address){    return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);}/* * the pmd page can be thought of an array like this: pmd_t[PTRS_PER_PMD] * * this macro returns the index of the entry in the pmd page which would * control the given virtual address */static inline unsigned long pmd_index(unsigned long address){    return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1);}

仅仅看这个或许看不出来,我们来看一个图,再来做个实验。

把下面这段代码放在某个内核模块中。

    printk(KERN_ERR "pgd_index(%s) is %lu\n", "__START_KERNEL_map", pgd_index(__START_KERNEL_map));    printk(KERN_ERR "pud_index(%s) is %lu\n", "__START_KERNEL_map", pud_index(__START_KERNEL_map));    printk(KERN_ERR "pmd_index(%s) is %lu\n", "__START_KERNEL_map", pmd_index(__START_KERNEL_map));    printk(KERN_ERR "pgd_index(%s) is %lu\n", "__START_KERNEL", pgd_index(__START_KERNEL));    printk(KERN_ERR "pud_index(%s) is %lu\n", "__START_KERNEL", pud_index(__START_KERNEL));    printk(KERN_ERR "pmd_index(%s) is %lu\n", "__START_KERNEL", pmd_index(__START_KERNEL));

在log文件中会有,

 [  612.844418] pgd_index(__START_KERNEL_map) is 511 [  612.844420] pud_index(__START_KERNEL_map) is 510 [  612.844422] pmd_index(__START_KERNEL_map) is 0 [  612.844424] pgd_index(__START_KERNEL) is 511 [  612.844426] pud_index(__START_KERNEL) is 510 [  612.844428] pmd_index(__START_KERNEL) is 8

你猜到了什么? 那来看这张图吧~
pgd_index

__START_KERNEL和__START_KERNEL_map算出的pgd_index和pud_index是一样的。
而__START_KERNEL的pmd_index算出来是8。每个页是2M,那就是对应16M。

16M = 0x1000000

__PHYSICAL_START 正好等于 0x1000000。

完美~

__pa()

计算出虚拟地址的物理地址。

这个函数在x86_64平台上展开后,实际上是这个函数。

static inline unsigned long __phys_addr_nodebug(unsigned long x){    unsigned long y = x - __START_KERNEL_map;    /* use the carry flag to determine if x was < __START_KERNEL_map */    x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));    return x;}

如果把上面的函数展开,可以得到

    if (x < __START_KERNEL_map)        x = x - PAGE_OFFSET    else        x = x - __START_KERNEL_map + phys_base;

从地址映射的角度看,内核空间分成了两块

[0xffff8000 00000000] – [0xfffffffff 7fffffff]
[0xffffffff 80000000] – [0xffffffff ffffffff]

这第二块就是内核本身的地址映射。

所以当虚拟地址映射到物理地址时,针对虚拟地址所属的空间会有不同映射方式。

__va()

计算物理地址对应的虚拟地址

#ifndef __va#define __va(x)         ((void *)((unsigned long)(x)+PAGE_OFFSET))#endif

这个倒是简单的。但是有一个有意思的东西

    pr_err(":               (__START_KERNEL_map)   = %lx \n", __START_KERNEL_map);    pr_err(":           __pa(__START_KERNEL_map)   = %lx \n", __pa(__START_KERNEL_map));    pr_err(":      __va(__pa(__START_KERNEL_map))  = %lx \n",            (unsigned long)__va(__pa(__START_KERNEL_map)));    pr_err(": __pa(__va(__pa(__START_KERNEL_map))) = %lx \n",            __pa((unsigned long)__va(__pa(__START_KERNEL_map))));

你猜,会看到什么?

[    0.000000] :               (__START_KERNEL_map)   = ffffffff80000000 [    0.000000] :           __pa(__START_KERNEL_map)   = 0 [    0.000000] :      __va(__pa(__START_KERNEL_map))  = ffff880000000000 [    0.000000] : __pa(__va(__pa(__START_KERNEL_map))) = 0 

是的,对于同一个物理地址可以有两个虚拟地址与之对应。不知道怎么算会不会有什么隐患?

遍历页表

整个页表有好几层,在分配页表的过程中需要从指定的cr3地址出发,找到相应的页表项,填入相应的值。在内核中就有相应的函数来帮助遍历页表,找到相应的位置。

这些函数是
* pgd_val()/pgd_page_vaddr()
* pud_val()/pud_page_vaddr()
* pmd_val()/pmd_page_vaddr()

这些函数两两一对,前者是获取保存的原始值。而后者则是转换成下一层的虚拟地址以便从内核访问。

单纯这么看略有枯燥,看一个实际的代码例子。

diff --git a/arch/x86/mm/init.c b/arch/x86/mm/init.cindex 22af912..233a4d8 100644--- a/arch/x86/mm/init.c+++ b/arch/x86/mm/init.c@@ -589,6 +595,16 @@ static void __init memory_map_bottom_up(unsigned long map_start, void __init init_mem_mapping(void) {    unsigned long end;+   int i, j, k;+   pud_t *pud;+   pmd_t *pmd;+    probe_page_size_mask();@@ -624,6 +642,34 @@ void __init init_mem_mapping(void)        memory_map_top_down(ISA_END_ADDRESS, end);    }+   pr_err("ywtest: after memory mapped\n");+   for (i = 0; i < 512; i++) {+       if (pgd_val(init_level4_pgt[i])) {+           pr_err("ywtest: init_level4_pgt[%d] 0x%lx\n",+               i, pgd_val(init_level4_pgt[i]));++           pud = (pud_t *)pgd_page_vaddr(init_level4_pgt[i]);++           for(j = 0; j < 512; j++) {+               if (!pud_none(pud[j])) {+                   pr_err("ywtest: \t pud[%d] = %lx\n",+                       j, pud_val(pud[j]));+               }++           if ((i == 272 && j == 0) +                || (i == 511 && j == 510)) {+               pmd = (pmd_t *)pud_page_vaddr(pud[j]);+               pr_err("ywtest: \t pgd[%d][%d] vaddr = %p\n",+                       i, j, pmd);+               for(k = 0; k < 12; k++) {+                   pr_err("ywtest: \t\t pmd[%d] = %lx, flags %lx\n",+                       k, pmd_val(pmd[k]), pmd_flags(pmd[k]));+               }+           }+           }+       }+   }+ #ifdef CONFIG_X86_64    if (max_pfn > max_low_pfn) {        /* can we preseve max_low_pfn ?*/

这样就可以打印出内核页表的样子了~ 暂时还有点小问题,后面再来改正~

__pfn_to_page()/__page_to_pfn()

这两个函数用来做page frame number和page结构体之间的映射。

因为内核中内存模型有好几种,所以必须分开看。

SPARSEMEM

先来一张图看看样子。

    mem_section                         +-----------------------------+     |pageblock_flags              |     |   (unsigned long *)         |     |                             |     |                             |     +-----------------------------+         mem_map[PAGES_PER_SECTION]    |section_mem_map              |  ---->  +------------------------+    |   (unsigned long)           |    [0]  |struct page             |    |                             |         |                        |    |                             |         +------------------------+    +-----------------------------+    [1]  |struct page             |                                            |                        |                                            +------------------------+                                       [2]  |struct page             |                                            |                        |                                            +------------------------+                                            |                        |                                            .                        .                                            .                        .                                            .                        .                                            |                        |                                            +------------------------+                                            |struct page             |                                            |                        |                                            +------------------------+                   [PAGES_PER_SECTION - 1]  |struct page             |                                            |                        |                                            +------------------------+

也就是在SPARSEMEM模型下,page结构体存放在mem_section结构体中。

好了,可以看代码了。

先看这个section_mem_map保存了什么

static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum){    return (unsigned long)(mem_map - (section_nr_to_pfn(pnum)));}static int __meminit sparse_init_one_section(struct mem_section *ms,        unsigned long pnum, struct page *mem_map,        unsigned long *pageblock_bitmap){    ...    ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum) |                            SECTION_HAS_MEM_MAP;    ...}

嗯,这个很有意思。section_mem_map存放了

page结构体的内存地址 - 这个section对应的第一个pfn。

嗯,为什么要这么写呢? 来看一下 __pfn_to_page()的定义你就知道了。

#define __pfn_to_page(pfn)              \({  unsigned long __pfn = (pfn);            \    struct mem_section *__sec = __pfn_to_section(__pfn);    \    __section_mem_map_addr(__sec) + __pfn;      \})

如果我们把这个宏展开,稍微做点变换可以得到:

    (struct page*) section_mem_map +     (__pfn - __section_start_pfn)

怎么样,这次看清楚了么?嗯,确实有点难表达。

好了,现在可以来看看__page_to_pfn()的定义了。

#define __page_to_pfn(pg)                   \({  const struct page *__pg = (pg);             \    int __sec = page_to_section(__pg);          \    (unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \})

那这个再转换一下就是

    __section_start_pfn +    (pg - __section_start_pg)

good~

virt_to_page()

有了前面几个概念,这个转换就相对容易了。

#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)

其实很简单

  • 虚拟地址转换成物理地址
  • 再把pfn转换成page地址

manipulate page->flags

page->flags隐藏了不少bit用来表示不同的含义。内核为了统一,对这个域的操作做了不少封装。

基本上看到的定义在page-flags.h。

具体展开后使用形式为

SetPagePrivate(page);

gfp_zone() 获取目标页面的最高zone index

这个函数虽然用到的地方不多,但是很关键。比如在__alloc_pages_nodemask()中会被使用到。所以可见,每次的内存分配,都会执行到这个函数。

那这个函数是用来干什么的呢?

用来获取gfp_mask对应的最高zone index

比如说我们都见过GFP_KERNEL是吧,当然这个mask的意义细节包含几个层面,那其中一个层面就是我们能够在哪一个zone上分配内存?这个mask或许还不是很清晰,那来看看这个GFP_DMA。这个参数实际指定了只能在DMA zone上分配内存,如果分配到了其他zone上根本不能用。

好了,这个函数的意义了解了。那来看看是如何实现的。

static inline enum zone_type gfp_zone(gfp_t flags){    enum zone_type z;    int bit = (__force int) (flags & GFP_ZONEMASK);    z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &                     ((1 << GFP_ZONES_SHIFT) - 1);    VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);    return z;}

虽然很短,但是很精炼,精炼到根本看不懂。。。

嗯,那我们一步步分开来看。

截取mask中zone相关的部分

    int bit = (__force int) (flags & GFP_ZONEMASK);

这个还比较简单,就是截取传入参数和zone相关的bit。那来看一下定义。

#define __GFP_DMA   ((__force gfp_t)___GFP_DMA)#define __GFP_HIGHMEM   ((__force gfp_t)___GFP_HIGHMEM)#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)#define __GFP_MOVABLE   ((__force gfp_t)___GFP_MOVABLE)  /* ZONE_MOVABLE allowed */#define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

所以在分配的时候,我们只看这四个zone。再细心的童鞋可以看到,这四个bit就是flags的最低四位。

保证zone mask不是非法的

一共有四个bit来表示zone的可选范围,那一共就是2^4种组合。但是这16个组合中,不是所有的情况都是合法的。

这一点在注释中就说的很明确了。

/* * GFP_ZONE_TABLE is a word size bitstring that is used for looking up the * zone to use given the lowest 4 bits of gfp_t. Entries are ZONE_SHIFT long * and there are 16 of them to cover all possible combinations of * __GFP_DMA, __GFP_DMA32, __GFP_MOVABLE and __GFP_HIGHMEM. * * The zone fallback order is MOVABLE=>HIGHMEM=>NORMAL=>DMA32=>DMA. * But GFP_MOVABLE is not only a zone specifier but also an allocation * policy. Therefore __GFP_MOVABLE plus another zone selector is valid. * Only 1 bit of the lowest 3 bits (DMA,DMA32,HIGHMEM) can be set to "1". * *       bit       result *       ================= *       0x0    => NORMAL *       0x1    => DMA or NORMAL *       0x2    => HIGHMEM or NORMAL *       0x3    => BAD (DMA+HIGHMEM) *       0x4    => DMA32 or DMA or NORMAL *       0x5    => BAD (DMA+DMA32) *       0x6    => BAD (HIGHMEM+DMA32) *       0x7    => BAD (HIGHMEM+DMA32+DMA) *       0x8    => NORMAL (MOVABLE+0) *       0x9    => DMA or NORMAL (MOVABLE+DMA) *       0xa    => MOVABLE (Movable is valid only if HIGHMEM is set too) *       0xb    => BAD (MOVABLE+HIGHMEM+DMA) *       0xc    => DMA32 (MOVABLE+DMA32) *       0xd    => BAD (MOVABLE+DMA32+DMA) *       0xe    => BAD (MOVABLE+DMA32+HIGHMEM) *       0xf    => BAD (MOVABLE+DMA32+HIGHMEM+DMA) * * GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms. */

从0到f,一共16种组合都列出来了。一共可以分成两半,前八个和后八个是对称的。

虽然这个注释是在讲GFP_ZONE_TABLE,但是我却要先来讲讲函数中的这句

    VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);

这条语句就是用来保证传入参数中zone的bits是不是有非法的置位。

先来看一下GFP_ZONE_BAD的定义。

#define GFP_ZONE_BAD ( \    1 << (___GFP_DMA | ___GFP_HIGHMEM)                    \    | 1 << (___GFP_DMA | ___GFP_DMA32)                    \    | 1 << (___GFP_DMA32 | ___GFP_HIGHMEM)                    \    | 1 << (___GFP_DMA | ___GFP_DMA32 | ___GFP_HIGHMEM)           \    | 1 << (___GFP_MOVABLE | ___GFP_HIGHMEM | ___GFP_DMA)             \    | 1 << (___GFP_MOVABLE | ___GFP_DMA32 | ___GFP_DMA)           \    | 1 << (___GFP_MOVABLE | ___GFP_DMA32 | ___GFP_HIGHMEM)           \    | 1 << (___GFP_MOVABLE | ___GFP_DMA32 | ___GFP_DMA | ___GFP_HIGHMEM)  \)

仔细看,其中每一条是不是和注释当中的BAD条目是对应的?这样就可以得到一个非法zone index和一个bitmap的对应。这也就是为什么能够在gfp_zone()函数中通过位移判断是否有非法的参数。

计算出允许的最高zone index

当然之前的都是准备和检查工作,真正的计算还是在这里。

    z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &                     ((1 << GFP_ZONES_SHIFT) - 1);

嗯,确实比较难懂,当然了,如果你一眼就看懂了,那说明你是高手!下次来教我~

这个东西怎么看呢? 或许我画一张图可能你就明白了。

  [0]       [1]                 [14]     [15]  +-----------+-----------+         +----------+-----------+  |ZONE_NORMAL|ZONE_DMA   |  .....  |          |           |  +-----------+-----------+         +----------+-----------+

也就是说GFP_ZONE_TABLE被分成了16分,每个是2^GFP_ZONE_SHIFT大小。而其中的内容就是保存了对应gf_mask的允许最大zone index。

嗯,有点绕,但真的就是这个意思了!其实是一个最简单的hash table。。。

0 0
原创粉丝点击