Linux用户进程内存分配及二级页表PTE的二三事

Linux用户进程内存分配及二级页表PTE的二三事

我们在用调试器看Linux用户进程代码时,发现了一件很有意思的事情,在一段内存空间中,有一整页(4K)都是data abort,如下:

第一页4011c000数据正常

... ...

4011cfec [0xe28dd014]   add      r13,r13,#0x14

4011cff0 [0xe8bd40f0]   ldmfd    r13!,{r4-r7,r14}

4011cff4 [0xe12fff1e]   bx       r14

4011cff8 [0xe92d41f0]   stmfd    r13!,{r4-r8,r14}

4011cffc [0xe59f4064]   ldr      r4,0x4011d068


第二页4011d000 都是Data abort

4011d000       *** Data abort ***

4011d004       *** Data abort ***

4011d008       *** Data abort ***

4011d00c       *** Data abort ***

... ...


第三页 4011e000 数据正常

4011e000        数据正常


由于当时并不知道Linux是如何处理用户进程的内存分配,所以认为这是一个“错误”。既然有错误,我们决定找到这个问题发生的根源。

在追踪这个问题的过程中,leeming同学做了一个很BT的实验。我简单说两句,详细大家可以去看他的文章( http://blog.chinaunix.net/u3/99423/showart_2096904.html)。大概的方法就是将物理内存全部dump出来,通过第一页的代码,比如0xe59f4064,查找其在内存中的物理地址,再通过提取物理地址的前20位,就可以查找Linux系统的二级页表(对应ARM的TLB,Linux中叫PTE)。这个实验虽然看上去很不可思议,但实现起来并不复杂。最终得到了Linux存放在内存中的TLB数据。

每个表项是32bit

虚拟页地址    对应表项的内容

4011c000              3156caae

4011d000              00000000

4011e000        3156faae


       很有意思,Data abort的数据段,对应的PTE也是空的,这难道是一个系统错误?

       NO!经过进一步的学习后发现,在Linux系统中,这是一个很正常的现象。Linux在用户进程执行时并没有建立所有内存页面的映射,而是需要用到的时候再建立映射关系。当Linux用户进程访问到没有建立映射的页表(此时PTE指针为0),会调用相应的函数进行处理,或建立、或换出,具体执行这个操作的函数叫handle_pte_fault(),位于内核的mm/memory.c中。

       但是,Linux是如何进入缺页处理的呢?

有两种情况,都是利用了ARM处理器的异常中断进行相应的处理。

       第一种是程序顺序执行,正常页面的最后一条指令执行完后进入空页面,当空页面的第一条指令进入ARM处理器流水线的执行周期时,ARM处理器会报告一个指令预取异常中断,并跳转到地址0x0c,在Linux系统中由于使用了高地址向量表,所以会跳转到0xffff000c。此时ARM处理器进入ABORT状态,执行一系列代码保存现场(代码位于/arch/arm/kernel/entry-armv.s),然后进入SVC状态执行arch/arm/mm/fault.c中的do_PrefetchAbort(),最后会调用handle_pte_fault()处理缺页异常。

       第二种情况,页面中的程序执行时需要使用未分配页面的数据,比如“ldr r0,未分配页地址”。遇到这种情况,就不是指令预取异常了,而是数据访问异常(Data abort)。此时处理器依然会进入ABORT状态,跳转到0xffff0010执行相应的vector_dabt代码(entry_armv.s)保存状态,进入SVC态,执行do_DataAbort()函数,最后同样调用handle_pte_fault()处理缺页异常。

       因此,最开始遇到的情况:三个PTE,中间是空的,这是一个很正常的情况。因为第三页很可能由于前面的调用而已经建立,第二页却还没有建立。

       至于handle_pte_fault()如何处理缺页异常,我还没有看完,就不在本文讨论了。已知至少有do_no_page()、do_swap_page()、do_wp_page等多种方式,此为后话。

       通过跟踪用户程序,发现Linux用户进程基本所有的页面都是这样处理,因此处理器会很频繁的进出Abort状态,执行页面处理函数,这是会不会效率有点低了呢?待研究。

handle_pte_fault()

上文最后提到了handle_pte_fault()这个函数,用来处理页错,分配PTE。为了更清楚的了解PTE是如何申请到的,还是有必要深究一下。

handle_pte_fault()有几个函数用来检查当前pte的状态:

pte_present() 检测页面是否在内存中

pte_none() 检测页表项是否为空

pte_file() 同一地址多映射(此函数不重要)

vm->ops->fault标记位

内核用likely对其做了标记,说明这个标记一般满足,适用于已经建立好虚拟内存和文件的映射关系的情况。


1)针对满足pte_present()函数,即PTE不在内存中,会在以4个下函数中选择一个进行处理:

(1)do_linear_fault();

       最常见的情况,PTE表项为空,但满足vm->ops->fault,说明已经在内存中建立虚拟内存和文件的映射关系。

(2)do_anonymous_page();

       PTE表项为空,但是没有建立和文件的映射关系,说明是第一次demanding page。

(3)do_nonlinear_fault();

       PTE表项非空,满足pte_file()检查,对同一个物理地址做多个虚拟映射。

(4)do_swap_page();

       PTE表项非空,不满足pte_file()检查,此页将会被换出。


2)如果不满足pte_present(),即PTE在内存中,则会执行下面的COW操作:

       COW的全称叫做“copy on write”,即写时复制。这一块是涉及到两个进程共享操作的,简单的说两个进程可以共享页面(特别是fork出的进程),只有当一个进程需要写入文件时,才从同一页面复制一份副本。下面的函数调用do_wp_page(),将生成的复制页赋值给写进程。由于我仅仅跟了系统的“/sbin/init”,所以这里根本没有调用到。

      

附:

do_anonymous_page()函数的跟踪

       ——>mk_pte() 构建映射表

              ——>ptn_pte()

                     ——>__pte((ptn << 12 | pgprot_val)

最终生成的PTE为32bit,其中20bit物理地址,12bit控制信息

你可能感兴趣的:(Linux用户进程内存分配及二级页表PTE的二三事)