swapper_pg_dir主内核页表、init和kthreadd、do_fork时新建子进程页表、vmalloc与kmalloc

都是以前看到一个点扯出的很多东西,当时做的总结,有问题欢迎讨论,现在来源难寻,侵删!

1、Init_task、idle、init和kthreadd的区别和联系

idle进程其pid=0,其前身是系统创建的第一个进程(我们称之为init_task),也是唯一一个没有通过fork或者kernel_thread产生的进程。

init_task是内核中所有进程、线程的task_struct雏形,它是在内核初始化过程中,通过静态定义构造出了一个task_struct接口,取名为init_task,然后在内核初始化的后期,在rest_init()函数中通过kernel_thread创建了两个内核线程:kernel_init线程,kthreadd内核线程, 前者后来通过演变,进入用户空间,成为所有用户进程的先祖,也就是1号进程init, 而后者则成为所有内核态其他守护线程的父线程, 也就是2号进程,负责接手内核线程的创建工作。

然后init_task通过变更调度类为sched_idle等操作演变成为idle进程, 此时系统中只有0(idle), 1(init), 2(kthreadd)3个进程, 然后执行一次进程调度, 必然切换当前进程到init。

实际上,init_task和idle描述的都是0号进程,该进程的comm字段是swapper。init_task是一个task_struct结构,该结构的comm字段是swapper,active_mm是init_mm(其中的pgd就是主内核页目录swapper_pg_dir);idle进程的名字也是swapper,通过内核模块方式可以得到0号进程名为swapper,在遍历内核维护的进程链表时,for_each_proces的起始点是:task=&init_task。

 

2、swapper_pg_dir所属者及作用

内核维持着一组自己使用的页表,也即主内核页全局目录。当内核在初始化完成后,其存放在 swapper_pg_dir 中,swapper_pg_dir其实就是一个页目录的指针,页目录指针在x86中是要被加载到cr3寄存器的,每个进程都有一个页目录指针,这个指针指示这个进程的内存映射信息,每当切换到一个进程时,该进程的页目录指针就被加载到了cr3,然后直到切换到别的进程的时候才更改。

swapper_pg_dir只是在内核初始化的时候被载入到cr3指示内存映射信息,之后在init进程启动后就成了idle内核线程的页目录指针。

/sbin/init由一个叫做init的内核线程exec而成,而init内核线程是原始的内核也就是后来的idle线程do_fork而成的,而在do_fork中会为新生的进程重启分配一个页目录指针,由此可见swapper_pg_dir只是在idle和内核线程中被使用。

可是它的作用却不只是为idle进程指示内存映射信息,更多的,它作为一个内核空间的内存映射模板而存在,在linux中,任何进程在内核空间就不分彼此了,所有的进程都会公用一份内核空间的内存映射,因此,内核空间是所有进程共享的,每当一个新的进程建立的时候,都会将swapper_pg_dir的768项以后的信息全部复制到新进程页目录的768项以后,代表内核空间。另外在操作3G+896M以上的虚拟内存时,只会更改swapper_pg_dir的映射信息,当别的进程访问到这些页面的时候会发生缺页,在缺页处理中会与swapper_pg_dir同步。

 

3 、子进程页表中内核空间目录项的建立

创建子进程时如果没有CLONE_VM表示共享地址空间,则会初始化子进程页表:do_fork->copy_process->copy_mm-> dup_mm-> mm_init初始化子进程页表,调用mm_alloc_pgd为子进程页表分配连续的页框,调用pgd_alloc初始化新的页目录 mm指向子进程的struct mm_struct,然后调用pgd_ctor设置子进程的页目录项(主要设置从主内核页表中复制内核地址空间的页目录项)

pgd_t *pgd_alloc(struct mm_struct *mm)
{/* 初始化子进程页表mm_alloc_pgd调用,初始化新的页目录 mm指向子进程的struct mm_struct*/
    pgd_t *pgd; /* 页目录指针 */
    pmd_t *pmds[PREALLOCATED_PMDS];/* 预先分配的页中间目录指针数组,只有在PAE模式下才会需要预分配出一些页中间目录 */

    /* 从伙伴系统中获取连续的页框作为子进程的页目录,PGALLOC_GFP标志会将新获取的内存清空 */
    pgd = (pgd_t *)__get_free_page(PGALLOC_GFP);

    if (pgd == NULL) /* 获取内存失败处理 */
        goto out;

    mm->pgd = pgd; /* 设置struct mm_struct中的pgd指向新的页目录,之前是指向父进程的页目录 */

    if (preallocate_pmds(pmds) != 0)/* 预分配页中间目录,正常情况下不会预分配 */
        goto out_free_pgd;

    if (paravirt_pgd_alloc(mm) != 0)
        goto out_free_pmds;

    /*
     * Make sure that pre-populating the pmds is atomic with
     * respect to anything walking the pgd_list, so that they
     * never see a partially populated pgd.
     */
    spin_lock(&pgd_lock);

    pgd_ctor(mm, pgd);/* 子进程的页目录项设置,主要设置了内核地址空间的页目录项,具体见后面代码 */
    pgd_prepopulate_pmd(mm, pgd, pmds);/* 子进程的页中间目录项设置,也是主要设置了内核地址空间的页中间目录项,如果没有预分配页中间目录,则直接返回 */

    spin_unlock(&pgd_lock);

    return pgd; /* 将设置好的页目录首地址返回 */

out_free_pmds:
    free_pmds(pmds);
out_free_pgd:
    free_page((unsigned long)pgd);
out:
    return NULL;
}

