Archive for the ‘Kernel内核分析’ category

从用户态到内核态的切换

June 14th, 2015

我们都知道用户态向内核态切换有三种方式:1:系统调用 2:中断  3:异常 。之前只是知道这个切换的原理,但是并没有仔细的从代码上面学习切换的过程。之前我学习了页表,但是内核页表与进程页表是割裂的,信号处理也是割裂的。这里我会结合用户态到内核态从代码进行分析。

这部分切换的函数是用汇编代码写的。详细路径存在于arch/x86/kernel/entry_32.S文件中。

内核栈

内核栈:在Linux中每个进程有两个栈,分别用于用户态和内核态的进程执行(类似于内核页表与用户页表),其中的内核栈就是用于内核态的栈,它和进程的thread_info结构一起放在两个连续的页框大小的空间内。

在kernel 源代码中使用C语言定义了一个联合结构thread_union 方便地表示一个进程的thread_info和内核栈(定义在定义在include/linux/sched.h)

union thread_union {
     struct thread_info thread_info;
     unsigned long stack[THREAD_SIZE/sizeof(long)];
};

结合一个图可以看的更加清楚,其中栈是从高地址向低地址生长的,其中esp寄存器是CPU栈指针,存放内核栈栈顶地址。从用户态刚切换到内核态时,进程的内核栈总是空的(非常类似于进程页表切换到内核态页表,惰性传值),此时esp指向这个栈的顶端。

在x86中调用int指令系统调用后会把用户栈的%esp的值及相关寄存器压入内核栈中,系统调用通过iret指令返回,在返回之前会从内核栈弹出用户栈的%esp和寄存器的状态,然后进行恢复。所以在进入内核态之前要保存进程的上下文,中断结束后恢复进程上下文,那靠的就是内核栈
Screenshot1
thread_info结构的定义如下

25 struct thread_info {
 26         struct task_struct      *task;          /* main task structure */
 27         struct exec_domain      *exec_domain;   /* execution domain */
 28         __u32                   flags;          /* low level flags */
 29         __u32                   status;         /* thread synchronous flags */
 30         __u32                   cpu;            /* current CPU */
 31         int                     saved_preempt_count;
 32         mm_segment_t            addr_limit;
 33         struct restart_block    restart_block;
 34         void __user             *sysenter_return;
 35 #ifdef CONFIG_X86_32
 36         unsigned long           previous_esp;   /* ESP of the previous stack in
 37                                                    case of nested (IRQ) stacks
 38                                                 */
 39         __u8                    supervisor_stack[0];
 40 #endif
 41         unsigned int            sig_on_uaccess_error:1;
 42         unsigned int            uaccess_err:1;  /* uaccess failed */
 43 };

内核栈保存用户态的esp,eip等寄存器的值,首先得知道内核栈的栈指针,那在进入内核态之前,通过TSS才能获得内核栈的栈指针。

TSS

TSS反映了CPU上的当前进程的特权级。linux为每一个cpu提供一个tss段,并且在tr寄存器中保存该段。在从用户态切换到内核态时,可以通过获取TSS段中的esp0来获取当前进程的内核栈 栈顶指针,从而可以保存用户态的cs,esp,eip等上下文。

注:linux中之所以为每一个cpu提供一个tss段,而不是为每个进程提供一个tss段,主要原因是tr寄存器永远指向它,在任务切换的适合不必切换tr寄存器,而且进程切换的时候只会切换新的esp0。结合scheduler()中的switch_to宏,next_p->thread.esp0装入对应于本地CPU的TSS的esp0字段(其实,任何由sysenter汇编指令产生的从用户态到内核态的特权级转换将把这个地址拷贝到esp寄存器中)。

内核代码中TSS结构的定义位于arch/x86/include/asm/processor.h文件。

其中主要的内容是:

  • 硬件状态结构:          x86_hw_tss(arch/x86/include/asm/processor.h)
  • IO权位图:     io_bitmap
  • 备用内核栈:        stack

linux的tss段中只使用esp0和iomap等字段,并不用它的其他字段来保存寄存器,在一个用户进程被中断进入内核态的时候,从tss中的硬件状态结构中取出esp0(即内核栈栈顶指针),然后切到esp0,其它的寄存器则保存在esp0指的内核栈上而不保存在tss中。

