存档在 ‘Linux内核源码分析’ 分类

vmalloc()的实现-分散的物理页框与连续vmalloc区的映射

2011年11月22日

在vmalloc()的实现过程中,首先遍历vmlist链表找到一个所需大小的vmalloc区,接着为这个子内存区依次分配物理页框。从这两部分的实现过程可以看到,虚拟的子内存区是在整个vmalloc区中查找一块合适大小的子区域,而物理内存则是分散的依次分配。因此,vmalloc()的实现就落在了如何将这些分散的物理页框映射到连续的vmalloc区上。整个映射过程可以简单的看作是不断修改内核页表的过程。

map_vm_area()的实现

vmalloc()通过map_vm_area()完成物理页框与虚拟子内存区的映射。map_vm_area()首先计算虚拟内存子区域的起始地址addr和终止地址end。由于vm_struct中的size是实际子区间长度加上一个页大小,因此终止地址必须减去PAGE_SIZE。接着它调用了vmap_page_range()。下面的代码表明了map_vm_area()的实际调用过程(本文所有内核源码均取自v2.6.34)。

int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)
{
        unsigned long addr = (unsigned long)area->addr;
        unsigned long end = addr + area->size - PAGE_SIZE;
        int err;

        err = vmap_page_range(addr, end, prot, *pages);
        if (err > 0) {
                *pages += err;
                err = 0;
        }

        return err;
}

vmap_page_range()又再次封装了真正用于映射的函数vmap_page_range_noflush()。当映射完毕后,它调用flush_cache_vmap(),即将刚刚修改的内核页表项刷新到CPU高速缓存。

static int vmap_page_range(unsigned long start, unsigned long end,
                           pgprot_t prot, struct page **pages)
{
        int ret;

        ret = vmap_page_range_noflush(start, end, prot, pages);
        flush_cache_vmap(start, end);
        return ret;
}

修改内核页表

map_vm_area()经过重重调用,终于来到了进行实际映射工作的vmap_page_range_noflush()中。这个函数中首先通过pgd_offset_k()计算出addr在主内核页全局目录中对应的页表项地址。接着通过一个循环,为start到end之间的子内存区修改内核页表。 在每次循环的过程中,next为当前页表项所映射的内存区的终止地址。通过vmap_pud_range()继续修改start到next之间所对应的页上级目录。

static int vmap_page_range_noflush(unsigned long start, unsigned long end,
                                   pgprot_t prot, struct page **pages)
{
        pgd_t *pgd;
        unsigned long next;
        unsigned long addr = start;
        int err = 0;
        int nr = 0;

        BUG_ON(addr >= end);
        pgd = pgd_offset_k(addr);
        do {
                next = pgd_addr_end(addr, end);
                err = vmap_pud_range(pgd, addr, next, prot, pages, &nr);
                if (err)
                        return err;
        } while (pgd++, addr = next, addr != end);

        return nr;
}

vmap_pud_range()所做的工作和vmap_page_range_noflush()类似,只不过它是针对页上级目录的页表项做出对应的修改。首先pud_alloc()为addr分配对应的页上级目录,同时也将该页上级目录对应的物理地址写入页全局目录对应的表项中。接着再次进入一个循环来依次修改addr对应的页中间目录,每次循环时计算出当前页上级目录所映射的内存区间范围addr和next。

static int vmap_pud_range(pgd_t *pgd, unsigned long addr,
                unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
        pud_t *pud;
        unsigned long next;

        pud = pud_alloc(&init_mm, pgd, addr);
        if (!pud)
                return -ENOMEM;
        do {
                next = pud_addr_end(addr, end);
                if (vmap_pmd_range(pud, addr, next, prot, pages, nr))
                        return -ENOMEM;
        } while (pud++, addr = next, addr != end);
        return 0;
}

vmap_pmd_range()为页中件目录所指向的所有页表执行和上述类似的循环。