所有的进程共享内核空间,所以共享内核页表是很自然的事。理论上内核只有一个页表,对应的内核全局页目录swapper_pg_dir。每个进程有自己的页目录,共1024项,其中的768项后与swapper_pg_dir相同,指向的是内核空间。

但是每个进程的内核地址空间的页表并不是与主内核页表完全一致的,原因就是当vmalloc后修改了主内核页表,但是进程的页表并没有修改,当进程访问到这块虚拟地址空间时,进程的页目录项是空的,此时会产生一个缺页中断,在缺页中断中会先检查主内核页表中此项是否为空,如果不为空,则复制到进程的页表中。这样的最后结果就是进程页目录项的值与主内核页表完全一致,也就是指向了相同的页上级目录。结果最后只有访问到这段vmalloc区间的进程才会进行与主内核页表的更新,当然内核修改了主内核页表后一定要向所有CPU发送一个TLB刷新请求,因为有可能某个CPU上正在运行的进程对应的页表项保存在了TLB中。

而释放阶段就与进程没多大关系了,因为释放时主内核页表并不清空页上级目录、页中间目录和页表,只是单纯地清空页表项pte,而所有访问了此段vmalloc区间的进程除了页目录不同,它们的页上级目录、页中间目录、页表是同一个页框(可以简单理解为页目录中保存的目录项是一个指针,这些进程的vmalloc区间的目录项都指向了同一个页上级目录,同理页上级目录项又指向同一个页中间目录,页中间目录项又指向同一个页表),当页表项清空时实际上所有访问的进程的相应页表项都会被清空。

 

4、vmalloc

Vmalloc分配的虚拟地址空间则限于vmalloc_start与vmalloc_end之间。每一块vmalloc分配的内核虚拟内存都对应一个vm_struct结构体(这个结构体是通过kmalloc分配的,也就在0-896m物理内存中,可别和vm_area_struct搞混,那是进程虚拟内存区域的结构),不同的内核虚拟地址被4k大小的空闲区间隔,以防止越界。与进程虚拟地址的特性一样,这些虚拟地址与物理内存没有简单的位移关系,必须通过内核页表才可转换为物理地址或物理页。

Vmalloc区域并不和用户空间内存映射一样,通过page fault来装载页面的。vmalloc映射建立好后,逻辑地址,物理页面全部分配好,而且页表也已经更新好,只是此处为内核页表,并没有更新相关进程的页表。在vmalloc区发生page fault时,将“内核页表”同步到“进程页表”中。这部分区域对应的线性地址在内核使用vmalloc分配内存时,其实就已经分配了相应的物理内存,并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新。

在缺页异常的检查过程中,会判断这个地址所在空间,如果是在内核地址空间的VMALLOC区,那么内核会去内核页表中查看,该地址是不是在内核中有记录。如果有记录则把内核的addr对应的pmd项复制给进程的pmd项,意味着,进程和内核公用一个pte页表(感觉此处是x86_32系统下,只需要复制pmd,否则应该是还要复制pgd项和pud项,也就是说达成目的是只有页目录页框不同,页上级目录、页中间目录和页表的页框都是同一个)。

进程要vfree释放这个区域,其实修改的还是内核页表,会把addr对应的pte页表项设置为0。其它的都不做改变。那么当进程试图访问一个已经被释放区间的地址addr时候,由于它和内核对于addr的pmd项是一样的,所以,会继续去访问内核页表关于addr的pte页表,最后发现,pte页表项为0,又触发了缺页异常。

这次的缺页异常和上面分配的流程一样,只是最后对内核页表pte项做检查时候,发现内核页表关于addr的pte页表项是0,就会报错。这样就避免了进程的非法访问。Vmalloc缺页同步进程页表和vfree造成的缺页都在vmalloc_fault中:

/*
 * 32-bit:
 *
 *   Handle a fault on the vmalloc or module mapping area
 由于使用vmalloc申请内存时,内核只会更新主内核页表,
 所以当前使用的进程页表就有可能因为未与主内核页表同步导致这次异常的触发,
 因此该函数试图将address对应的页表项与主内核页表进行同步
 */