下面,我们看看INIT_TSS定义,其中init_stack是宏定义,指向内核栈 #define init_stack (init_thread_union.stack)

824 #define INIT_TSS  {                                                       \
825         .x86_tss = {                                                      \
826                 .sp0            = sizeof(init_stack) + (long)&init_stack, \
827                 .ss0            = __KERNEL_DS,                            \
828                 .ss1            = __KERNEL_CS,                            \
829                 .io_bitmap_base = INVALID_IO_BITMAP_OFFSET,               \
830          },                                                               \
831         .io_bitmap              = { [0 ... IO_BITMAP_LONGS] = ~0 },       \
832 }

内核栈栈顶指针、内核代码段、内核数据段赋值给TSS中的相应项。从而进程从用户态切换到内核态时,可以从TSS段中获取内核栈栈顶指针,进而保存进程上下文到内核栈中。

 

综上所述:

  • 读取tr寄存器,访问TSS段
  • 从TSS段中的esp0获取进程内核栈的栈顶指针
  • 由控制单元在内核栈中保存当前eflags,cs,ss,eip,esp寄存器的值。
  • 由SAVE_ALL保存其寄存器的值到内核栈
  • 把内核代码选择符写入CS寄存器,内核栈指针写入ESP寄存器,把内核入口点的线性地址写入EIP寄存器

此时,CPU已经切换到内核态,根据EIP中的值开始执行内核入口点的第一条指令。

 

参考:

http://guojing.me/linux-kernel-architecture/posts/process-switch/

信号处理的时机

June 4th, 2015

Update 2015-6-6

我们知道1-32是最早就拥有的信号,之后又加入了33~64信号。编号为33-64的信号又称为实时信号。需要注意的是:这里的“实时”和实时操作系统中的“实时”没有任何联系,实时信号在处理速度上并不会比普通信号快,它们之间的区别就是:普通信号会对多次的同一个信号进行“合并”处理而实时信号会一一处理。这就要求我们在编写信号监听函数时,要捕获普通信号,必须时刻轮训监听,因为系统默认会丢弃同种类型的普通信号!

在用户态下,我们向一个进程发送信号,但是这个信号如何被处理呢?其实信号处理的时机就是从异常或中断恢复发生时处理,这里我们可以把中断理解为1)中断 2)system call 两个时机,这两个时机都会有从内核态->用户态切换的过程。

根据ULK中kernel返回用户态的流程图:

39693914

我们发现当kernel 从中断异常返回时,会检查USER_RPL决定返回 kernel 还是 userspace,当跳入到resume_userspace,也就意味着在返回用户空间时,检查_TIF_WORK_MASK 是否有信号,如果没有则返回restore_all,否则进入work_pending,其实这里就可以看到信号处理是一个异步的!

ret_from_exception:
    preempt_stop(CLBR_ANY)
ret_from_intr:
    GET_THREAD_INFO(%ebp)
#ifdef CONFIG_VM86
...
#else
    /*
     * We can be coming here from child spawned by kernel_thread().
     */
    movl PT_CS(%esp), %eax
    andl $SEGMENT_RPL_MASK, %eax
#endif
    cmpl $USER_RPL, %eax
    jb resume_kernel        # not returning to v8086 or userspace

ENTRY(resume_userspace)
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    TRACE_IRQS_OFF
    movl TI_flags(%ebp), %ecx
    andl $_TIF_WORK_MASK, %ecx  # is there any work to be done on
                    # int/exception return?
    jne work_pending
    jmp restore_all
END(ret_from_exception)

