ps: Linux操作系统对于程序地址,物理地址的处理,对于源码,我也看不大懂,只是截取当我们进程发生正常缺页中断的时候的调用情况。本文中所有的源码都是进行截取过的,如果大家感兴趣可以去下载源码。
在Linux 操作系统 进程(1)-CSDN博客我们在最后简单介绍了我们所写的C语言程序的地址都是虚拟地址,通过页表映射到物理地址,那么这篇文章,我们就深入一点,通过观察Linux源码中对于页面内容的填充,或者是当发生缺页中断的时候,如何获取到物理地址。
进程的起点 (task_struct)
当说到一个进程所有的属性的时候,必不可少的一个结构体就是task_struct结构体,那么当我们说到程序地址空间的时候,该结构体里一定会包含描述这个属性的相关字段。
cpp
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
//就在这里
struct mm_struct *mm, *active_mm;
}
通过这个结构体,让我们仔细看看Linux对于程序地址空间描述(截取),
cpp
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb; /* 红黑树,用于管理 VMA */
struct vm_area_struct *mmap_cache; /* 上一个查找的 VMA */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, anon_rss, total_vm, locked_vm, shared_vm;
unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;
//虚拟地址空间的定义 stack ...
};
对于程序地址空间的定义有了,那么相对应的页表的描述不就在第一行 struct vm_area_struct *mmap;
vm_area_struct
cpp
struct vm_area_struct {
struct mm_struct * vm_mm; /* 所属的mm_struct. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next; /*链接下一个进程的VMA空间*/
unsigned long vm_flags; /* 保存的数据的权限 -> 只读 读写... */
struct rb_node vm_rb; /*栈空间,堆空间... 的范围 */
页表的填充
mm_struct的初始化
mm_struct 结构体的初始化是由一个 init_mm的宏完成的
cpp
struct mm_struct init_mm = INIT_MM (init_mm);
cpp
#define INIT_MM(name) \
{ \
.mm_rb = RB_ROOT, \
.pgd = swapper_pg_dir, \
.mm_users = ATOMIC_INIT(2), \
.mm_count = ATOMIC_INIT(1), \
.mmap_sem = __RWSEM_INITIALIZER(name.mmap_sem), \
.page_table_lock = SPIN_LOCK_UNLOCKED, \
.mmlist = LIST_HEAD_INIT(name.mmlist), \
.cpu_vm_mask = CPU_MASK_ALL, \
.default_kioctx = INIT_KIOCTX(name.default_kioctx, name), \
}
但这也只是对于虚拟地址空间的初始化,页表并没有填充任何内容,当我们进行读取程序内容的时候,一定会发生缺页中断,既然初始化并没有对于页表初始化,那也就是说,在缺页中断的过程中,会有对该情况的判断。那么让我们跳转到缺页中断时,系统执行的函数吧!
do_page_fault
cpp
/*datammu : 错误类型
* esr0 : 错误信息
* ear0 ; 错误的虚拟地址
*/
asmlinkage void do_page_fault(int datammu, unsigned long esr0, unsigned long ear0)
{
struct vm_area_struct *vma;
struct mm_struct *mm;
unsigned long _pme, lrai, lrad, fixup;
siginfo_t info;
pgd_t *pge;
pud_t *pue;
pte_t *pte;
int write;
mm = current->mm; //获取错误页的 mm_struct
//...
vma = find_vma(mm, ear0);
switch (handle_mm_fault(mm, vma, ear0, write))
// ...
}
在这个函数的前面数据的定义中,我们发现了几个之前并未出现的参数 pgd_t *pge; pud_t *pue; pte_t *pte; 这几个参数是操作系统对自己页表访问的具体描述,等下再说。当这个函数正常执行时,我们会发现他调用了这个函数vma = find_vma(mm, ear0); 获取到发生缺页终端的虚拟地址所在的vma
cpp
/* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
/* Check the cache first. */
/* (Cache hit rate is typically around 35%.) */
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
struct rb_node * rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node,
struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
在这个函数中,我们可以很清楚的看到,查找vma的时候,先去查找上一次使用过的vma(我们所写的程序的都具有局部性),然后在使用红黑树结构查找。那么有同学就有疑问了,为什么我们已经得到了出现错误的虚拟地址,为什么还要去查找他所在的vma范围呢?
cpp
unsigned long vm_flags; /* 保存的数据的权限 -> 只读 读写... */
在我们所写的程序中,有只读的变量,可以读写的变量,或者是需要申请内存的堆空间的变量,如果我们只有虚拟地址,什么不知道,那么这个数据是需要重新申请内存呢,或者说是不可更改呢,vm_flags保存的权限,和vma结构体中的其他字段就起到了作用。
页表填充
找到我们地址的其他属性后,就应该去找到页表了,handle_mm_fault(mm, vma, ear0, write),为什么说是找页表呢? 在do_page_fault 函数中,我们没有见过的那几个变量其实就是Linux系统的三级页表结构,通过一级一级的转变才得到最后的页表, 分别就是页目录 , 页中间目录 ,页表,最后通过偏移量才得到虚拟地址所在的页框。
cpp
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, int write_access)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
__set_current_state(TASK_RUNNING); //更改进程为运行态
inc_page_state(pgfault);
//通过页目录去获得页表
pgd = pgd_offset(mm, address);
spin_lock(&mm->page_table_lock);
pud = pud_alloc(mm, pgd, address);
if (!pud)
goto oom;
pmd = pmd_alloc(mm, pud, address);
if (!pmd)
goto oom;
pte = pte_alloc_map(mm, pmd, address);
if (!pte)
goto oom;
return handle_pte_fault(mm, vma, address, write_access, pte, pmd);
oom:
spin_unlock(&mm->page_table_lock);
return VM_FAULT_OOM;
}
我们终于获得进程的页表,进入到了最后一个函数,页表第一次映射的处理就出现了,以及后续对于正常缺页中断的处理。
cpp
static inline int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct * vma, unsigned long address,
int write_access, pte_t *pte, pmd_t *pmd)
{
pte_t entry;
entry = *pte;
// 判断页框存在不存在 不存在就是第一次映射
if (!pte_present(entry)) {
/*
* If it truly wasn't present, we know that kswapd
* and the PTE updates will not touch it later. So
* drop the lock.
*/
if (pte_none(entry))
return do_no_page(mm, vma, address, write_access, pte, pmd);
if (pte_file(entry))
return do_file_page(mm, vma, address, write_access, pte, pmd);
return do_swap_page(mm, vma, address, pte, pmd, entry, write_access);
}
if (write_access) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address, pte, pmd, entry);
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
ptep_set_access_flags(vma, address, pte, entry, write_access);
update_mmu_cache(vma, address, entry);
pte_unmap(pte);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
}
当然,为了加快这个过程,cpu中汇集成一个MMU(内存管理单元)用来处理这些事情