static int vmap_pmd_range(pud_t *pud, unsigned long addr,
                unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
        pmd_t *pmd;
        unsigned long next;

        pmd = pmd_alloc(&init_mm, pud, addr);
        if (!pmd)
                return -ENOMEM;
        do {
                next = pmd_addr_end(addr, end);
                if (vmap_pte_range(pmd, addr, next, prot, pages, nr))
                        return -ENOMEM;
        } while (pmd++, addr = next, addr != end);
        return 0;
}

现在为addr更新最后一级页表,首先由pte_alloc_kernel()生成页表pte,再通过循环依次更新每个页表项。在每次循环过程中,先通过pages数组得到第nr个页框的页描述符page,再将其传入mk_pte生成对应页表项。最后由set_pte_at()将此页表项更新到pte所指的页表的对应项中。页表项的增加过程由当前线性地址addr加上PAGE_SIZE完成。

static int vmap_pte_range(pmd_t *pmd, unsigned long addr,
                unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
        pte_t *pte;

        pte = pte_alloc_kernel(pmd, addr);
        if (!pte)
                return -ENOMEM;
        do {
                struct page *page = pages[*nr];

                if (WARN_ON(!pte_none(*pte)))
                        return -EBUSY;
                if (WARN_ON(!page))
                        return -ENOMEM;
                set_pte_at(&init_mm, addr, pte, mk_pte(page, prot));
                (*nr)++;
        } while (pte++, addr += PAGE_SIZE, addr != end);
        return 0;
}

经过对每级页表的层层修改,最终start到end之间的连续vmalloc区都与相应的物理页框相映射。整个vmalloc()完成。

vmalloc()的基本实现

2011年11月21日

每个进程都拥有一段连续而且平坦的虚拟地址空间,这段连续的空间被划分为两大部分:用户空间和内核空间。在x86-32架构下,用户空间占据最低端的3G,内核空间占据最高的1G。事实上,每个进程并不会同时使用掉整个3G的地址空间,因此整个用户空间又进一步被划分为若干个虚拟内存区域(struct vm_area_struct),每个内存区域都有相应的访问权限,并且针对当前的内存区域还有具体的操作函数。

对于内核空间而言,根据不同的映射规则,整个内核空间划分为四大部分:物理内存映射区、vmalloc区、永久内核映射区和固定映射的线性地址区域。内核空间的映射情况如下图所示:

其中vmalloc区(struct vm_struct)跟用户空间的虚拟内存区域有些类似,它们都是利用分散的物理页框构建连续的虚拟地址区间。

非连续内存区的数据结构

vmalloc区也被称为非连续内存区域,整个非连续内存区的起始地址定义为VMALLOC_START宏,结束地址定义为VMALLOC_END宏。它由若干个vmalloc区组成,每个vmalloc区之间间隔4KB,这是为了防止非法的内存访问。内核中使用vm_struct结构来表示每个vmalloc区,也就是说,每次调用vmalloc()函数在内核中申请一段连续的内存后,都对应着一个vm_struct,系统中所有的vmalloc区组成一个链表,链表头指针为vmlist。vm_sttruct结构在最新内核源码的描述如下(本文所涉及的内核源码均来自v3.0.4):

struct vm_struct {
        struct vm_struct        *next;
        void                    *addr;
        unsigned long           size;
        unsigned long           flags;
        struct page             **pages;
        unsigned int            nr_pages;
        unsigned long           phys_addr;
        void                    *caller;
};

下面是这个结构中各个字段的解释:

next:所有的vm_struct结构组成一个vmlist链表,该字段指向下一个节点;

addr:vmalloc()最终是在内核空间中申请一个内存区域,addr代表这段子区域的起始地址;

size:表示子区域的大小;

flags:表示该非连续内存区的类型,VM_ALLOC表示由vmalloc()映射的内存区,VM_MAP表示通过vmap()映射的内存区,VM_IOREMAP表示通过ioremap()将硬件设备的内存映射到内核的一段内存区;

pages:指针数组,该数组的成员是struct page*类型的指针,每个成员都关联一个映射到该虚拟内存区的物理页框;

nr_pages:pages数组中page结构的总数;

phys_addr:通常为0,当使用ioremap()映射一个硬件设备的物理内存时才填充此字段;