如果返回是kernel的话,按照汇编代码与图示,要先禁中断,然后判断__preempt_count变量值,当出现中断嵌套这个值会+1,如果为0也就意味着当前系统没有出现中断的嵌套执行restore_all,否则然后查看X86_EFLAGS_IF是否是禁止屏蔽中断,如果屏蔽中断然后执行restore_all,否则调用preempt_schedule_irq,然后处理信号。直到__preempt_count==0,跳出这个循环。(Linux中的信号机制优先级是:高优先级中断->低优先级中断->软中断->信号->进程运行。

#ifdef CONFIG_PREEMPT
ENTRY(resume_kernel)
    DISABLE_INTERRUPTS(CLBR_ANY)
need_resched:
    cmpl $0,PER_CPU_VAR(__preempt_count)
    jnz restore_all
    testl $X86_EFLAGS_IF,PT_EFLAGS(%esp)    # interrupts off (exception path) ?
    jz restore_all
    call preempt_schedule_irq
    jmp need_resched
END(resume_kernel)
#endif
    CFI_ENDPROC
...

这里我们进入到work_pending函数段,系统会进行调度,work_resched是个循环,一直在调度schedule,直到跳出循环。在这个汇编的末尾,kernel会判断$USER_RPL 值决定是返回kernel space还是user space,如果返回user space才会调用do_notify_resume()函数,这个函数是处理信号,调用do_signal()的基础,这个也从侧面反映了内核空间没有信号处理机制

work_pending:
    testb $_TIF_NEED_RESCHED, %cl
    jz work_notifysig
work_resched:
...
work_notifysig:             # deal with pending signals and
                    # notify-resume requests
#ifdef CONFIG_VM86
...
#else
    movl %esp, %eax
#endif
    TRACE_IRQS_ON
    ENABLE_INTERRUPTS(CLBR_NONE)
    movb PT_CS(%esp), %bl
    andb $SEGMENT_RPL_MASK, %bl
    cmpb $USER_RPL, %bl
    jb resume_kernel
    xorl %edx, %edx
    call do_notify_resume
    jmp resume_userspace

我们进入到do_notify_resume()函数,这个函数会调用do_signal,它就是处理系统调用或者中断发生信号的核心函数,这个函数又依次处理信号(通过调用handle_signal())和建立用户态堆栈(通过调用setup_frame()或setup_rt_frame())。当进程又切换到用户态时,因为信号处理程序的起始地址被强制放进程序计数器中,因此开始执行信号处理程序。

__visible void
do_notify_resume(struct pt_regs *regs, void *unused, __u32 thread_info_flags)
{
...
    /* deal with pending signal delivery */
    if (thread_info_flags & _TIF_SIGPENDING)
        do_signal(regs);
...
}

当处理程序终止时,setup_frame()或setup_rt_frame()函数(这里的rt代表实时系统中的rt结构。)放在用户态堆栈中的返回代码就被执行。这个代码调用sigreturn()或rt_sigrenturn()系统调用,相应的服务例程把正常程序的用户态堆栈硬件上下文拷贝到内核堆栈,并把用户态堆栈恢复到它原来的状态(通过调用restore_sigcongtext()).当这个系统调用结束时,普通进程就因此能恢复自己的执行。

8294024f-f541-3b5f-b47e-3a476c65f50a

 

所以综上所述:

如果我们自定义一个信号处理函数,那么这个处理函数定义在用户空间,1. 在进入内核态时,内核态堆栈中保存了一个中断现场,也就是一个pt_regs结构,中断返回地址就保存在pt_regts中的pc中,因此我们这里只要把当前进程的pt_regs中pc设置为sa_handler,然后返回到用户态就开始从sa_handler处开始执行了。

2.当信号的用户态处理函数执行结束时,需要再次进入内核态,内核专门提供了一个系统调用sys_sigreturn()(还有一个sys_rt_sigreturn())

3.在构建临时堆栈环境时,内核会把最初的pt_regs上下文备份到临时堆栈中(位于用户态堆栈),当通过系统调用sys_sigreturn()再次进入内核时,内核从用户态空间还原出原始的pt_regs。最后正常返回。

stack2

这里我们要考虑一种情况,进程处于TASK_INTERRUPTIBLE状态,某个进程向他发送一个信号,这时kernel将这个进程立即设置为TASK_RUNNING状态,但是系统宾没有完成当前的服务例程,并返回EINTR错误码,我们可以测试这个错误码,并重行发送新的system call,还有一些错误码用来判断kernel是否重行执行system call。

 

参考:

ULK  P445~P446

http://wangyuxxx.iteye.com/blog/1703252

http://blog.csdn.net/ce123_zhouwei/article/details/8570616

内核页表和进程页表

May 28th, 2015

最近在看vmalloc()分配代码,我们知道当通过alloc_page()分配出来page后,需要将这些分散的物理页框page映射到vmalloc区,这里我们就要修改内核页表,以前我学页表是把内核空间与用户空间割裂学习的,导致二者无法很好地衔接,这里我会把两个概念重新解释清楚。

下面代码映射到vmalloc区的关键就是map_vm_area()函数,

for (i = 0; i < area->nr_pages; i++) {
        struct page *page;

        if (node == NUMA_NO_NODE)
            page = alloc_page(alloc_mask);
        else
            page = alloc_pages_node(node, alloc_mask, order);
...
    }

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

拿IA32架构的虚拟地址来说,0~3GB属于用户空间,用户空间是由每个进程的task_struct.mm_struct.pgd的成员变量,这个指向的就是进程页表。而3G~4GB属于内核空间,这个页表是由内核页目录表管理,存放在主内核页全局目录init_mm.pgd(swapper_pg_dir)中

struct mm_struct init_mm = {
         .mm_rb          = RB_ROOT,
         .pgd            = swapper_pg_dir,
         .mm_users       = ATOMIC_INIT(2),
         .mm_count       = ATOMIC_INIT(1),
         .mmap_sem       = __RWSEM_INITIALIZER(init_mm.mmap_sem),
         .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
         .mmlist         = LIST_HEAD_INIT(init_mm.mmlist),
         INIT_MM_CONTEXT(init_mm)
};

进程切换切换的是进程页表:即将新进程的pgd(页目录)加载到CR3寄存器中。而内核页表是所有进程共享的,每个进程的“进程页表”中内核态地址相关的页表项都是“内核页表”的一个拷贝。

vmalloc区发生page fault

在vmalloc区发生page fault时,将“内核页表”同步到“进程页表”中。这部分区域对应的线性地址在内核使用vmalloc分配内存时,其实就已经分配了相应的物理内存,并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新。

所以linux中,只有进程的页表是时刻在更换的,而内核页表全局只有一份,所有进程共享内核页表!

物理内存管理:vmalloc()的设计实现

May 26th, 2015

对于内核空间,根据不同的映射规则,可以将整个内核空间划分为四大部分:物理内存映射区、vmalloc区、永久内核映射区和固定映射的线性地址区域。这里我们只来谈谈vmalloc()的分配。首先我们来看一下之前的一个图:

phy_addr

vmalloc()区域是从VMALLOC_START开始,一直到VMALLOC_END结束,中间有PAGE_SIZE大小的guard_page作为保护,以防止防止非法的内存访问,内核中使用vm_struct结构来表示每个vmalloc区,也就是说,每次调用vmalloc()函数在内核中申请一段连续的内存后,都对应着一个vm_struct,kernel 中所有的vmalloc区组成一个链表,链表头指针为vmlist

static struct vm_struct *vmlist __initdata;

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

这里我们只介绍几个重要的字段:
next:所有的vm_struct结构组成一个vmlist链表,该字段指向下一个节点;
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()-> __vmalloc_node_flags() -> __vmalloc_node() -> __vmalloc_node_range() ,通过这几层调用,就会向最终的__vmalloc_node_range()传入很多参数,GFP_KERNEL|__GFP_HIGHMEM表明内存管理子系统将从高端内存区(ZONE_HIGHMEM)中分配内存空间;NUMA_NO_NODE表示当前不是NUMA架构。

void *vmalloc(unsigned long size)
{
    return __vmalloc_node_flags(size, NUMA_NO_NODE,
                    GFP_KERNEL | __GFP_HIGHMEM);
}
static inline void *__vmalloc_node_flags(unsigned long size,                             
                    int node, gfp_t flags)
{
    return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
                    node, __builtin_return_address(0));
}
static void *__vmalloc_node(unsigned long size, unsigned long align,                    
                gfp_t gfp_mask, pgprot_t prot,
                int node, const void *caller)
{
    return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
                gfp_mask, prot, node, caller);
}

