linux 进程 页表 mmap,进程的页表创建详解

[那么每个进程的页表是怎么创建的呢?]

进程的内核页全局目录的装载过程

do_fork()->copy_process()->copy_mm()(如果是fork一个内核线程kernel thread的话,内核线程将会直接使用当前普通进程的页表集,内核线程并不拥有自己的页表集)->dup_mm()->mm_init()->mm_alloc_pgd()->pgd_alloc

pgd_alloc()

{

pgd = (pgd_t *)__get_free_page(PGALLOC_GFP)   //为pgd分为一个物理页

preallocate_pmds(pmds)     //为pmd 页中间目录预先分配页请参考下面的 分析

preallocate_pmds(pmd_t *pmds[])

{

int i;

bool failed = false;

for(i = 0; i < PREALLOCATED_PMDS; i++) {  //PREALLOCATED 该宏只有在定义了 CONFIG_X86_PAE即PAE模式时才有用,否则该宏为0,即32位系统在没有开启PAE模式时只使用三级页表机制

pmd_t *pmd = (pmd_t *)__get_free_page(PGALLOC_GFP);

if (pmd == NULL)

failed = true;

pmds[i] = pmd;

}

return 0;

}     pgd_ctor(mm, pgd)   //将swapper_pg_dir全局页目录(部分后256项--即内核最后1G的虚拟地址,这里指的是内核的页表)拷到pgd里,则可以看出,linux下所有进程的内核页全局目录是一样的,都是swapper_pg_dir里最后的1/4的内容,而每个进程的用户态的页表确是不同的,所以在dup_mmap会去将父进程的页表一项一项的爬出来设置为当前进程的页表。

pgd_ctor{

clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,swapper_pg_dir + KERNEL_PGD_BOUNDARY,KERNEL_PGD_PTRS);

//KERNEL_PGD_BOUNDARY=768,  KERNEL_PGD_PTRS=256    //具体的值见下面的macro

//将swapper_pg_dir 中的768到1024项拷到pgd里,即所有的进程的内核页是相同的。

pgd_set_mm(pgd, mm);  pgd->index = mm    建立反向映射吧

pgd_list_add(pgd);   将pgd加入到pgd_list中去

}

进程的用户态地址页拷贝

dup_mmap()函数实现页表映射的拷贝

dup_mmap()

{

struct vm_area_struct *mpnt,

for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) { //遍历父进程的所有的虚拟地址空间

tmp = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL); //分配一个新的虚拟地址空间

*tmp = *mpnt;   //将父进程的虚拟地址空间拷贝到新分配的虚拟地址空间中去,并将新分配的虚拟地址空间插入到新进程内存空间中去,这里有两种数据结构,一种是链表用于方便的遍历所有的虚拟地址空间,另一种是红黑树,用来快速的找出适合的虚拟地址空间块

*pprev = tmp;

pprev = &tmp->vm_next;

tmp->vm_prev = prev;

prev = tmp;

__vma_link_rb(mm, tmp, rb_link, rb_parent); //插入红黑树中去

rb_link = &tmp->vm_rb.rb_right;

rb_parent = &tmp->vm_rb;                retval = copy_page_range(mm, oldmm, mpnt); //最后进行重新映射,要是没有这项(页表复制)的话,即使有合法访问的虚拟存储区域,但是没有正确的页表,不能访问到具体的物理内存,所以为了能建立正确的页映射,使进程能够访问到具体的物理页。

}

}

页表的复制

copy_page_range()

{

dst_pgd = pgd_offset(dst_mm, addr);  //取得pgd

src_pgd = pgd_offset(src_mm, addr); //取得pgd

do {

copy_pud_range();  //拷贝页上级目录

} while (...)

}

copy_pud_range()

{

dst_pud = pud_alloc(dst_mm, dst_pgd, addr); //分配一页内存做为页上级表,如果是32位没有开启PAE的话,pud就等于pgd

src_pud = pud_offset(src_pgd, addr);

do {

copy_pmd_range();  //拷贝页中间目录

} while (...)

}

copy_pud_range()

{

dst_pmd = pmd_alloc(dst_mm, dst_pud, addr); //分配一页内存做为页中间目录,如果是32位没有开启PAE的话,pud就等于pgd

src_pmd = pmd_offset(src_pud, addr);

do {

copy_pte_range();  //拷贝页表项

} while (...)

}

copy_pte_range