caller:表示一个返回地址;

vmalloc()的实现

vmalloc()内部封装了__vmalloc_node(),该函数的原型和调用如下代码所示。其中,size表示要分配子内存区的大小,它通过vmalloc()传递过来的;align表示将所申请长度的内存区分为几部分,1表示将size大小的虚拟内存区作为一个整体;gfp_mask描述页面分配的标志,GFP_KERNEL|__GFP_HIGHMEM表明内存管理子系统将从高端内存区(ZONE_HIGHMEM)中分配内存空间;prot描述当前页的保护标志;node表示在哪个节点(struct pg_data_t)上为这段子内存区分配空间,-1表明在当前节点中分配;caller表示该函数的返回地址。

static void *__vmalloc_node(unsigned long size, unsigned long align,
                            gfp_t gfp_mask, pgprot_t prot,
                            int node, void *caller)
void *vmalloc(unsigned long size)
{
        return __vmalloc_node(size, 1, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL,
                                        -1, __builtin_return_address(0));
}

__vmalloc_node函数的主要功能分为两部分:

1.在非连续内存区的起始和终止地址之间查找一个空闲的内存区域,这部分由__get_vm_area_node()完成。

2.为该子内存区分配物理页框,并将分散的物理页框分别映射到连续的vmalloc区中,这部分由__vmalloc_area_node()完成。

__vmalloc_node()一开始会先修正一下自内存取的大小,PAGE_ALIGN将size的大小修改成页大小的倍数。假如要申请1KB的内存区,那么事实上分配的是4KB大小(一个页大小)的区域。接着进行size合法性的检查,如果size为0,或者size所占页框数大于系统当前空闲的页框数(totalram_pages),将返回NULL,既申请失败。

如果子内存区大小合法,__get_vm_area_node()将在整个非连续内存区中查找一个size大小的子内存区。该函数先遍历整个vmlist链表,依次比对每个vmalloc区,直到找到满足要求的子内存区为止。接着为这个子内存区建立一个vm_struct结构,再将这个结构插入到整个vmlist链表中。该函数的详细实现过程本文不做分析。

static void *__vmalloc_node(unsigned long size, unsigned long align,
                            gfp_t gfp_mask, pgprot_t prot,
                            int node, void *caller)
{
        struct vm_struct *area;
        void *addr;
        unsigned long real_size = size;

        size = PAGE_ALIGN(size);
        if (!size || (size >> PAGE_SHIFT) > totalram_pages)
                return NULL;

        area = __get_vm_area_node(size, align, VM_ALLOC, VMALLOC_START,
                                  VMALLOC_END, node, gfp_mask, caller);

        if (!area)
                return NULL;

        addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller);

        kmemleak_alloc(addr, real_size, 3, gfp_mask);

        return addr;
}

__vmalloc_area_node()的实现

当__get_vm_area_node()创建了一个新的vm_struct结构后,接下来就要通过__vmalloc_area_node()为这个子内存区分配真正的物理页。
首先计算通过右移PAGE_SHIFT位来计算nr_pages,它表示这个子内存区映射的页数。接着根据子内存区所映射的页框数计算pages数组的大小,这个数组的元素为struct page*型,每个元素都指向一个用来描述物理页框的page结构。

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                                 pgprot_t prot, int node, void *caller)
{
        struct page **pages;
        unsigned int nr_pages, array_size, i;
        gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;

        nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT;
        array_size = (nr_pages * sizeof(struct page *));

        area->nr_pages = nr_pages;

接着,__vmalloc_area_node()为页描述符指针数组分配空间。如果这个指针数组的大小超过一个页的大小,那么递归调用__vmalloc_node()为其分配空间,也就是说pages数组本身就采用vmalloc区来存储;否则,通过kmalloc_node()为pages数组分配一段连续的空间,这段空间既位于内核空间的物理内存线性映射区。

接下来用刚才的局部变量pages更新area中的pages。如果pages数组分配失败,则调用remove_vm_area()将__get_vm_area_node()的到的vm_struct结构从vmlist中移除,并返回NULL,表示vmalloc()申请失败。

        if (array_size > PAGE_SIZE) {
                pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                                PAGE_KERNEL, node, caller);
                area->flags |= VM_VPAGES;
        } else {
                pages = kmalloc_node(array_size, nested_gfp, node);
        }
        area->pages = pages;
        area->caller = caller;
        if (!area->pages) {
                remove_vm_area(area->addr);
                kfree(area);
                return NULL;
        }