static noinline __kprobes int vmalloc_fault(unsigned long address)
{//do_page_fault会调用
    unsigned long pgd_paddr;
    pmd_t *pmd_k;
    pte_t *pte_k;

    /* Make sure we are in vmalloc area: /* 确定触发异常的地址是否处于VMALLOC区域*/
    if (!(address >= VMALLOC_START && address < VMALLOC_END))
        return -1;

    WARN_ON_ONCE(in_nmi());

    /*
     * Synchronize this task's top level page-table
     * with the 'reference' page table.
     *
     * Do _not_ use "current" here. We might be inside
     * an interrupt in the middle of a task switch..
     */
    /*获取pgd(最顶级页目录)地址,直接从CR3寄存器中读取。
     *不要通过current获取,因为缺页异常可能在上下文切换的过程中发生,
     *此时如果通过current获取,则可能会出问题 
     */
    pgd_paddr = read_cr3();//获取当前的PGD地址
    pmd_k = vmalloc_sync_one(__va(pgd_paddr), address);  //从主内核页表中,同步vmalloc区发生缺页异常地址对应的pmd
    if (!pmd_k)
        return -1;
    
    /*到这里已经获取了内核页表对应于address的pmd,并且将该值设置给了当前使用页表的pmd, 
      最后一步就是判断pmd对应的pte项是否存在

      如果同步后,相应的PTE还不存在,则说明该地址有问题引起的page_fault

      此处也就是vfree,vfree把内核页表addr对应pte页表项设为0,进程试图访问addr时,
      由于它和内核页表对于addr的pmd项一样,
      所以,会继续访问内核页表关于addr的pte页表,最后发现pte页表项为0,缺页走到这里,避免进程非法访问
    */
    pte_k = pte_offset_kernel(pmd_k, address); //获取pmd对应address的pte项 
    if (!pte_present(*pte_k)) //判断pte项是否存在,不存在则失败
        return -1;

    return 0;
}

顺势贴一下缺页异常的处理:

/*
 * This routine handles page faults.  It determines the address,
 * and the problem, and then passes it off to one of the appropriate
 * routines.
 *
 缺页异常被触发原因:
 1.程序设计的不当导致访问了非法的地址,不正常的,内核要采取各种可行的手段将这种异常带来的破坏减到最小
 2.访问的地址是合法的,但是该地址还未分配物理页框,正常

 用户空间的缺页异常可以分为两种情况--
1.触发异常的线性地址处于用户空间的vma中,但还未分配物理页,如果访问权限OK的话内核就给进程分配相应的物理页了
2.触发异常的线性地址不处于用户空间的vma中,这种情况得判断是不是因为用户进程的栈空间消耗完而触发的缺页异常,
如果是的话则在用户空间对栈区域进行扩展,并且分配相应的物理页,如果不是则作为一次非法地址访问来处理,内核将终结进程
*/
asmlinkage void __kprobes do_page_fault(struct pt_regs *regs,
                    unsigned long error_code,
                    unsigned long address)
{//
    unsigned long vec;
    struct task_struct *tsk;
    struct mm_struct *mm;
    struct vm_area_struct * vma;
    int fault;
    int write = error_code & FAULT_CODE_WRITE;
    unsigned int flags = (FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE |
                  (write ? FAULT_FLAG_WRITE : 0));

    tsk = current;
    mm = tsk->mm;//获取当前进程的地址空间
    vec = lookup_exception_vector();

    /*
     * We fault-in kernel-space virtual memory on-demand. The
     * 'reference' page table is init_mm.pgd.
     *
     * NOTE! We MUST NOT take any locks for this case. We may
     * be in an interrupt or a critical region, and should
     * only copy the information from the master page table,
     * nothing more.
     
      缺页地址位于内核空间。并不代表异常发生于内核空间,有可能是用户态访问了内核空间的地址

      vmalloc返回值是高端内存虚拟地址,但其分配内存时,其实就已经分配了相应的物理内存,
      并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,
      内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,
      发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新
     */
    if (unlikely(fault_in_kernel_space(address))) {  //判断address是否处于内核线性地址空间
        if (vmalloc_fault(address) >= 0) //处理vmalloc异常 将内核页表内容更新到进程页表的相应页表项
            return;
        if (notify_page_fault(regs, vec))
            return;
    
    /*bad_area_nosemaphore表明这次异常是由于对非法的地址访问造成的
    在内核中产生这样的结果的情况一般有两种:
    1.内核通过用户空间传递的系统调用参数,访问了无效的地址. 内核尚且能通过异常修正机制来进行修复
    2.内核的程序设计缺陷 导致OOPS错误了,内核将强制用SIGKILL结束当前进程
    */
        bad_area_nosemaphore(regs, error_code, address); 
        return;
    }

    if (unlikely(notify_page_fault(regs, vec)))
        return;

    /* Only enable interrupts if they were on before the fault */
    if ((regs->sr & SR_IMASK) != SR_IMASK)
        local_irq_enable();

    perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);

    /*
     * If we're in an interrupt, have no user context or are running
     * in an atomic region then we must not take the fault:
     */
    if (unlikely(in_atomic() || !mm)) {
        bad_area_nosemaphore(regs, error_code, address);
        return;
    }