这里我们进入真正分配vmalloc区域的函数__vmalloc_node_range(),我们只要关注第三个第四个参数,就是vm区域的开始地址与结束地址,__vmalloc_node_range()一开始会先修正一下size对齐,PAGE_ALIGN将size的大小修改成页大小的倍数。进行size合法性的检查,如果size为0,或者size所占页框数大于系统当前空闲的页框数(totalram_pages),将返回NULL,申请失败。

如果分配的内存区大小合法,__get_vm_area_node()中的alloc_vmap_area()将在整个非连续内存区中查找一个size大小的子内存区。该函数先遍历整个vmlist链表,依次比对每个vmalloc区,直到找到满足要求的子内存区结束。在这里面还做好了内核页表的映射等操作。

建立好了vm_struct 结构,下面就要分配使用__vmalloc_area_node()为这个vmalloc内存区分配真正的物理页。

void *__vmalloc_node_range(unsigned long size, unsigned long align,
            unsigned long start, unsigned long end, gfp_t gfp_mask,
            pgprot_t prot, int node, const void *caller)
{
    struct vm_struct *area;
    void *addr;
    unsigned long real_size = size;
                                                                                                                                 
    size = PAGE_ALIGN(size);
    if (!size || (size >> PAGE_SHIFT) > totalram_pages)
        goto fail;
    
    area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED,
                  start, end, node, gfp_mask, caller);
    if (!area)
        goto fail;
    
    addr = __vmalloc_area_node(area, gfp_mask, prot, node);
    if (!addr) 
        return NULL;