现在到了最关键的时刻,通过一个循环依次为pages数组中的每个页面描述符分配真正的物理页框。需要注意的是page结构并不是代表一个具体的物理页框,只是用来描述物理页框的数据结构而已。如果node小于0,也就是未指定物理内存所在节点,那么使用alloc_page()分配一个页框,并将该页框对应的页描述符指针赋值给page临时变量;否则通过alloc_pages_node()在指定的节点上分配物理页框。接着将刚刚分配的物理页框对应的页描述符赋值给pages数组的第i个元素。一旦某个物理页框分配失败则直接返回NULL,表示本次vmalloc()操作失败。

        for (i = 0; i < area->nr_pages; i++) {
                struct page *page;
                if (node < 0)
                        page = alloc_page(gfp_mask);
                else
                        page = alloc_pages_node(node, gfp_mask, 0);

                if (unlikely(!page)) {
                        area->nr_pages = i;
                        goto fail;
                }
                area->pages[i] = page;
        }

        if (map_vm_area(area, prot, &pages))
                goto fail;
        return area->addr;

fail:
        vfree(area->addr);
        return NULL;
}

到目前为止,__vmalloc_area_node()已经分配了所需的物理页框,但是这些分散的页框并没有映射到area所代表的那个连续vmalloc区中。map_vm_area()将完成映射工作,它依次修改内核使用的页表项,将pages数组中的每个页框分别映射到连续的vmalloc区中。

x86架构下与分页机制有关的宏和函数

2011年11月8日

Linux采用了通用的四级分页机制,所谓通用就是指Linux使用这种分页机制管理所有架构的分页模型,即便某些架构并不支持四级分页。对于常见的x86架构,如果系统是32位,二级分页模型就可满足系统需求;如果32位系统采用PAE(物理地址扩展)模式,Linux使用三级分页模型;如果是64位系统,Linux使用四级分页模型,也就是说x86架构的分页模型可能是二级、三级或四级。

正如上面所说,Linux通用的四级分页机制均可以满足上述三种情况。本文将分析x86架构下与分页机制相关的一些宏和函数,来展现这种分页机制的通用型。

四级分页模型

x86-64架构采用四级分页模型,它是Linux四级分页机制的一个很好的实现。我们将x86-64架构的分页模型作为分析的入口点,它很好的“迎合”了Linux的四级分页机制。稍候我们再分析这种机制如何同样做到适合三级和二级分页模型。

PGDIR_SHIFT及相关宏

表示线性地址中offset字段、Table字段、Middle Dir字段和Upper Dir字段的位数。PGDIR_SIZE用于计算页全局目录中一个表项能映射区域的大小。PGDIR_MASK用于屏蔽线性地址中Middle Dir字段、Table字段和offset字段所在位。

在四级分页模型中,PGDIR_SHIFT占据39位,即9位页上级目录、9位页中间目录、9位页表和12位偏移。页全局目录同样占线性地址的9位,因此PTRS_PER_PGD为512。

arch/x86/include/asm/pgtable_64_types.h
#define PGDIR_SHIFT 39
#define PTRS_PER_PGD 512
#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE - 1))

pgd_offset()

该函数返回线性地址address在页全局目录中对应表项的线性地址。mm为指向一个内存描述符的指针,address为要转换的线性地址。该宏最终返回addrress在页全局目录中相应表项的线性地址。

arch/x86/include/asm/pgtable.h
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))

PUD_SHIFT及相关宏

表示线性地址中offset字段、Table字段和Middle Dir字段的位数。PUD_SIZE用于计算页上级目录一个表项映射的区域大小,PUD_MASK用于屏蔽线性地址中Middle Dir字段、Table字段和offset字段所在位。