{

dst_pte = pte_alloc_map_lock(); //分配大小为一页的页表

do {

copy_one_pte() //具体的实现是set_pte_at(dst_mm, addr, dst_pte, pte) 即native_set_pte中的 *dst_pte = pte

}

}

//分配pmd

static inline pmd_t *pmd_alloc(struct mm_struct *mm, pud_t *pud, unsigned long address)

{

return (unlikely(pud_none(*pud)) && __pmd_alloc(mm, pud, address))? NULL: pmd_offset(pud, address);

}

在32位的non-pae里__pmd_alloc直接返回0,否则__pmd_alloc()分配一页做为pmd

int __pmd_alloc(struct mm_struct *mm, pud_t *pud, unsigned long address)

{

pmd_t *new = pmd_alloc_one(mm, address);  //直接分配一页

if (!new)

return -ENOMEM;

smp_wmb(); /* See comment in __pte_alloc */

spin_lock(&mm->page_table_lock);

#ifndef __ARCH_HAS_4LEVEL_HACK

if (pud_present(*pud))          /* Another has populated it */

pmd_free(mm, new);

else

pud_populate(mm, pud, new);

#else

if (pgd_present(*pud))          /* Another has populated it */

pmd_free(mm, new);

else

pgd_populate(mm, pud, new);

#endif /* __ARCH_HAS_4LEVEL_HACK */

spin_unlock(&mm->page_table_lock);

printk (KERN_INFO "wangbo in __pmd_alloc\n");

return 0;

}

[some macro definition]

#define PAGE_OFFSET 0xc0000000

#define PGDIR_SHIFT     22

#define PTRS_PER_PGD    1024

#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))

#define KERNEL_PGD_BOUNDARY     pgd_index(PAGE_OFFSET) // 768

#define KERNEL_PGD_PTRS         (PTRS_PER_PGD - KERNEL_PGD_BOUNDARY) //256

mm_alloc_pgd() 函数会调用pgd_alloc()会为该进程分配一页(4K)的页全局目录的线性地址并保存在 task_struct->mm_struct->pgd中

具体的实现是通过__get_free_pages((gfp_mask), 0)实现的,该函数通过alloc_pages()在低端内存里( 小于896M的空间里)分配一个页描述符(struct page *page),并将该页的页描述符通过page_address()转换成虚拟地址。实际上就是通过__va(PFN_PHYS(page_to_pfn(page)))先将页描述符转换成实际物理地址((page - mem_map) << 12 )(所有的物理页描述符存放在mem_map数组里,左移12是一页4K的大小),然后再将物理地址通过__va转换成虚拟地址,也即将得到的低端物理内存地址直接加上PAGE_OFFSET即可 (unsigned long )(x)+PAGE_OFFSET

到现在可以得知进程描述符里的mm_struct->pgd是线性地址,而且属于内核空间的地址(大于0xc0000000)。

pte的映射(写时复制机制)

dup_mm()->dup_mmap 中完成中间页表pmd到页表pte的映射从而建立起页表,并将每一个pte页表,置为只读,以便激发起写时复制技术dup_mmap执行,继续复制pte页表项,使子进程的每个中间页表pmd的每个页表项pte=父进程对应的该pte,并且将该pte最后几个标志位中的只读位置1,从而完成写时复制的准备工作

cr3寄存器的加载

cr3寄存器的加载是在进程调度的时候更新的,具体如下

schedule()->context_switch()->switch_mm()->load_cr3(next->pgd)

load_cr3加载的是mm_struct->pgd,即线性地址,而实际上加裁到cr3寄存器的是实际的物理地址write_cr3(__pa(pgdir));在装载cr3寄存器时将线性地址通过__pa转换成了物理地址了,所以cr3寄存器是装的是实实在在的物理地址。

uid-31404751-id-5753858.html

正在使用的页目录的物理地址存在cr3控制寄存器中

当要fork一个新的进程时,会先去分配一个物理页(4K)(copy_mm()->dup_mm()->mm_init()中实现)作为该进程的页目录

32位机器时 4K页里可用1024项,而页目录里的每一项指向一个页表(4K),4K的页表也只有1024项可用,而页表中的每一项又指向具体的一页,所以一个进程的理论可用的空间是1024( 页目录)x1024(页表)x4Kb(每页4K)= 4G(物理页)

上述说的cr3寄存器装载的是普通进程的页目录首地址,而内核线程使用的是swapper_pg_dir页目录地址

内核态访问的是线性地址空间,没有线性地址的物理内存是不能被内核访问的,因此,映射就是指将物理内存地址映射到内核线性地址上。这样内核才能访问。

转载出处:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值