...    
    return addr;
....

__vmalloc_area_node()的实现

首先根据传入的vm_struct *area 获取要分配的物理页大小,这里的大小包括了guard_page,所以要在get_vm_area_size()减去这个PAGE_SIZE,翻译过来就是nr_pages = (area->size – PAGE_SIZE) >> PAGE_SHIFT;抹去低位,也就是要分配的page数,然后nr_pages * sizeof(struct page *)也就是真正的物理page大小。

根据这个array_size大小,我们可以判断要分配的是大于PAGE_SIZE还是小于PAGE_SIZE,大于的话就递归分配pages数组,小于的话,通过kmalloc_node()为pages数组分配一段连续的空间,这段空间位于内核空间的物理内存线性映射区。然后将分配好的page加入area,更新area中的pages。

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

    nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
    array_size = (nr_pages * sizeof(struct page *));

    area->nr_pages = nr_pages;
    /* Please note that the recursion is strictly bounded. */
    if (array_size > PAGE_SIZE) {
        pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                PAGE_KERNEL, node, area->caller);
        area->flags |= VM_VPAGES;
    } else {
        pages = kmalloc_node(array_size, nested_gfp, node);
    }
    area->pages = pages;

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

for (i = 0; i < area->nr_pages; i++) {
        struct page *page;

        if (node == NUMA_NO_NODE)
            page = alloc_page(alloc_mask);
        else
            page = alloc_pages_node(node, alloc_mask, order);

        if (unlikely(!page)) {
            /* Successfully allocated i pages, free them in __vunmap() */
            area->nr_pages = i;
            goto fail;
        }
        area->pages[i] = page;
        if (gfp_mask & __GFP_WAIT)
            cond_resched();
    }

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

fail:
    warn_alloc_failed(gfp_mask, order,
              "vmalloc: allocation failure, allocated %ld of %ld bytes\n",
              (area->nr_pages*PAGE_SIZE), area->size);
    vfree(area->addr);
    return NULL;
}

这里当kernel分配完page,并将信息输入到area中后,这些分散的页框并没有映射到area所代表的那个连续vmalloc区中。使用map_vm_area()将完成映射,它依次修改内核页表,将pages数组中的每个页框分别映射到连续的vmalloc区中。

 

map_vm_area()工作原理会在下一篇文章中说明!

[1] http://lxr.free-electrons.com/source/mm/vmalloc.c#L1557

物理内存管理:Page的逆向映射

May 18th, 2015

kernel中包含着庞大的元数据struct page,一个struct page管理一个4k大小的物理内存(默认),之前我们进行的是从task_struct向vm_area_struct一直到最后的struct page的寻找过程。

但是从RAS角度,如果一个page发生问题,那么如何从struct page向上找到对应的page table呢(找到page table也就找到了pid、task_struct….)?这时候就要使用逆向映射