在64位系统四级分页模型下,PUD_SHIFT的大小为30,包括12位的offset字段、9位Table字段和9位Middle Dir字段。由于页上级目录在线性地址中占9位,因此页上级目录的表项数为512。

arch/x86/include/asm/pgtable_64_types.h
#define PUD_SHIFT 30
#define PTRS_PER_PUD 512
#define PUD_SIZE        (_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK        (~(PUD_SIZE - 1))

pud_offset()

pgd_val(pgd)获得pgd所指的页全局目录项,它与PTE_PFN_MASK相与得到该项所对应的物理页框号。__va()用于将物理地址转化为虚拟地址。也就是说,pgd_page_vaddr最终返回页全局目录项pgd所对应的线性地址。因为pud_index()返回线性地址在页上级目录中所在表项的索引,因此pud_offset()最终返回addrress对应的页上级目录项的线性地址。

arch/x86/include/asm/page.h
#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))
arch/x86/include/asm/pgtable_types.h
#define PTE_PFN_MASK            ((pteval_t)PHYSICAL_PAGE_MASK)
arch/x86/include/asm/pgtable.h
static inline unsigned long pgd_page_vaddr(pgd_t pgd)
{
        return (unsigned long)__va((unsigned long)pgd_val(pgd) & PTE_PFN_MASK);
}
static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
        return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}

PMD_SHIFT及相关宏

表示线性地址中offset字段和Table字段的位数,2的PMD_SHIFT次幂表示一个页中间目录项可以映射的内存区域大小。PMD_SIZE用于计算这个区域的大小,PMD_MASK用来屏蔽offset字段和Table字段的所有位。PTRS_PER_PMD表示页中间目录中表项的个数。

在64位系统中,Linux采用四级分页模型。线性地址包含页全局目录、页上级目录、页中间目录、页表和偏移量五部分。在这两种模型中PMD_SHIFT占21位,即包括Table字段的9位和offset字段的12位。PTRS_PER_PMD的值为512,即2的9次幂,表示页中间目录包含的表项个数。

#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE - 1))

pmd_offset()

该函数返回address在页中间目录中对应表项的线性地址。

static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
        return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}
static inline unsigned long pud_page_vaddr(pud_t pud)
{
        return (unsigned long)__va((unsigned long)pud_val(pud) & PTE_PFN_MASK);
}

PAGE_SHIFT及相关宏

表示线性地址offset字段的位数。该宏的值被定义为12位,即页的大小为4KB。与它对应的宏有PAGE_SIZE,它返回一个页的大小;PAGE_MASK用来屏蔽offset字段,其值为oxfffff000。PTRS_PER_PTE表明页表在线性地址中占据9位。

arch/x86/include/asm/page_types.h
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT 12
#define PTRS_PER_PTE    512
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))

通过上面的分析可知,在x86-64架构下64位的线性地址被划分为五部分,每部分占据的位数分别为9,9,9,9,12,实际上只用了64位中的48位。对于四级页表而言,级别从高到底每级页表中表项的个数为512,512,512,512。

三级分页模型

Linux四级分页机制在x86-64架构下有完美的体现,因为每个页表实际上都是有效的。而在接下来要分析的使用PAE的x86-32架构中,页上级目录实际上是不存在的,但是Linux采用了一种有效的方法使三级分页模型仍然可以用四级分页模型来管理。

PGDIR_SHIFT及相关宏

在x86-32的PAE模式下,PGDIR_SHIFT的值为30,它包含offset的12位,Table字段的9位,Middle Dir字段的9位。通过PTRS_PER_PGD可知,页全局目录占线性地址最高的2位。

#define PGDIR_SHIFT 30
#define PTRS_PER_PGD 4

pgd_offset()

在此模式下,pgd_offset()的实现与四级分页机制相同。

PUD_SHIFT及相关宏