retry:
    down_read(&mm->mmap_sem);

    vma = find_vma(mm, address); //试图寻找到一个离address最近的vma,vma包含address或在address之后 
     /*没有找到这样的vma则说明address之后没有虚拟内存区域,因此该address肯定是无效的, 
      通过bad_area()路径来处理,bad_area()的主体就是__bad_area()-->bad_area_nosemaphore()*/ 
    if (unlikely(!vma)) {
        bad_area(regs, error_code, address);
        return;
    }
    /*如果该地址包含在vma之中,则跳转到good_area处进行处理*/ 
    if (likely(vma->vm_start <= address))
        goto good_area;
    /*不是前面两种情况的话,则判断是不是由于用户堆栈所占的页框已经使用完,而一个PUSH指令 
      引用了一个尚未和页框绑定的虚拟内存区域导致的一个异常,属于堆栈的虚拟内存区,其VM_GROWSDOWN位 
      被置位*/
    if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
        bad_area(regs, error_code, address);//不是堆栈区域,则用bad_area()来处理
        return;
    }
    if (unlikely(expand_stack(vma, address))) {//堆栈扩展不成功同样由bad_area()处理 
        bad_area(regs, error_code, address);
        return;
    }

    /*
     * Ok, we have a good vm_area for this memory access, so
     * we can handle it..
     */
good_area:
    /*访问权限不够则通过bad_area_access_error()处理,该函数是对__bad_area()的封装,只不过 
    发送给用户进程的信号为SEGV_ACCERR*/ 
    if (unlikely(access_error(error_code, vma))) {
        bad_area_access_error(regs, error_code, address);
        return;
    }

    set_thread_fault_code(error_code);

    /*
     * 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, flags);/*分配新的页表和页框*/

    if (unlikely(fault & (VM_FAULT_RETRY | VM_FAULT_ERROR)))
        if (mm_fault_error(regs, error_code, address, fault))
            return;

    if (flags & FAULT_FLAG_ALLOW_RETRY) {
        if (fault & VM_FAULT_MAJOR) {
            tsk->maj_flt++;
            perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,
                      regs, address);
        } else {
            tsk->min_flt++;
            perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,
                      regs, address);
        }
        if (fault & VM_FAULT_RETRY) {
            flags &= ~FAULT_FLAG_ALLOW_RETRY;
            flags |= FAULT_FLAG_TRIED;

            /*
             * No need to up_read(&mm->mmap_sem) as we would
             * have already released it in __lock_page_or_retry
             * in mm/filemap.c.
             */
            goto retry;
        }
    }

    up_read(&mm->mmap_sem);
}

 

5、vmalloc和kmalloc的区别

1、kmalloc对应于kfree,分配的内存处于3GB~high_memory之间,这段内核空间与物理内存的映射一一对应,可以分配连续的物理内存;vmalloc对应于vfree,分配的内存[j1] 在VMALLOC_START~4GB之间,分配连续的虚拟内存,但是物理上不一定连续。

2、vmalloc() 分配的物理地址无需连续,而kmalloc() 确保页在物理上是连续的 。

3、kmalloc分配内存是基于slab,因此slab的一些特性包括着色,对齐等都具备,性能较好。物理地址和逻辑地址都是连续的。 

4、最主要的区别是分配大小的问题,比如你需要28个字节,那一定用kmalloc,如果用vmalloc,分配不多次机器就罢工了。 

尽管仅仅在某些情况下才需要物理上连续的内存块,但是,很多内核代码都调用kmalloc(),而不是用vmalloc()获得内存。这主要是出于性能的考虑。vmalloc()函数为了把物理上不连续的页面转换为虚拟地址空间上连续的页,必须专门建立页表项。还有,通过 vmalloc()获得的页必须一个一个的进行映射(因为它们物理上不是连续的),这就会导致比直接内存映射大得多的缓冲区刷新。因为这些原因,vmalloc()仅在绝对必要时才会使用,最典型的就是为了获得大块内存时,例如,当模块被动态插入到内核中时,就把模块装载到由vmalloc()分配的内存上。

源码都是Linux-3.10.1内核。书籍主要是《深入Linux内核架构》。以上来自网络部分侵删!!! 

posted @ 2019-03-10 21:34  前进的code  阅读(2225)  评论(1编辑  收藏  举报