为了尽可能减小strut page的大小,Andrea复用了page的struct address_space *mapping数据结构来表示不同类型的页,主要分为三个类型swap、page cache、anonymous page。这里我们只说明page cache 与 anonymous page的逆向映射。

0.什么是匿名页?

想说明匿名页,必须首先说明什么是映射页。在用户态下打开一个文件,kernel会使用map() 映射文件的某个部分,这个部分在用户态地址空间存在地址,这种页要被回收的时候,会检查是否是dirty,如果为dirty,则需要写回相应的磁盘文件。

而匿名页没有对应了打开的磁盘文件,比如进程的用户态堆和stack可以称为匿名页,当匿名页过长时间驻留内存时,kernel可以要把它保存到一个特定的磁盘分区,这就是swap分区!

首先我们看一下struct page的结构:

struct page {
          /* First double word block */
          unsigned long flags;            /* Atomic flags, some possibly
                                           * updated asynchronously */
          union {
                  struct address_space *mapping;  /* If low bit clear, points to
                                                   * inode address_space, or NULL.
                                                   * If page mapped as anonymous
                                                   * memory, low bit is set, and
                                                   * it points to anon_vma object:
                                                   * see PAGE_MAPPING_ANON below.
                                                   */
                  void *s_mem;                    /* slab first object */
          };

          /* Second double word */
          struct {
                  union {
                          pgoff_t index;          /* Our offset within mapping. */
 ....

struct address_space *mapping是确定页是映射还是匿名

  • mapping为空表示该页属于交换高速缓存swap;
  • mapping非空,且最低位是1,表示该页为匿名页,同时mapping字段中存放的是指向anon_vma描述符的指针;
  • mapping非空,且最低位是0,表示该页为映射页;同时mapping字段指向对应文件的address_space对象。

而第二部分的pgoff_t index是用来指明偏移量的,在映射页中是以页为单位偏移。

1.当我们判断这个页是映射页时,每个page的mapping都指向一个对应的address_space,这个结构是page cache的核心,可以通过这个找到具体的inode!

2.当我们判断这个页是匿名页时,那么这个page指向struct anon_vma

但是这里kernel在2010年提交过一个补丁,为了解决大量fork出来的子进程占用大量anon_vma结构,他们的结构完全相同,而且这种结构导致匿名映射寻找也是O(N)的复杂度。所以引入了struct anon_vma_chain

假设系统只有一个task_struct时,组织结构如下:

anon_map

struct anon_vma_chain {
         struct vm_area_struct *vma;
         struct anon_vma *anon_vma;
         struct list_head same_vma;   /* locked by mmap_sem & page_table_lock */
         struct rb_node rb;                      /* locked by anon_vma->rwsem */
         unsigned long rb_subtree_last;
 #ifdef CONFIG_DEBUG_VM_RB
         unsigned long cached_vma_start, cached_vma_last;
 #endif
};

这样子就形成了anon_vma与vm_area_struct N对N的组织形式,当父进程fork出一个子进程,那么这个子进程拥有一个vm_area_struct,当然也就拥有一个anon_vma,除非发生COW,否则父子进程共享vm_area_struct。

this patch changes the way anon_vmas and VMAs are linked, which allows us to associate multiple anon_vmas with a VMA. At fork time, each child process gets its own anon_vmas, in which its COWed pages will be instantiated. The parents’ anon_vma is also linked to the VMA, because non-COWed pages could be present in any of the children.

3.刚才说的是正向的一个匿名页查找,现在我们反过来,一直一个struct page,找到这个匿名页的page table。结合正向的结构图与相关struct成员,我们可以画出相应的结构图,这里page指向anon_vma就是通过mapping做到的:

 

anonvma2

 

而anon_vma (AV),anon_vma_chain entry (AVC),vm_area_struct(VMA)三者的关系如下

avchain1

当father process fork()出来子进程后,会产生新的struct anon_vma ,这个结构就会分裂,创建VMA->创建AVC->创建AV。

avchain4

 

 

 

参考:

http://lwn.net/Articles/335768/

http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=5beb49305251e5669852ed541e8e2f2f7696c53e