在三级分页模型中,页上级目录是不存在的,但是Linux仍然保留了它应有的位置。具体的做法是将PUD_SHIFT的值设为PGDIR_SHIFT,那么与此值有关的宏也就与页全局目录对应的宏相等。在这种模式下,页上级目录在线性地址占据的位数为0,因此页上级目录的表项仅有一项。

typedef struct { pgd_t pgd; } pud_t;

#define PUD_SHIFT PGDIR_SHIFT
#define PTRS_PER_PUD 1
#define PUD_SIZE (1UL << PUD_SHIFT)
#define PUD_MASK (~(PUD_SIZE-1))

pud_offset()

pud_offset()本应该返回线性地址addrress在页上级目录对应的表项地址,由于三级分页模型中并不存在该页目录,因此pud_offset()直接返回address在页全局目录中表项的地址。这也正是Linux四级分页机制通用型的体现。

static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
        return (pud_t *)pgd;
}

PMD_SHIFT及相关宏

在x86-32的PAE模式下,PMD_SHIFT的值与上述四级分页模型相同。因此与此值对应的PTRS_PER_PMD、PMD_SIZE和PMD_MASK都与四级分页模型中的值相等。

arch/x86/include/asm/pgtable-3level_types.h
#define PMD_SHIFT 21
#define PTRS_PER_PMD 512

pmd_offset()

该函数与四级分页模型中的对应函数相同。

通过上面的分析可知,采用PAE的x86-32架构本质上采用了三级分页模型,但是Linux依然将它按照四级分页模型对待。因此线性地址依旧被划分为5部分,每部分占据2,0,9,9,12位。与此对应,每级页表中依次包含的表项个数为4,1,512,512。

二级分页模型

x86-32采用二级分页模型,在此分页模型中只包含页全局目录和页表,并不存在页中间目录和页上级目录。

PGDIR_SHIFT

在三级分页模型中,PGDIR_SHIFT占据22位,即0位页上级目录、0位页中间目录、10位页表和12位偏移。页全局目录同样占线性地址的10位,因此PTRS_PER_PGD为1024。

arch/x86/include/asm/pgtable-2level_types.h
#define PGDIR_SHIFT 22
#define PTRS_PER_PGD 1024

pgd_offset()

实现方法和上述两种分页机制相同。

PMD_SHIFT和PUD_SHIFT及相关宏

在二级分页模型中,PMD_SHIFT和PUD_SHIFT最后都指向PGDIR_SHIFT,因此与该值相关的宏都与页全局目录对应的值相同。

include/asm-generic/pgtable-nopmd.h
#define PUD_SHIFT PGDIR_SHIFT
#define PTRS_PER_PUD 1
#define PUD_SIZE (1UL << PUD_SHIFT)
#define PUD_MASK (~(PUD_SIZE-1))

typedef struct { pud_t pud; } pmd_t;
#define PMD_SHIFT PUD_SHIFT
#define PTRS_PER_PMD 1
#define PMD_SIZE (1UL << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE-1))

pmd_offset()和pud_offset()

这两个宏最终都是返回address在页全局目录中对应的表项。

static inline pmd_t * pmd_offset(pud_t * pud, unsigned long address)
{
        return (pmd_t *)pud;
}
static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
        return (pud_t *)pgd;
}

通过上面的分析可知,对于采用二级分页模型的x86-32架构,Linux仍然使用四级分页机制进行管理,只不过将页上级目录和页中间目录都等价于页全局目录。因此,线性地址被划分为五部分,每部分依次占据10,0,0,10,12。每级页表中包含的表项个数为1024,1,1,1024。

Linux内存管理实践-虚拟地址转换物理地址

2011年11月1日

Linux内核中采用了通用的四级分页模型,这种模型不仅适合32位系统也适合64位系统。分页单元是MMU(内存管理单元)中的一部分,它将线性地址转换为物理地址。本文通过一个内核模块程序模拟内核中虚拟地址转换为物理地址的过程,有关分页机制的原理可以参见这里的文章。

static void get_pgtable_macro(void)
{
	printk("PAGE_OFFSET = 0x%lx\n", PAGE_OFFSET);
	printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
	printk("PUD_SHIFT = %d\n", PUD_SHIFT);
	printk("PMD_SHIFT = %d\n", PMD_SHIFT);
	printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);

	printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
	printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
	printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
	printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);

	printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}

