内存地址这一块是很大的一块,今天就一点一点从了解到深入。
首先从物理内存开始说起,在32位Linux操作系统下有4G的物理内存,CPU直接访问的就是物理地址,也称物理寻址,当CPU到读取到内存中的内容时,先通过生成一个物理地址,然后通过总线,找到内存中的值去出返回给CPU。这一部分不再细讲。这是早期计算机的取址方式,而现代的计算机采用的是虚拟寻址:即就是CPU先生成一个虚拟地址来访问主存,再访问主存之前先要将虚拟地址转换成物理地址,这个工作就交给了CPU上的MMU(内存管理单元)。
虚拟内存空间在此就不讲了,其他博客中有讲到32位Linux的虚拟地址空间图,接下来就要说到页表这个数据结构,页表就是专门将虚拟页转换成物理页,如图所示。页表仿佛就像是一个字典,通过你给出的虚拟地址从中找到物理地址。图中可以看到有效位标识,设置了有效为则说明这个物理页中缓存着这个虚拟页,若没有设置标识位,则说明这个虚拟页还未被分配。其中每一行都是PTE数组的一个数据,它是由一个有效位和n位字段组成。
对上面内容简单了解一下之后,就该对其页表的具体问题进行探讨了,刚才说到标识位若被设置则说明有这个虚拟页的缓存,这也就是页命中,命中这一块就没什么多说的了,主要来说一下未命中(缺页)该怎么办,还是上面这幅图,VP3这一块就是未被缓存的页,这个时候就会触发缺页异常。(do_page_fault)。那么接下来就来看看这个过程吧。
在此之前,本想直接源码来进行讲解,但发现一些知识认识不充分会影响到代码的阅读,所以先将一些专业名词认识一下。
TLB:当cpu产生一个虚拟地址要转换成物理地址的时候,都会查询PTE表,最糟糕的情况下是全部遍历一遍,这并不是我们想看到的,于是TLB就出现了,它是一种翻译后备缓冲区,构成如下,其中标记字段是从虚拟地址中的虚拟页号中提取出来的,而索引,如果有2^n个TLB,那么就有n个索引。其过程是在处理器上的MMU上运行的,所以速度非常之快,其步骤如下:
- 1.CPU产生一个虚拟地址
- 2.MMU从TLB中取出相应的PTE。
- 3.MMU将这个虚拟地址翻译成物理地址,再将其发送到缓存/主存上
- 4.主存/缓存再将数据返回给CPU
接下来讲解的是两个结构体(很重要)mm_struct和vm_area_struct,vm_area_struct说的是虚存空间基本单位,mm_struct说的是整个虚拟地址空间。
struct vm_area_struct {
/**
* 指向线性区所在的内存描述符。
*/
struct mm_struct * vm_mm; /* The address space we belong to. */
/**
* 线性区内的第一个线性地址。
*/
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;
/**
* 线性区中页框的访问许可权。
*/
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
/**
* 线性区的标志。
*/
unsigned long vm_flags; /* Flags, listed below. */
/**
* 用于红黑树的数据。
*/
struct rb_node vm_rb;
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap prio tree, or
* linkage to the list of like vmas hanging off its node, or
* linkage of vma in the address_space->i_mmap_nonlinear list.
*/
/**
* 链接到反映射所使用的数据结构。
*/union {
/**
* 如果在优先搜索树中,存在两个节点的基索引、堆索引、大小索引完全相同,那么这些相同的节点会被链接到一个链表,而vm_set就是这个链表的元素。
*/
struct {
struct list_head list;
void *parent; /* aligns with prio_tree_node parent */
struct vm_area_struct *head;
} vm_set;
/**
* 如果是文件映射,那么prio_tree_node用于将线性区插入到优先搜索树中。作为搜索树的一个节点。
*/
struct raw_prio_tree_node prio_tree_node;
} shared;
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
/**
* 指向匿名线性区链表的指针(参见"映射页的反映射")。
* 页框结构有一个anon_vma指针,指向该页的第一个线性区,随后的线性区通过此字段链接起来。
* 通过此字段,可以将线性区链接到此链表中。
*/
struct list_head anon_vma_node; /* Serialized by anon_vma->lock */
/**
* 指向anon_vma数据结构的指针(参见"映射页的反映射")。此指针也存放在页结构的mapping字段中。
*/
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
/**
* 指向线性区的方法。
*/
struct vm_operations_struct * vm_ops;
/* Information about our backing store: */
/**
* 在映射文件中的偏移量(以页为单位)。对匿名页,它等于0或vm_start/PAGE_SIZE
*/
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
/**
* 指向映射文件的文件对象(如果有的话)
*/
struct file * vm_file; /* File we map to (can be NULL). */
/**
* 指向内存区的私有数据。
*/
void * vm_private_data;
unsigned long vm_truncate_count;/* truncate_count or restart_addr */
#ifndef CONFIG_MMU
atomic_t vm_usage; /* refcount (VMAs shared if !MMU) */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};
接下来是mm_struct结构体:网上有一个很好的图
struct mm_struct {
/**
* 指向线性区对象的链表头。
*/
struct vm_area_struct * mmap; /* list of VMAs */
/**
* 指向线性区对象的红-黑树的根
*/
struct rb_root mm_rb;
/**
* 指向最后一个引用的线性区对象。
*/
struct vm_area_struct * mmap_cache; /* last find_vma result */
/**
* 在进程地址空间中搜索有效线性地址区的方法。
*/
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
/**
* 释放线性地址区间时调用的方法。
*/
void (*unmap_area) (struct vm_area_struct *area);
/**
* 标识第一个分配的匿名线性区或文件内存映射的线性地址。
*/
unsigned long mmap_base; /* base of mmap area */
/**
* 内核从这个地址开始搜索进程地址空间中线性地址的空间区间。
*/
unsigned long free_area_cache; /* first hole */
/**
* 指向页全局目录。
*/
pgd_t * pgd;
/**
* 次使用计数器。存放共享mm_struct数据结构的轻量级进程的个数。
*/
atomic_t mm_users; /* How many users with user space? */
/**
* 主使用计数器。每当mm_count递减时,内核都要检查它是否变为0,如果是,就要解除这个内存描述符。
*/
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
/**
* 线性区的个数。
*/
int map_count; /* number of VMAs */
/**
* 内存描述符的读写信号量。
* 由于描述符可能在几个轻量级进程间共享,通过这个信号量可以避免竞争条件。
*/
struct rw_semaphore mmap_sem;
/**
* 线性区和页表的自旋锁。
*/
spinlock_t page_table_lock; /* Protects page tables, mm->rss, mm->anon_rss */
/**
* 指向内存描述符链表中的相邻元素。
*/
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
/**
* start_code-可执行代码的起始地址。
* end_code-可执行代码的最后地址。
* start_data-已初始化数据的起始地址。
* end_data--已初始化数据的结束地址。
*/
unsigned long start_code, end_code, start_data, end_data;
/**
* start_brk-堆的超始地址。
* brk-堆的当前最后地址。
* start_stack-用户态堆栈的起始地址。
*/
unsigned long start_brk, brk, start_stack;
/**
* arg_start-命令行参数的起始地址。
* arg_end-命令行参数的结束地址。
* env_start-环境变量的起始地址。
* env_end-环境变量的结束地址。
*/
unsigned long arg_start, arg_end, env_start, env_end;
/**
* rss-分配给进程的页框总数
* anon_rss-分配给匿名内存映射的页框数。s
* total_vm-进程地址空间的大小(页框数)
* locked_vm-锁住而不能换出的页的个数。
* shared_vm-共享文件内存映射中的页数。
*/
unsigned long rss, anon_rss, total_vm, locked_vm, shared_vm;
/**
* exec_vm-可执行内存映射的页数。
* stack_vm-用户态堆栈中的页数。
* reserved_vm-在保留区中的页数或在特殊线性区中的页数。
* def_flags-线性区默认的访问标志。
* nr_ptes-this进程的页表数。
*/
unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;
/**
* 开始执行elf程序时使用。
*/
unsigned long saved_auxv[42]; /* for /proc/PID/auxv */
/**
* 表示是否可以产生内存信息转储的标志。
*/
unsigned dumpable:1;
/**
* 懒惰TLB交换的位掩码。
*/
cpumask_t cpu_vm_mask;
/* Architecture-specific MM context */
/**
* 特殊体系结构信息的表。
* 如80X86平台上的LDT地址。
*/
mm_context_t context;
/* Token based thrashing protection. */
/**
* 进程有资格获得交换标记的时间。
*/
unsigned long swap_token_time;
/**
* 如果最近发生了主缺页。则设置该标志。
*/
char recent_pagein;
/* coredumping support */
/**
* 正在把进程地址空间的内容卸载到转储文件中的轻量级进程的数量。
*/
int core_waiters;
/**
* core_startup_done-指向创建内存转储文件时的补充原语。
* core_done-创建内存转储文件时使用的补充原语。
*/
struct completion *core_startup_done, core_done;
/* aio bits */
/**
* 用于保护异步IO上下文链表的锁。
*/
rwlock_t ioctx_list_lock;
/**
* 异步IO上下文链表。
*/
struct kioctx *ioctx_list;
/**
* 默认的异步IO上下文。
*/
struct kioctx default_kioctx;
/**
* 进程所拥有的最大页框数。
*/
unsigned long hiwater_rss; /* High-water RSS usage */
/**
* 进程线性区中的最大页数。
*/
unsigned long hiwater_vm; /* High-water virtual memory usage */
};
每一个进程都会有自己独立的mm_struct,这样每一个进程都会有自己独立的地址空间。而vm_area_struct描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。(这一块有很多要点,这里不提出了)。
接下来就是do_page_fault的源码分析
asmlinkage void
do_page_fault(unsigned long address, unsigned long mmcsr,
long cause, struct pt_regs *regs)
{
struct vm_area_struct * vma;
struct mm_struct *mm = current->mm;
const struct exception_table_entry *fixup;
int fault, si_code = SEGV_MAPERR;
siginfo_t info;
/* As of EV6, a load into $31/$f31 is a prefetch, and never faults
(or is suppressed by the PALcode). Support that for older CPUs
by ignoring such an instruction. */
if (cause == 0) {
unsigned int insn;
__get_user(insn, (unsigned int __user *)regs->pc);
if ((insn >> 21 & 0x1f) == 0x1f &&
/* ldq ldl ldt lds ldg ldf ldwu ldbu */
(1ul << (insn >> 26) & 0x30f00001400ul)) {
regs->pc += 4;
return;
}
}
/* If we're in an interrupt context, or have no user context,
we must not take the fault. */
if (!mm || in_interrupt())
goto no_context;//查看是否有中断操作
#ifdef CONFIG_ALPHA_LARGE_VMALLOC
if (address >= TASK_SIZE)
goto vmalloc_fault;//vmalloc内核内存分配函数,虚拟地址连续,物理地址不连续
#endif
down_read(&mm->mmap_sem);
vma = find_vma(mm, address);//Linux进程通过vma管理,其中每一个vma节点对应一段连续内存澹(物理地址不一定李娜需)
if (!vma)
goto bad_area;
if (vma->vm_start <= address)// 如果vma->start_address<=address,则直接跳到 "合法访问"阶段
goto good_area;
if (!(vma->vm_flags & VM_GROWSDOWN))//"入栈"操作,则该vma的标志为 "向下增长"
//如果vma->start_address>address,则也有可能是用户的"入栈行为"导致缺页
goto bad_area;
if (expand_stack(vma, address))//扩展栈
goto bad_area;
/* Ok, we have a good vm_area for this memory access, so
we can handle it. */
good_area:
si_code = SEGV_ACCERR;
if (cause < 0) {
if (!(vma->vm_flags & VM_EXEC))
goto bad_area;
} else if (!cause) {
/* Allow reads even for write-only mappings */
if (!(vma->vm_flags & (VM_READ | VM_WRITE)))//标志位和读写位
goto bad_area;
} else {
if (!(vma->vm_flags & VM_WRITE))
goto bad_area;
}
survive:
/* If for any reason at all we couldn't handle the fault,
make sure we exit gracefully rather than endlessly redo
the fault. */
fault = handle_mm_fault(mm, vma, address, cause > 0);
up_read(&mm->mmap_sem);
switch (fault) {
case VM_FAULT_MINOR:
current->min_flt++;
break;
case VM_FAULT_MAJOR:
current->maj_flt++;
break;
case VM_FAULT_SIGBUS:
goto do_sigbus;
case VM_FAULT_OOM:
goto out_of_memory;
default:
BUG();
}
return;
/* Something tried to access memory that isn't in our memory map.
Fix it, but check if it's kernel or user first. */
bad_area:
up_read(&mm->mmap_sem);
if (user_mode(regs))
goto do_sigsegv;
no_context:
/* Are we prepared to handle this fault as an exception? */
if ((fixup = search_exception_tables(regs->pc)) != 0) {
unsigned long newpc;
newpc = fixup_exception(dpf_reg, fixup, regs->pc);
regs->pc = newpc;
return;
}
/* Oops. The kernel tried to access some bad page. We'll have to
terminate things with extreme prejudice. */
printk(KERN_ALERT "Unable to handle kernel paging request at "
"virtual address %016lx\n", address);
die_if_kernel("Oops", regs, cause, (unsigned long*)regs - 16);
do_exit(SIGKILL);
/* We ran out of memory, or some other thing happened to us that
made us unable to handle the page fault gracefully. */
out_of_memory:
if (current->pid == 1) {
yield();
down_read(&mm->mmap_sem);
goto survive;
}
printk(KERN_ALERT "VM: killing process %s(%d)\n",
current->comm, current->pid);
if (!user_mode(regs))
goto no_context;
do_exit(SIGKILL);
do_sigbus:
/* Send a sigbus, regardless of whether we were in kernel
or user mode. */
info.si_signo = SIGBUS;
info.si_errno = 0;
info.si_code = BUS_ADRERR;
info.si_addr = (void __user *) address;
force_sig_info(SIGBUS, &info, current);
if (!user_mode(regs))
goto no_context;
return;
do_sigsegv:
info.si_signo = SIGSEGV;//发生段错误时发送给它的信号
info.si_errno = 0;
info.si_code = si_code;
info.si_addr = (void __user *) address;
force_sig_info(SIGSEGV, &info, current);
return;
#ifdef CONFIG_ALPHA_LARGE_VMALLOC
vmalloc_fault:
if (user_mode(regs))
goto do_sigsegv;
else {
/* Synchronize this task's top level page-table
with the "reference" page table from init. */
long index = pgd_index(address);
pgd_t *pgd, *pgd_k;
pgd = current->active_mm->pgd + index;
pgd_k = swapper_pg_dir + index;
if (!pgd_present(*pgd) && pgd_present(*pgd_k)) {
pgd_val(*pgd) = pgd_val(*pgd_k);
return;
}
goto no_context;
}
#endif
}
这一部分其实并还没有处理到分配,这里先是检测部分,从上面的注释也能看出来,先检测了是否有中断和线程的临界区操作,其次是检测这个传进来的这个地址是否在进程的地址空间内,再往下走,是获取信号mm->mmap_sem,再是find_vma,接下来就是判断这vma的起始地址和address, 如果vma->start_address<=address,则直接跳到 "合法访问"阶段,下面还有一些特殊情况(源码中有注释处理),还有当栈被映射到线性区,那么就对栈进行栈扩展,当这些都处理完之后,我们就有vm_area进行分配处理了。接下来就是handle_mm_fault()了。见下篇。