static unsigned long vaddr2paddr(unsigned long vaddr)
{
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;
	unsigned long paddr = 0;
        unsigned long page_addr = 0;
	unsigned long page_offset = 0;

	pgd = pgd_offset(current->mm, vaddr);
	printk("pgd_val = 0x%lx\n", pgd_val(*pgd));
	printk("pgd_index = %lu\n", pgd_index(vaddr));
	if (pgd_none(*pgd)) {
		printk("not mapped in pgd\n");
		return -1;
	}

	pud = pud_offset(pgd, vaddr);
	printk("pud_val = 0x%lx\n", pud_val(*pud));
	if (pud_none(*pud)) {
		printk("not mapped in pud\n");
		return -1;
	}

	pmd = pmd_offset(pud, vaddr);
	printk("pmd_val = 0x%lx\n", pmd_val(*pmd));
	printk("pmd_index = %lu\n", pmd_index(vaddr));
	if (pmd_none(*pmd)) {
		printk("not mapped in pmd\n");
		return -1;
	}

	pte = pte_offset_kernel(pmd, vaddr);
	printk("pte_val = 0x%lx\n", pte_val(*pte));
	printk("pte_index = %lu\n", pte_index(vaddr));
	if (pte_none(*pte)) {
		printk("not mapped in pte\n");
		return -1;
	}

	//页框物理地址机制 | 偏移量
	page_addr = pte_val(*pte) & PAGE_MASK;
	page_offset = vaddr & ~PAGE_MASK;
	paddr = page_addr | page_offset;
	printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
        printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);

	return paddr;
}

static int __init v2p_init(void)
{
	unsigned long vaddr = 0;

	printk("vaddr to paddr module is running..\n");
	get_pgtable_macro();
	printk("\n");

	vaddr = (unsigned long)vmalloc(1000 * sizeof(char));
	if (vaddr == 0) {
		printk("vmalloc failed..\n");
		return 0;
	}
	printk("vmalloc_vaddr=0x%lx\n", vaddr);
	vaddr2paddr(vaddr);

	printk("\n\n");
	vaddr = __get_free_page(GFP_KERNEL);
	if (vaddr == 0) {
		printk("__get_free_page failed..\n");
		return 0;
	}
	printk("get_page_vaddr=0x%lx\n", vaddr);
	vaddr2paddr(vaddr);

	return 0;
}

static void __exit v2p_exit(void)
{
	printk("vaddr to paddr module is leaving..\n");
        vfree((void *)vaddr);
        free_page(vaddr);
}

整个程序的结构如下:

1.get_pgtable_macro()打印当前系统分页机制中的一些宏。

2.通过vmalloc()在内核空间中分配内存,调用vaddr2paddr()将虚拟地址转化成物理地址。

3.通过__get_free_pages()在内核空间中分配页框,调用vaddr2paddr()将虚拟地址转化成物理地址。

4.分别通过vfree()和free_page()释放申请的内存空间。

vaddr2paddr()的执行过程如下:

1.通过pgd_offset计算页全局目录项的线性地址pgd,传入的参数为内存描述符mm和线性地址vaddr。接着打印pgd所指的页全局目录项。

2.通过pud_offset计算页上级目录项的线性地址pud,传入的参数为页全局目录项的线性地址pgd和线性地址vaddr。接着打印pud所指的页上级目录项。

3.通过pmd_offset计算页中间目录项的线性地址pmd,传入的参数为页上级目录项的线性地址pud和线性地址vaddr。接着打印pmd所指的页中间目录项。

4.通过pte_offset_kernel计算页表项的线性地址pte,传入的参数为页中间目录项的线性地址pmd和线性地址vaddr。接着打印pte所指的页表项。

5.pte_val(*pte)先取出页表项,与PAGE_MASK相与的结果是得到要访问页的物理地址;vaddr&~PAGE_MASK用来得到线性地址offset字段;两者或运算得到最终的物理地址。

6.打印物理地址。

Linux中的分页机制

2011年10月25日

Linux中采用了一种通用的四级分页机制,即页全局目录(Page Global Directory)、页上级目录(Page Upper Directory)、页中间目录(Page Middle Directory)和页表(Page Table)。在这种分页机制下,一个完整的线性地址被分为五部分:页全局目录、页上级目录、页中间目录、页表和偏移量,但是对于每个部分所占的位数则是不定的,这跟系统所在的体系架构有关。

对于x86-32(未采用PAE)架构的系统来说,线性地址中的页上级目录和页中间目录两部分占用的位数均为0,页上级目录和页中间目录中都只包含一个目录项,这两个页表项(这里称为页目录项更为确切,下文中的页表项均指某级页表中的一项,对页目录项和页表项不再作特别区分)都被映射到页全局目录中一个适当的目录项中。这种方法本质上只包含两级页表,但是它却仍然保持着四级页表模型。其他未采用四级页表模型的体系架构都采用类似的方法,因此这种四级分页机制具有通用性。

1.数据结构

Linux分别采用pgd_t、pmd_t、pud_t和pte_t四种数据结构来表示页全局目录项、页上级目录项、页中间目录项和页表项。这四种数据结构本质上都是无符号长整型,Linux为了更严格数据类型检查,将无符号长整型分别封装成四种不同的页表项。如果不采用这种方法,那么一个无符号长整型数据可以传入任何一个与四种页表相关的函数或宏中,这将大大降低程序的健壮性。下面仅列出pgd_t类型的内核源码实现,其他类型与此类似。

arch/x86/include/asm/pgtable_64_types.h
 13 typedef unsigned long   pgdval_t;
arch/x86/include/asm/pgtable_types.h
192 typedef struct { pgdval_t pgd; } pgd_t;
arch/x86/include/asm/pgtable.h
 66 #define pgd_val(x)      native_pgd_val(x)
arch/x86/include/asm/pgtable_types.h
199 static inline pgdval_t native_pgd_val(pgd_t pgd)
200 {
201         return pgd.pgd;
202 }

这里需要区别指向页表项的指针和页表项所代表的数据。如果已知一个pgd_t类型的指针pgd,那么通过pgd_val(*pgd)即可获得该页表项(也就是一个无符号长整型数据),这里利用了面向对象的思想。

2.线性地址、页表和页表项

线性地址

不管系统采用多少级分页模型,线性地址本质上都是索引+偏移量的形式,甚至你可以将整个线性地址看作N+1个索引的组合,N是系统采用的分页级数。在四级分页模型下,线性地址被分为5部分,如下图:

 

在线性地址中,每个页表索引即代表线性地址在对应级别的页表中中关联的页表项。正是这种索引与页表项的对应关系形成了整个页表映射机制。

页表

多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。

页表项

从四种页表项的数据结构可以看出,每个页表项其实就是一个无符号长整型数据。每个页表项分两大类信息:页框基地址和页的属性信息。在x86-32体系结构中,每个页表项的结构图如下:

这个图是一个通用模型,其中页表项的前20位是物理页的基地址。由于32位的系统采用4kb大小的 页,因此每个页表项的后12位均为0。内核将后12位充分利用,每个位都表示对应虚拟页的相关属性。

不管是那一级的页表,它的功能就是建立虚拟地址和物理地址之间的映射关系,一个页和一个页框之间的映射关系体现在页表项中。上图中的物理页基地址是个抽象的说明,如果当前的页表项位于页全局目录中,这个物理页基址是指页上级目录所在物理页的基地址;如果当前页表项位于页表中,这个物理页基地址是指最终要访问数据所在物理页的基地址。

 3.地址转换过程

有了上述的基本知识,就很好理解四级页表模式下如何将虚拟地址转化为逻辑地址了。基本过程如下:

1.从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。

2.第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。

3.从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。

4.第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。

5.从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。

6.第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。

7.从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。

8.第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。

9.从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。

10.第五次读取内存得到最终要访问的数据。

整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key