linux存储管理 MMU

来源:互联网 发布:淘宝上的好店铺推荐 编辑:程序博客网 时间:2024/05/18 00:13

/*
* =====================================================================================
*
*       Filename:  storage_managment存储管理原理和实现.c
*
*    Description:  存储管理和实现,庞大的模块,2周搞定
*
*        Version:  1.0
*        Created:  2010年12月29日 16时52分54秒
*       Revision:  none
*       Compiler:  gcc
*
*         Author:  Yang Shao Kun (), cdutyangshaokun@163.com
*        Company:  College of Information Engineering of CDUT
*
* =====================================================================================
*/

                1》    基本原理

1:分段存储:
    可执行文件在内存中的布局:
    最高地址-------------------------------------------------------------------------最低地址
    堆栈段(局部函数的数据)---空洞---BSS段(未初始化的数据)---数据段(经过初始化)---文本段(指令)

    考虑到,如果我们有一个以上的进程都在使用相同的代码,如果将代码段重数据和堆栈段中分离出来,单独进行地址交换,那么可以通过两个进程共享代码空间来节省内存,这就是使用分段储存管理系统的动机。实现的最简单的方法是用单基址寄存器和上下界寄存器。
    分段的特点:段数较少,但段的大小相对较大。
2:分页存储:
    在使用分页内存管理单元的系统中,将虚拟地址空间分为若干个页,每个页的大小
    k                                                     n-k
是2  字节,如果虚拟地址为n位,那么虚拟地址存储空间包括2  个页,较高的n-k位形
成页号,较低的k位是页的位移量。                    k
    页面(page frame):在物理内存空间中,页被映射的2   字节空间称为 页面。
    页表项(page table entry):
        1:页面号:这个字段确定页号映射到那个页面的问题。
        2:保护位:通常需要限制某些页的使用。
        3:存在位:当将页号变换成页面时就设值有效位。如果试图访问内存位置中的某页时,但此页没有进行有效变换,就会产生一个中断,被称为  缺页page fault。
        4:修改标志位(D):通常称为:modified bit ,但设置这一位时,意味着自上一次清除这一位后,已经又对这一页进行了写访问,这个标志可以告诉我们,当重新分配这个页面时,我们是否将这一页保存到磁盘。
        5:访问位(A):当要实现访问位时,需要通过硬件进行设置来表明:自上次清除这一位后,已经又访问了这一页。

分页的特点:页数相对较多,但页是较小的固定单元。

3:表现空闲内存块的两种方法:
    1:空闲的位图:通过这个可以将页标记为空闲或以分配。如果页面是空闲的,这
    一位设置为1,如果已经分配,则设置成0.
    2:空闲链表,使用链表来表示而已。采用在空闲块本身来保存大小和指向下一个
    块的指针。
4:碎片
    内部碎片:如果实际分配的内存超过了请求的内存,那么已分配块中的一些部分就
                 没有使用。
    外部碎片:如果可用的空闲块太小而不能满足任何要求。
5:选择策略
    1: 最先适应法---first fit,思想是,我们开始搜索链表,并找到大小大于或等
    于请求块的第一个块。结果一帮会造成低端内存区出现许多碎片,而高端内存区经
    常还有较大的空闲块。
    2:下一个适应法---next fit,可以使的内存空间分配更加均衡。在链表中最后一
    次分配之后的下一个块开始搜索空闲块。
    3:最佳适应法---best fit,在链表中搜索最小的块,但此块大于或等于请求的块
    的大小。
    4:最怀适应法--worst fit,为每个请求分配最大的块。
6:伙伴系统管理
    基本思想是:所有已经分配块的大小都是2的幂。
    它的步骤:在每一步都满足条件后,进行下一步:
    1:如果n小于最小分配单元,那么设置n为最小大小。
    2:对n取最接近2的幂的大小。
    3:如果没有大小为2 k 的幂的空闲块,那么递归的分配大小为2 k+1的幂的大小,
    并将此块分割成两个大小为2 k 的空闲块。
    4:为了响应请求,返回第一个大小为2 k 的空闲块。
7:过度分配技术
    交换:内存和磁盘之间的复制类型。

8:swapping processes out---进程的换出
    the kernel swaps a process out if it needs space in memory,whice may res
    ult from any of the following:
    1:the fork system call must allocate space for a child process.
    2:the brk system call incresses the size of a proces.
    3:a process become large by the natural growth of its stack.
    4:the kernel wants to free space in memory for processed it had previous
    lu swapped out and should now swap in.

    the kernel 没有必要将一进程的整个虚地址空间全部写道对换设备上去,instead
    it copyies the physical memory assigned to a process to the allocated sp
    ace on the swap device,ignoring unassigned virtual addresses.

    the kernel 决定将某一进程 swapping out 时,它使该进程中的每个区的引用数
    1,并把那些引用数减为0 的区换出。

    for the fork swap:
        one:enough memory,the parent process creat the child context.
        two:not enough memory,the kernel swaps the process out with out free
        ing the memory occupied by the in-core(parent)copy,when the swap is
        complete ,the child process exists on the swap device,parent 进程(p
        rocess)将 child 设置为 "ready-to-run"---就绪状态,然后返回用户态,si
        nce the child is in the "ready-to-run"state,the swapper will eventu
        lly swap it into memory---重新换入内存。

    expansion swap---扩展对换:
        如果进程需要的内存比当前以分配给它的内存还多,不管这是有栈增长引起的
        还是由于invocation of the brk system call 引起的,内核都要进行一次进
        程的 扩展对换。内核在对换设备上预定了enough space to contain the mem
        ory space of the process,including the newly requested space---足够
        的空间以容纳进程的存储空间,其中包括新申请的空间。then it adjusts th
        e address translation mapping of the process to account for the new
        vitual memory but does not assign physical memort(since none was ava
        ilible)---然后内核修改进程地址转换映射以适应新的虚存空间,但此时并不
        分配物理存储空间。最后内核通过一次通常的对换操作将该进程换出,同时将
        对换设备上新分配的空间清零,当以后按新的地址转换映射时来分配地址,这
        样该进程恢复执行就有了足够的空间。
9:swapping process in---进程的换入
    process 0 ,the swapper ,is the only process that swaps process into memo
    ry from swap devices.
    the clock handler measures the time that each process has been in core o
    r swapped out---时钟处理程序度量每一个进程在内存中换入和换出的时间。但对
    换process 被wakes up 时,它进行换入进程的工作,此时,它查找所有处在“就绪
    且换出---ready-to run but swapped out”,and selects one that has been swa
    pped out the longest.If has the enough free memory avaliable ,the swappe
    r swaps the process in.
    换入操作最终会产生如下的情况:
    1:对换设备上没有“就绪”的进程,这时,对换进程进入睡眠,直到一个对换设备
    上的进程被唤醒或内核换出一个"就绪"状态的进程。
    2:对换进程找到了应被换入的进程,但系统没有足够的内存空间,此时,对换进
    程试图换出另一进程,如果成功则重新启动对换算法,查找需要换入的进程。
    但是换出的进程不能是:
    zombie process--because they do not take up any physical memory.
    process locked in memory.
    选择换出进程以得到内存空间的算法有严重的缺陷:serious flaws:
    第一:it may swap out a process that does not provide enough memory for
    the incoming process.---an alternative strategy would be to swap out gro
    ups of process only if they provide enough memory for the incoming proce
    ss.
    第二:if the swapper sleeps because it could not find enough memory to s
    wap jin a process ,it searches again for a process to swap in although i
    t had previously chosen one.
    第三:if the swapper chooses a "ready-to-run",process to swap out ,it po
    ssible that the process had not executed since it was previously swapped
    in.
    第四:如果对换进程要换出一个进程,但在对换设备上又找不到空区,这时,可能
    会产生死锁。

    换出的是正在睡眠的process 而不是"ready-to-run"process.选取哪一个睡眠进程
    来换出取决于进程的优先权和它在内存驻留的时间。如果您没有睡眠进程,那么选
    择那个"ready-to-run",进程来换出取决于进程的nice 值和它在内存中驻留的时间

    A"ready-to-run"process must be core resident for at least 2 seconds befo
    re being swapped out,and a process to be swapped in must have been swapp
    ed out for at least 2 seconds.如果都不满足的话,就睡眠。

    the algorithm swapper:
    input :none
    output :none
    {
        loop:
            for(all swapped out process that are ready to run)
                pick process swapped out longest;
            if(no such procedd)
            {
                sleep(event must swap in);
                goto loop;
            }
            if(enough room in main memory for process)
            {
                swap process in;
                goto loop;
            }
        /*the loop2:here in revised algorithm*/
            for(all process loaded in main memory ,not zombie and locked in
                    memory)
            {
                if (therr is a sleeping process)
                {
                    choose process such that priority +residence time is num
                        erically highest;
                }
                else/*no sleeping processes*/
                    choose process such that residence time +nice is numeric
                        ally highest;
                if(chosen process not sleeping or residensy requirements no
                        t satisfied)
                    sleep(event must swap process in);
                else
                    swap out process;
                goto loop;/*loop2*/
            }
    }

10:demand paging---请求调页
    the implementation of a paging subsystem has two parts:
        1:swapping rarely used pages to a swapping device and 2:handling pag
        e faults.---将不常用的页面换到对换设备上去以及处理有效性错。
    data structures for demand paging---请求调页的数据结构
        支持低层存储管理和请求调页的主要内核数据结构有4个:page table entrie
        s---页表表项,磁盘块描述项---disk block descripors,页面表数据项---
        page frame data table,对换使用表---swap-use table.
        在系统的生存期内,内核仅为pfdata 分配一次空间,对其他数据结构则动态
        分配内存页。
11:the page-stealer process--偷页进程
    偷页进程:将不再是进程工作集的页偷偷地换出内存,the kernel creats the pa
    ge stealer during system initialization and invokes it throughout the li
    fetime of the system when low on free pages.
    the kernel wakes up the page stealer when the avaliable free memory in s
    ystem is below a low-water mark,and the page swaps out pages until the a
    vailable free memory in the system exceeds a high-water mark.

    总而言之--to summarize,there are two phases to swapping a page from memo
    ry.
        first:the page stealer finds the page eligible for swapping and plac
              es the page number on a list of pages to be swapped.
        second:the kernel copies the page to a swap device when convenient,t
               urn off the valid bit in the page table entry,decrements the
               pfdata table entry referencd count---将引用数减1,and places
               the pfdata table entry at the end of the free list if its ref
               rerence count is 0.

                           2》linux中具体的实现

页面目录:PGD
中间目录:PMD
页面表  :PT
cpu发出的是线性地址,linux中的处理步骤是这样的:
    1:用线性地址中最高的那个位段作为下标在PGD中找到相应的表项,该表项指向相应的中间目录PMD。
    2:用线性地址中的第二个位段作为下标在此PMD中找到相应的表项,该表项指向相应的页面表。
    3:用线性地址资料宏的第三个位段作为下标在此PMD中找到相应的表项PTE ,该表项中存放的就是指向物理页面的指针。
    4:线性地址中的最后位段为物理页面内的相对位移量,将此位移量与目标物理页面的起始地址相加变得到相应的物理地址。

大概的模型为:
   PGD----------PMD----------PT-------------位移

/*
* Traditional 2-level paging structure
*/
#define PGDIR_SHIFT    22 //这里的表示地址中的PGD下标位段的起始位置,为22,也就是22bit第23位。由于该位段是从第23位到32位,一共是10位。
#define PTRS_PER_PGD    1024

#define PTRS_PER_PTE    1024 //每个pGD表中指针的个数为1024.每个指针的大小的为4字节,故,在32位系统中,PGD表中的大小是4kb。

在文件pgtable_2level.h 中定义了另一个常数:
/* PGDIR_SHIFT determines what a third-level page table entry can map */

#define PGDIR_SIZE    (1UL << PGDIR_SHIFT) //也就是说PGD的每一个表项所代表的空间大小是1*2的22次方,而不是PGD本身所占的空间。
#define PGDIR_MASK    (~(PGDIR_SIZE-1))

32位地址意味着4G字节的虚存空间,linux内核将这4G空间分成两部分,将最高的1 G 字节----0xc0000000~0xffffffff 用于内核本身,称为系统空间。
而将较低的3G字节---0x0~0xbfffffff用做各个进程的用户空间。这样理论上每个进程可以使用的用户空间都是3 G。
虽然系统空间占据来每个虚拟空间中最高的1G字节,在物理的内存中确总是从最低的地址开始,所以对于内核来说,其地址的映射时很简单的线性映射,0xc0000000,就是两者之间的位移量。内核中的实现为:

#define __PAGE_OFFSET        (0xC0000000)//位移量,同时也代表用户空间的上限
#define PAGE_OFFSET        ((unsigned long)__PAGE_OFFSET)
#define __pa(x)            ((unsigned long)(x)-PAGE_OFFSET)//x是虚地址,这个宏是把虚拟地址转化成物理地址,只是为内核代码中需要知道与虚拟地址对应的物理地址时提供方便。
#define __va(x)            ((void *)((unsigned long)(x)+PAGE_OFFSET))//这个宏是把x物理地址,转化为虚地址。

                                        2》地址映射全过程
linux内核采用页式存储管理,虚拟地址空间划分成固定大小的“页面”,由MMU 在运行时将虚拟地址“映射”成某个物理内存页面中的地址。i386cpu一律队程序中使用的地址先进行段式映射,然后在进行页式映射。

段寄存器的格式定义:
    15                                 3    2    1    0
    -----------------index-------------|----ti---|-rpl--》request privileg level
    bit2 is zero is GDT,one is LDT
i386中cpu使用代码段寄存器cs的当前值来作为段式映射的“选择码”,也就是用它来作为段描述表中的下标。分微全局的和局部的段描述表。
#define start_thread(regs, new_eip, new_esp) do {        /
    __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));    /
    set_fs(USER_DS);                    /
    regs->xds = __USER_DS;                    /
    regs->xes = __USER_DS;                    /
    regs->xss = __USER_DS;                    /
    regs->xcs = __USER_CS;                    /
    regs->eip = new_eip;                    /
    regs->esp = new_esp;                    /
} while (0)
CPU内部的段寄存器:

CS——代码段寄存器(Code Segment Register),其值为代码段的段值;
DS——数据段寄存器(Data Segment Register),其值为数据段的段值;
ES——附加段寄存器(Extra Segment Register),其值为附加数据段的段值;
SS——堆栈段寄存器(Stack Segment Register),其值为堆栈段的段值;
FS——附加段寄存器(Extra Segment Register),其值为附加数据段的段值;
GS——附加段寄存器(Extra Segment Register),其值为附加数据段的段值。

也就是说,intel 的意图是将进程的映像分成代码段,数据段,堆栈段,但是linux内核却把这些统统弄成来代码段和数据段.也就时说,linux中只有代码段和数据段,而没有堆栈段。
现在来看看,USER_DS和USER_CS 时什么?
#ifndef _ASM_SEGMENT_H
#define _ASM_SEGMENT_H
                           ------idex------|TI|RPL
#define __KERNEL_CS    0x10 //0000 0000 0001 0 0  00
#define __KERNEL_DS    0x18 //0000 0000 0001 1 0  00
#define __USER_CS    0x23 //0000 0000 0010 0 0  11
#define __USER_DS    0x2B //0000 0000 0010 1 0  11

#endif
这是在i386的segment.h中的定义,也就是说,linux中只是用4中段寄存器的数值,两种用于内核本身,两种用于所有的进程。可以看出,在linux中内核几乎全部使用的是 DGT,内核的级别时 0级,最高,用户为 3级。

/*这是在arch/i386/kernel/head.s中定义的初始化GDT的内容*/
.data
442
443 ALIGN
444 /*
445  * This contains typically 140 quadwords, depending on NR_CPUS.
446  *
447  * NOTE! Make sure the gdt descriptor in head.S matches this if you
448  * change anything.
449  */
450 ENTRY(gdt_table)/*汇编的语法,相当与数组,gdt_table 作为基地值*/
451     .quad 0x0000000000000000    /* NULL descriptor */
452     .quad 0x0000000000000000    /* not used */
453     .quad 0x00cf9a000000ffff    /* 0x10 kernel 4GB code at 0x00000000 */
454     .quad 0x00cf92000000ffff    /* 0x18 kernel 4GB data at 0x00000000 */
455     .quad 0x00cffa000000ffff    /* 0x23 user   4GB code at 0x00000000 */
456     .quad 0x00cff2000000ffff    /* 0x2b user   4GB data at 0x00000000 */
457     .quad 0x0000000000000000    /* not used */
458     .quad 0x0000000000000000    /* not used */
459     /*
460      * The APM segments have byte granularity and their bases
461      * and limits are set at run time.
462      */
463     .quad 0x0040920000000000    /* 0x40 APM set up for bad BIOS's */
464     .quad 0x00409a0000000000    /* 0x48 APM CS    code */
465     .quad 0x00009a0000000000    /* 0x50 APM CS 16 code (16 bit) */
466     .quad 0x0040920000000000    /* 0x58 APM DS    data */
467     .fill NR_CPUS*4,8,0     /* space for TSS's and LDT's */

段描述项的定义:
    63            56 55        52 51      48 47                 40 39           32 31          16 15         8 7        0
    ---B31~B24----- |G|D/B|O|AV| |-L19~L16| P DPL S E ED/C R/W A| ---B23~B16--- |---B15~B0--- |------段上限L15~L0---|   
        基地址                        段上限                             基地址         基地址             段上限
其中:
    1:基地值是:32位,也就说,每个段都是从0 地址开始的整个4GB空间。
       对照上面的4个段寄存器的值,即,将上面的张开成段描述项的形式,我们可以看出,基地址,全部为0,也就是,在段式映射中虚地址到线性地址的映射保持原值不变。
    2: G:为1时,表示段长以4KB为单位,为0 时,表示以字节为单位。
        D/B:=1时,表示对该段的访问为32位的指令,=0,为16位指令。
        O:永远为0;
        AV:可由软件使用,cpu忽略改位。
    3: P:=1,该段在内存中。
        DPL:特权级别。
        S:=1,表示一般的代码段或数据段,=0,表示专用于系统管理的系统段。
        E:=1,代码段。
        ED:=0,向上升(数据段),=1,向下升(堆栈段)。
        C:=0,忽视特权级别,=1,依照特权级别。
        R/W:读写为,1,时有效。
        A:=1,已被访问过。
linux中的段式映射,只是检查DPL和段的类型,但是在页式中也会检查,就显得在段式中比较的多余,但是intel中规定,必须先段式,厚页式映射。

页式映射:
    在页式映射中,每个进程都有其自身的页面目录PGD,指向这个页面目录的指针保持在每个进程的mm_struct 数据结构中。每当调度一个进程进入运行的时候,内核都要为即将运行的进程设置好控制寄存器CR3,而MMU的硬件则总是从CR3映射中取得指向当前页面目录的指针。但是,cpu在执行程序时用的时虚存地址,而MMU 硬件进行映射时所用的则时物理地址。这个过程在include/asm-i386/mmu_contex.h 中完成的。

static inline void switch_mm(struct mm_struct *prev,struct mm_struct *next ,struct task_struct *tsk,unsigned cpu)
{
    ....
    asm volatile("movl %0,%%cr3"::"r"(__pa(next->pgd)));//将下一个进程的页面目录PGD的物理地址装入寄存器%%cr3.也就时使用不同的页面目录。
    ....
}
ox080483b4线性地址在页式管理中的映射:
0000 1000 00|00 0100 1000| 0011 1011 0100
----32------|-----72------
首先,我们知道最高的10位时PGD,即页面目录表的下标,找到相应的表项。该表项的高20位指向一个页面表PT,加上12个0 就得到该页面表指针。
then:找到页面表后,cpu在来看线性地址的中的中间10位,于是cpu就以此为下标去已经找到的页面表中找到相应的表项。
end:该表项中的高20位指向目标页面,在其起始地址上加上线性地址中的最低12位,就得到来最终的物理内存地址。

                                                        3》重要的数据结构和函数
/*
* These are used to make use of C type-checking..
*/
#if CONFIG_X86_PAE
typedef struct { unsigned long pte_low, pte_high; } pte_t;
typedef struct { unsigned long long pmd; } pmd_t;
typedef struct { unsigned long long pgd; } pgd_t;
#define pte_val(x)    ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
#else
typedef struct { unsigned long pte_low; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
#define pte_val(x)    ((x).pte_low)
#endif
#define PTE_MASK    PAGE_MASK

页面目录PGD,中间目录PMD和页面表PT分别是由表项pgd_t,pmd_t,以及Pte_t构成的数组。定义在/include/asm-i386/page.h中。
因为,表项pte作为指针,实际上只需要高20位,同时,所有的物理页面都是跟4K字节的边界对齐的,因而物理页面起始地址的高20位又可以看作是物理页面的序号。所以pte_t中的低12位用于页面的状态信息和访问权限。
typedef struct { unsigned long pgprot; } pgprot_t;//用来说明页面保护结构,参数pgprot的值与i386MMU的页面的低12位相对应
#define _PAGE_PRESENT    0x001
#define _PAGE_RW    0x002
#define _PAGE_USER    0x004
#define _PAGE_PWT    0x008
#define _PAGE_PCD    0x010
#define _PAGE_ACCESSED    0x020
#define _PAGE_DIRTY    0x040
#define _PAGE_PSE    0x080    /* 4 MB (or 2MB) page, Pentium+, if present.. */
#define _PAGE_GLOBAL    0x100    /* Global TLB entry PPro+ */

#define _PAGE_PROTNONE    0x080    /* If not present,对应与页面表项中的bit7,保留不用 */
在include/asm-i386/pgtable.h中对这几位做来设置。
在实际的使用中,将pte中的指针部分和pgprot合在一起就能得到实际用于页面表中的表项。具体的算法是由pgtable.h中宏定义的mk_pte完成的。
#define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))
涉及的宏还有:
#define pgprot_val(x)    ((x).pgprot)
#define __pte(x) ((pte_t) { (x) } )
下面这个宏用来把一个表项的值设置到一个页面表项中,这个宏操作定义于include/asm-i386/pgtable_2level.h中

#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
在映射的过程中,MMU首先检查的是P标志位,也就时402行的宏,它指示着所映射的页面是否在内存中,只有在p标志位为1的时候,MMU才会完成映射的全过程,否则就会因不能完成映射而产生一次缺页异常,此时表项中的其它内容对MMU就没有任何意义来。
    如果把整个物理内存看作看成是一个物理页面的数组,那么页面表项中的表项值的高20位就是数组的下标,也就时物理页面的序号,因为低的12位都是0,按照4K来对齐。那么用这个下标就可以在page结构数组中找到代表目标物理页面的数据结构。在include/asm-i386/pgtable_2level.h中定义来一个宏来处理。
#define pte_page(x)        (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT)))) //page_shift在i386中定义的值时12.
mem_map 是page结构指针,page数据结构定义在include/linux/mm.h中
/*
* Try to keep the most commonly accessed fields in single cache lines
* here (16 bytes or greater).---使得联系紧密的若干成分在执行时被填入高速缓存的同一缓存线上,
* This ordering should be particularly beneficial on 32-bit processors.
*
* The first line is data used in page cache lookup, the second line
* is used for linear searches (eg. clock algorithm scans).
*/
typedef struct page {
    struct list_head list;//和free_area_page中的双向链表有关?
    struct address_space *mapping;//该结构用来说明,管理当前页的结构信息。
    unsigned long index;//当页面的内容来自一个文件时,表示该页面在文件中的序号,当页面内容被换出到swap device 上,但还保留着内容作为缓冲,则表示指明来页面的去向
    struct page *next_hash;
    atomic_t count;
    unsigned long flags;    /* atomic flags, some possibly updated asynchronously */
    struct list_head lru;
    unsigned long age;
    wait_queue_head_t wait;
    struct page **pprev_hash;
    struct buffer_head * buffers;
    void *virtual; /* non-NULL if kmapped */
    struct zone_struct *zone;//zone_struct 数据结构,用来管理物理页面中划分的物理页面的总类,ZONE_DMA,ZONE_NORMAL.
} mem_map_t;

系统中每一个物理页面都有一个page结构,系统在初始化的时根据物理页面内存的大小建立的一个page结构数组mem_map,作为物理页面的仓库,里面的每个page结构数据结构都代表着系统中的一个物理页面,每个物理页面的page结构在这个数组里的下标就是该物理页面的序号,仓库里的物理页面被划分成ZONE_DMA,ZONE_NORMAL两个管理区,还有可能由第三个管理区ZONE_HIGHMEM。
其中第一个ZONE_DMA是用来给DMA用的,不经过MMU直接提供地址映射。

每个管理区都有一个数据结构---zone_struct。
typedef struct zone_struct {
    /*
     * Commonly accessed fields:
     */
    spinlock_t        lock;
    unsigned long        offset;//表示该分区在mem_map中的起始页,mem_map即初始化的时候建立的一个数组--page结构,仓库。一旦建立起来管理区,每个物理页面便永久的属于某一个管理区,具体取决于页面的起始地址。
    unsigned long        free_pages;
    unsigned long        inactive_clean_pages;
    unsigned long        inactive_dirty_pages;
    unsigned long        pages_min, pages_low, pages_high;

    /*
     * free areas of different sizes
     */
    struct list_head    inactive_clean_list;//不活跃 干净页面列表。
    free_area_t        free_area[MAX_ORDER];//    typedef struct free_area_struct {struct list_head    free_list;unsigned int        *map;} free_area_t;一个队列,用来分配连续的多个物理页面--块,这个队列来保持长度为2的页面块以及2的幂的页面块,直到2的MAX_ORDER,也就是说最大的页面块可以达到1024个页面,也就时4M了。

    /*
     * rarely used fields:
     */
    char            *name;
    unsigned long        size;
    /*
     * Discontig memory support fields.
     */
    struct pglist_data    *zone_pgdat;
    unsigned long        zone_start_paddr;
    unsigned long        zone_start_mapnr;
    struct page        *zone_mem_map;
} zone_t;

由于非均匀存储结构---Non-uniform memory architechure 的引入,管理区,不在是属于最高层的机构,而是在每个存储节点---质地相同的区域,都有至少两个管理区,前面的page数据结构现在时从属于具体的节点,不在是全局的了。
#define NR_GFPINDEX        0x100//表示分配的策略的总数。

typedef struct pglist_data {
    zone_t node_zones[MAX_NR_ZONES];//节点里面的区的个数,最多3个,用数组来管理。
    zonelist_t node_zonelists[NR_GFPINDEX];//每个存储节点的分配策略。
    struct page *node_mem_map;
    unsigned long *valid_addr_bitmap;
    struct bootmem_data *bdata;
    unsigned long node_start_paddr;
    unsigned long node_start_mapnr;
    unsigned long node_size;//节点的大小
    int node_id;//节点的id,序号。
    struct pglist_data *node_next;//pglist_data节点的链表
} pg_data_t;

虚拟空间的数据结构:
    虚存空间的管理不像物理空间的管理那样有一个物理页面仓库,而是以进程为基础,每个进程都有自己的虚存空间。
对虚存空间区间的抽象在一个重要的数据结构中:/inclue/linux/mm.h中的vm_area_struct数据结构。
/*
* This struct defines a memory VMM memory area. There is one of these
* per VM-area/task.  A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
    struct mm_struct * vm_mm;    /* VM area parameters */
    unsigned long vm_start;//虚存空间的开始
    unsigned long vm_end;//虚存空间的结束

    /* linked list of VM areas per task, sorted by address */
    struct vm_area_struct *vm_next;//将属于同一个进程的所有区间按虚拟地址的高低次序链接在一起。
    /*区间的划分不仅仅取决与地址的连续性,也取决与取决的其它属性,主要是对虚存页面的访问权限,如果一个地址范围内的前一半页面和后一半页面有不同的访问权限或属性,那么就要划分为2个区间,所以,同一个区间里面的所有的页面的具有相同的属性和访问权限*/

    pgprot_t vm_page_prot;//也就是低12的值,用来设置为属性和访问权限等等。
    unsigned long vm_flags;

    /* AVL tree of VM areas per task, sorted by address */
    short vm_avl_height;
    struct vm_area_struct * vm_avl_left;
    struct vm_area_struct * vm_avl_right;

    /* For areas with an address space and backing store,
     * one of the address_space->i_mmap{,shared} lists,
     * for shm areas, the list of attaches, otherwise unused.
     */
    struct vm_area_struct *vm_next_share;
    struct vm_area_struct **vm_pprev_share;

    struct vm_operations_struct * vm_ops;//指向该结构体,该结构体中是函数指针pointer to the functons,提供虚存空间的打开和关闭建立映射等,相当于一个函数跳转表,结构中主要和一些文件操作有关的函数指针。
    unsigned long vm_pgoff;        /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
    struct file * vm_file;
    unsigned long vm_raend;
    void * vm_private_data;        /* was vm_pte (shared mem) */
};
vm_ops指向的数据吗结构:
/*
* These are the virtual MM functions - opening of an area, closing and
* unmapping it (needed to keep files on disk up-to-date etc), pointer
* to the functions called when a no-page or a wp-page exception occurs.
*/
struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int write_access);
};
vm_mm指向的数据结构:
struct mm_struct {
    struct vm_area_struct * mmap;        /* list of VMAs---建立虚存空间结构的单链线性队列 */
    struct vm_area_struct * mmap_avl;    /* tree of VMAs ---建立虚存空间结构的avl树的根节点*/
    struct vm_area_struct * mmap_cache;    /* last find_vma result ---用来指向最近一次用到的那个虚存结构,这是应为程序用到的地址常常具有局部性*/
    pgd_t * pgd;//指向该进程的页面目录,当进程进入运行时,就将这个指针转换成物理地址。
    atomic_t mm_users;            /* How many users with user space? */
    atomic_t mm_count;            /* How many references to "struct mm_struct" (users count as 1) */
    int map_count;                /* number of VMAs ---虚存空间的数目,该进程有个虚存区间*/
    struct semaphore mmap_sem;//用于P,V操作的信号量,信号量是一个整数,当大于等于零时代表可供并发进程使用的资源实体数,小于零时则表示正在等待使用的临界区的进程数。P原语加一,V原语减一。
    spinlock_t page_table_lock;

    struct list_head mmlist;        /* List of all active mm's */

    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, total_vm, locked_vm;
    unsigned long def_flags;
    unsigned long cpu_vm_mask;
    unsigned long swap_cnt;    /* number of pages to swap on next pass */
    unsigned long swap_address;

    /* Architecture-specific MM context */
    mm_context_t context;//局部描述表,在linux中基本不用
};
这是比vm_area_struct 更高的数据结构。每个进程只有一个mm_struct结构,在每个进程的“进程控制块”中,即task_struct结构中,有一个指针指向该结构,可以说,该结构是对整个用户空间的抽象,也是总的控制结构。
给定一个属于某个进程的虚拟地址,要求找到其所属的区间以及相应的vma_area_struct 结构,这是由 find_vma函数实现的其代码在/mm/mmap.c中。
/* 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;
        /*看在cache中命中没有*/
        if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {//如果cache中找到了虚存空间vma,同时虚拟地址addr在虚存空间的范围内的条件不成立
            if (!mm->mmap_avl) {/*没有命中,同时,mmap_avl指针为零---没有建立来avl结构,线性队列中搜索*/
                /* Go through the linear list. */
                vma = mm->mmap;
                while (vma && vma->vm_end <= addr)
                    vma = vma->vm_next;
            } else {/*如果建立了avl结构,就在avl结构中搜索*/
                /* Then go through the AVL tree quickly. */
                struct vm_area_struct * tree = mm->mmap_avl;//将avl树的根节点赋值给tree
                vma = NULL;
                for (;;) {
                    if (tree == vm_avl_empty)//#define vm_avl_empty    (struct vm_area_struct *) NULL,avl为空树
                        break;
                    if (tree->vm_end > addr) {//在虚存空间vm_area_struct结构区的地址范围内,先判断根节点及其左字树
                        vma = tree;
                        if (tree->vm_start <= addr)
                            break;
                        tree = tree->vm_avl_left;//将
                    } else//判断右子树
                        tree = tree->vm_avl_right;
                }
            }
            if (vma)
                mm->mmap_cache = vma;//找到了,将mmap_cache 指针置成所找到的vm_area_struct结构。
        }
    }
    return vma;//返回值如果为NULL,表示该地址所属的区间,还没有建立,此时通常就得要建立一个新的虚存区间结构,在调用insert_vm_struct()将其插入到mm_struct的线性队列或者时AVL树中去。
}

                                                4》越界访问
异常处理服务程序主体函数do_page_fault():
/*
* This routine handles page faults.  It determines the address,
* and the problem, and then passes it off to one of the appropriate
* routines.
* ®s,指向异常发生前夕cpu各寄存器内容的一份副本。
* error_code,指明映射失败的具体原因。
*
* error_code:
*    bit 0 == 0 means no page found, 1 means protection fault
*    bit 1 == 0 means read, 1 means write
*    bit 2 == 0 means kernel, 1 means user-mode
*/
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
    struct task_struct *tsk;
    struct mm_struct *mm;
    struct vm_area_struct * vma;
    unsigned long address;
    unsigned long page;
    unsigned long fixup;
    int write;
    siginfo_t info;

    /* get the address */
    __asm__("movl %%cr2,%0":"=r" (address));//当i386cpu产生“页面错“异常时,cpu将导致映射失败的线性地址放在控制寄存器CR2中,而这显然是相应服务程序所必须的信息,可是c语言中没有相应的语言成分来读取cr2中的内容。该汇编代码输出cr2中的内容,输出内容放在一个寄存器中。

    tsk = current;//取得task_struct数据结构
                    //#ifndef _I386_CURRENT_H
                    //#define _I386_CURRENT_H

                    //struct task_struct;

                    //static inline struct task_struct * get_current(void)
                    //{
                        //struct task_struct *current;
                        //__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));//每个进程的内核栈的大小是8192,也就时8K,内核堆栈占用的内存地址都是以这个值对齐的,底13清零后就得到堆栈尾的地址。
                        //    return current;
                        // }
                    //#define current get_current()

                    //#endif /* !(_I386_CURRENT_H) */

    /*
     * 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.
     */
    if (address >= TASK_SIZE)//TASK_SIZE 的大小是3GB.也就是每个进程的用户空间大小。
        goto vmalloc_fault;//异常发生在内核空间

    mm = tsk->mm;//取进程的用户存储结构
    info.si_code = SEGV_MAPERR;//设置SIGSEGV信号故障码为非法区域

    /*
     * If we're in an interrupt or have no user
     * context, we must not take the fault..
     */
    if (in_interrupt() || !mm)//如果故障发生在硬件中断内或者用户存储结构为空---也就是说该映射的尚为建立。
        goto no_context;

    down(&mm->mmap_sem);//对信号量P原语操作

    vma = find_vma(mm, address);//寻找该地址的虚存区间和相应的vma_area_struct结构
    if (!vma)//越界
        goto bad_area;
    if (vma->vm_start <= address)
        goto good_area;
    if (!(vma->vm_flags & VM_GROWSDOWN))//落到空洞里面,如果此区域不是可以向下扩展的堆栈区域.
        goto bad_area;
    if (error_code & 4) {//空洞上方的区域是堆栈区,那么VM_GROWSDOWN为1
        /*
         * accessing the stack below %esp is always a bug.
         * The "+ 32" is there due to some instructions (like
         * pusha) doing post-decrement on the stack and that
         * doesn't show up until later..
         */
        if (address + 32 < regs->esp)//以%esp-32为检查的基准,如果超过,肯定是错的。pusha instruction can push 32bits per demand.
            goto bad_area;
    }
    if (expand_stack(vma, address))//属于正常的堆栈扩增要求,那就应该从空洞的顶部开始分配若干页面建立映射,并将之归入堆栈区间。,所以调用expand_stack()函数。
        goto bad_area;
/*
* Ok, we have a good vm_area for this memory access, so
* we can handle it..
*/
good_area://它主要是处理一些正常的访问
    info.si_code = SEGV_ACCERR;
    write = 0;
    switch (error_code & 3) {//根据中断响应机制传过来的error_code来进一步确定映射失败的原因,并采取相应的策略。
        default:    /* 3: write, present */
#ifdef TEST_VERIFY_AREA
            if (regs->cs == KERNEL_CS)
                printk("WP fault at %08lx/n", regs->eip);
#endif
            /* fall through */
        case 2:        /* write, not present */
            if (!(vma->vm_flags & VM_WRITE))
                goto bad_area;
            write++;
            break;
        case 1:        /* read, present */
            goto bad_area;
        case 0:        /* read, not present */
            if (!(vma->vm_flags & (VM_READ | VM_EXEC)))
                goto bad_area;
    }

    /*
     * If for any reason at all we couldn't handle the fault,
     * make sure we exit gracefully rather than endlessly redo
     * the fault.
     */
    switch (handle_mm_fault(mm, vma, address, write)) {//调用虚存管理函数handle_mm_fault(),在mm/memory.c中。
    case 1:
        tsk->min_flt++;
        break;
    case 2:
        tsk->maj_flt++;
        break;
    case 0:
        goto do_sigbus;
    default:
        goto out_of_memory;
    }

    /*
     * Did it hit the DOS screen memory VA from vm86 mode?
     */
    if (regs->eflags & VM_MASK) {
        unsigned long bit = (address - 0xA0000) >> PAGE_SHIFT;
        if (bit < 32)
            tsk->thread.screen_bitmap |= 1 << bit;
    }
    up(&mm->mmap_sem);
    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(&mm->mmap_sem);

bad_area_nosemaphore:
    /* User mode accesses just cause a SIGSEGV */
    if (error_code & 4) {//user mode
        tsk->thread.cr2 = address;
        tsk->thread.error_code = error_code;
        tsk->thread.trap_no = 14;
        info.si_signo = SIGSEGV;
        info.si_errno = 0;
        /* info.si_code has been set above */
        info.si_addr = (void *)address;
        force_sig_info(SIGSEGV, &info, tsk);
        return;
    }

    /*
     * Pentium F0 0F C7 C8 bug workaround.
     */
    if (boot_cpu_data.f00f_bug) {
        unsigned long nr;
        nr = (address - idt) >> 3;

        if (nr == 6) {
            do_invalid_op(regs, 0);
            return;
        }
    }

no_context:
    /* Are we prepared to handle this kernel fault?  */
    if ((fixup = search_exception_table(regs->eip)) != 0) {
        regs->eip = fixup;
        return;
    }

/*
* Oops. The kernel tried to access some bad page. We'll have to
* terminate things with extreme prejudice.
*/

    bust_spinlocks();

    if (address < PAGE_SIZE)
        printk(KERN_ALERT "Unable to handle kernel NULL pointer dereference");
    else
        printk(KERN_ALERT "Unable to handle kernel paging request");
    printk(" at virtual address %08lx/n",address);
    printk(" printing eip:/n");
    printk("%08lx/n", regs->eip);
    asm("movl %%cr3,%0":"=r" (page));
    page = ((unsigned long *) __va(page))[address >> 22];
    printk(KERN_ALERT "*pde = %08lx/n", page);
    if (page & 1) {
        page &= PAGE_MASK;
        address &= 0x003ff000;
        page = ((unsigned long *) __va(page))[address >> PAGE_SHIFT];
        printk(KERN_ALERT "*pte = %08lx/n", page);
    }
    die("Oops", regs, error_code);
    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:
    up(&mm->mmap_sem);
    printk("VM: killing process %s/n", tsk->comm);
    if (error_code & 4)
        do_exit(SIGKILL);
    goto no_context;

do_sigbus:
    up(&mm->mmap_sem);

    /*
     * Send a sigbus, regardless of whether we were in kernel
     * or user mode.
     */
    tsk->thread.cr2 = address;
    tsk->thread.error_code = error_code;
    tsk->thread.trap_no = 14;
    info.si_code = SIGBUS;
    info.si_errno = 0;
    info.si_code = BUS_ADRERR;
    info.si_addr = (void *)address;
    force_sig_info(SIGBUS, &info, tsk);

    /* Kernel mode? Handle exceptions or die */
    if (!(error_code & 4))
        goto no_context;
    return;

vmalloc_fault:
    {
        /*
         * Synchronize this task's top level page-table
         * with the 'reference' page table.
         */
        int offset = __pgd_offset(address);
        pgd_t *pgd, *pgd_k;
        pmd_t *pmd, *pmd_k;

        pgd = tsk->active_mm->pgd + offset;
        pgd_k = init_mm.pgd + offset;

        if (!pgd_present(*pgd)) {
            if (!pgd_present(*pgd_k))
                goto bad_area_nosemaphore;
            set_pgd(pgd, *pgd_k);
            return;
        }

        pmd = pmd_offset(pgd, address);
        pmd_k = pmd_offset(pgd_k, address);

        if (pmd_present(*pmd) || !pmd_present(*pmd_k))
            goto bad_area_nosemaphore;
        set_pmd(pmd, *pmd_k);
        return;
    }
}

空间的扩展:函数。在/:include/linux/mm.h中。just chagne the sttuct of vma_area_struct,does't build the new expand page for physical map.所以还要到do_page_fault中的good_area区域中去。
/* vma is the first one with  address < vma->vm_end,
* and even  address < vma->vm_start. Have to extend vma. */
static inline int expand_stack(struct vm_area_struct * vma, unsigned long address)
{
    unsigned long grow;

    address &= PAGE_MASK;//将地址按照页面边界对齐,4字节来对齐。
    grow = (vma->vm_start - address) >> PAGE_SHIFT;//计算要增长的页面大小。几个页面
    if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur ||
        ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur)//如果扩展以后的区域大小超过来可用于堆栈的资源,或者使动态分配的页面总量超过来可用于该进程的资源限制。那就不能扩展了。
        return -ENOMEM;//表示没有存储空间可以分配了。
    /*更改vma映射范围*/
    vma->vm_start = address;
    vma->vm_pgoff -= grow;
    vma->vm_mm->total_vm += grow;
    if (vma->vm_flags & VM_LOCKED)
        vma->vm_mm->locked_vm += grow;
    return 0;
}
虚存管理函数,上面已经说来其位置。这个函数主要来修改页面目录和页面表中的内容,是上面需求和下面供给的解决方案。
/*
* By the time we get here, we already hold the mm semaphore
*/
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
    unsigned long address, int write_access)
{
    int ret = -1;
    pgd_t *pgd;
    pmd_t *pmd;

    pgd = pgd_offset(mm, address);//pgd_offset是一个宏函数返回的是线性地址在pgd中对应的一个指针, 属于虚拟地址.mm这个变量,它记录了整个系统内存的情况。它的pgd就是系统4GB虚拟内存中一级页表的起始位置。
    pmd = pmd_alloc(pgd, address);//i386处理中,将pmd处理成pgd,该函数返回的是pgd的值。也就是说,下面的if条件肯定是成立的。

    if (pmd) {
        pte_t * pte = pte_alloc(pmd, address);
        if (pte)
            ret = handle_pte_fault(mm, vma, address, write_access, pte);
    }
    return ret;
}

pad_offset宏:在include/asm-i386/pgtable.h 中。
/* to find an entry in a page-table-directory. */
#define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))//PGDIR_SHIFT是22,右移后,为pgd的inidex,也就是页面目录表的下标。

#define __pgd_offset(address) pgd_index(address)

#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))//mm->pgd是页表的起始地址。通过该宏,找到线性地址在pgd中对应的一个指针---指向某个页面表的起始地址。
pte_alloc()函数,在include/asm-i386/pgalloc.h中。处理找到页面目录中的表项后的页面表的处理阶段。
extern inline pte_t * pte_alloc(pmd_t * pmd, unsigned long address)
{
    address = (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);//将给的线性地址转换成其所属页面表的下标---即高位的第二个10位,此时,PGD_SHIFT为12.

    if (pmd_none(*pmd))//没有该表项
        goto getnew;
    if (pmd_bad(*pmd))//例行检查,发现是错误的,也就是和页面信息不合适。感觉多余!
        goto fix;
    return (pte_t *)pmd_page(*pmd) + address;//返回的该地址所属页表中的表项,下一步就是物理地址了。
getnew://分配一个表项。一个页表所占的空间,恰好时一个物理页面。 当释放一个页表的时候,内核将释放的页表先保存在一个缓存池中,而不先将其物理内存释放掉,只有在缓冲池中已经满的情况下,释放。这就是get_pte_fast().如果缓冲池中空的,就要用get_pte_slow()来分配了。
{
    unsigned long page = (unsigned long) get_pte_fast();
    if (!page)
        return get_pte_slow(pmd, address);
    set_pmd(pmd, __pmd(_PAGE_TABLE + __pa(page)));//将一些属性标志位和起始地址一起写入页面目录项pgd中。这样,映射的“基础设施”已经弄好。但页面表项pte还是空的,剩下的就是物理内存本身了。那是由handle_pte_fault()完成的。
    return (pte_t *)page + address;
}
fix:
    __handle_bad_pmd(pmd);
    return NULL;
}

从缓冲池中取出没有被释放的页面:
extern __inline__ pte_t *get_pte_fast(void)
{
    unsigned long *ret;

    if((ret = (unsigned long *)pte_quicklist) != NULL) {
        pte_quicklist = (unsigned long *)(*ret);
        ret[0] = ret[1];
        pgtable_cache_size--;
    }
    return (pte_t *)ret;
}
在函数handle_mm_fault()中,执行handle_pte_fault()也就是处理页表项的函数。位于/mm/memory.c中:
static inline int handle_pte_fault(struct mm_struct *mm,struct vm_area_struct * vma, unsigned long address,int write_access, pte_t * pte)
{
    pte_t entry;

    /*
     * We need the page table lock to synchronize with kswapd
     * and the SMP-safe atomic PTE updates.
     */
    spin_lock(&mm->page_table_lock);
    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.
         */
        spin_unlock(&mm->page_table_lock);
        if (pte_none(entry))//测试表项是否为空,
            return do_no_page(mm, vma, address, write_access, pte);//进入空页处理
        return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access);//进行换页处理
    }
    if (write_access) {//如果是页面写保护故障
        if (!pte_write(entry))//如果页面不可写
            return do_wp_page(mm, vma, address, pte, entry);//进行写保护页处理

        entry = pte_mkdirty(entry);//设置脏页标志
    }
    entry = pte_mkyoung(entry);设置访问标志
    establish_pte(vma, address, pte, entry);//经过刷新的页目录放入页表
    spin_unlock(&mm->page_table_lock);
    return 1;
}

/*
* do_no_page() tries to create a new page mapping. It aggressively
* tries to share with existing pages, but makes a separate copy if
* the "write_access" parameter is true in order to avoid the next
* page fault.
*
* As this is called only for pages that do not currently exist, we
* do not need to flush old virtual caches or the TLB.
*
* This is called with the MM semaphore held.
*/
static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
    unsigned long address, int write_access, pte_t *page_table)
{
    struct page * new_page;
    pte_t entry;

    if (!vma->vm_ops || !vma->vm_ops->nopage)//vm_operations_struct结构,通常和文件,共享有关的处理时用到。堆栈不用。
        return do_anonymous_page(mm, vma, page_table, write_access, address);//进行匿名页处理

    /*
     * The third argument is "no_share", which tells the low-level code
     * to copy, not share the page even if sharing is possible.  It's
     * essentially an early COW detection.
     */
    new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, (vma->vm_flags & VM_SHARED)?0:write_access);
    /*进行调页处理,共享区域来说无条件共享缓冲页,一般区域区域作为写时拷贝处理*/
    if (new_page == NULL)    /* no page was available -- SIGBUS */
        return 0;//生成SIGBUS信号
    if (new_page == NOPAGE_OOM)
        return -1;//退出但前进程
    ++mm->rss;
    /*
     * This silly early PAGE_DIRTY setting removes a race
     * due to the bad i386 page protection. But it's valid
     * for other architectures too.
     *
     * Note that if write_access is true, we either now have
     * an exclusive copy of the page, or this is a shared mapping,
     * so we can make it writable and dirty to avoid having to
     * handle that later.
     */
    flush_page_to_ram(new_page);
    flush_icache_page(vma, new_page);
    entry = mk_pte(new_page, vma->vm_page_prot);//为新页生成目录项
    if (write_access) {//如果时写保护错
        entry = pte_mkwrite(pte_mkdirty(entry));//设置脏页和可写标志
    } else if (page_count(new_page) > 1 &&//如果新页的引用数大于1,并且所在虚存区域非共享域
           !(vma->vm_flags & VM_SHARED))
        entry = pte_wrprotect(entry);//设置写保护标志
    set_pte(page_table, entry);//将页帧目录项设置到页表
    /* no need to invalidate: a not-present page shouldn't be cached */
    update_mmu_cache(vma, address, entry);
    return 2;    /* Major fault */
}
static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table, int write_access, unsigned long addr)
{
    struct page *page = NULL;
    pte_t entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));//修正读标志位
    if (write_access) {//写保护故障
        page = alloc_page(GFP_HIGHUSER);//分配一高端用户页
        if (!page)
            return -1;
        clear_user_highpage(page, addr);
        entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot)));//修正写标志位
        mm->rss++;
        flush_page_to_ram(page);
    }
    set_pte(page_table, entry);
    /* No need to invalidate - it was non-present before */
    update_mmu_cache(vma, addr, entry);
    return 1;    /* Minor fault */
}

我们看do_anonymous_page()函数的代码时注意到,如果时读操作错误,那么由mk_pte()构筑的映射表要通过pte_wrprotect()加以修正,而如果是写操作的错误,则通过pte_mkwrite()加以修正。它们定义在include/asm-i386/pgtabe.h中:
static inline pte_t pte_wrprotect(pte_t pte)    { (pte).pte_low &= ~_PAGE_RW; return pte; }//将PAGE_RW设置为0,表示物理页面只允许读
static inline pte_t pte_mkwrite(pte_t pte)    { (pte).pte_low |= _PAGE_RW; return pte; }//恰好时和前一个函数不宜样,设置为1.表示页面只允许写。

                                                            5》物理页面的使用和周
系统初始化阶段:每一个页面对应一个page结构,形成一个page结构数组,并使一个全局变量mem_map指向这个数组,同时,又按照需要将物理地址连续的页面拼和成一个“块”,在根据需求建立“管理区”。

交换设备,通常时磁盘,的每个物理页面也要在内存中有个相应的数据结构,主要表示该页面是否已经被分配使用,以及有几个用户在共享这个页面。内核中定义了一个swap_info_struct数据结构。定义在inclue/linux/swap.h中:
struct swap_info_struct {
    unsigned int flags;
    kdev_t swap_device;
    spinlock_t sdev_lock;
    struct dentry * swap_file;
    struct vfsmount *swap_vfsmnt;
    unsigned short * swap_map;//指向一个数组,该数组中的每一个无符号短整数即代表磁盘上的物理页面,而数组的下标则决定来该页面在盘上或文件的位置。数组大小取决于pages,其中哦,swap_map[0]所代表的那个页面是不用于页面交换的,它包含了一些该设备或文件的一些信息以及一个表明那些页面可供使用的位图。
    unsigned int lowest_bit;//文件中从什么地方开始交换使用。
    unsigned int highest_bit;//文件中从什么地方停止交换使用。
    unsigned int cluster_next;//按集群的方式将页面放在磁盘扇区。
    unsigned int cluster_nr;
    int prio;            /* swap priority */
    int pages;
    unsigned long max;//该设备或文件中最大的页面号==设备或文件的物理大小。
    int next;            /* next entry on swap list */
};
还定义了一个swap_list结构,将各个可以分配的物理页面的磁盘设备或文件的swap_info_struct 按照优先高低链接在一起。
struct swap_list_t swap_list = {-1, -1};//开始时队列为空。
struct swap_list_t {
    int head;    /* head of priority-ordered swapfile list */
    int next;    /* swapfile to be used next */
};
typedef struct {
    unsigned long val;
} swp_entry_t;

释放一个页面的函数__swap_free(),在mm/swapfile.h中:
void __swap_free(swp_entry_t entry, unsigned short count)
{
    struct swap_info_struct * p;
    unsigned long offset, type;

    if (!entry.val)//页面0不用于页面交换
        goto out;

    type = SWP_TYPE(entry);//返回交换设备的序号,也就是swap_info_struct结构中的swap_info[]数组的下标。
    if (type >= nr_swapfiles)//它是曾被使用过的 swap_info 的最大索引值,而且从不会被降低.
        goto bad_nofile;
    p = & swap_info[type];
    if (!(p->flags & SWP_USED))//判断该页面是否已经被交换了。#define SWP_USED    1
        goto bad_device;
    offset = SWP_OFFSET(entry);//取得entry的高24位,为offset,表示页面在一个磁盘设备或文件中的位置,也就是文件中的逻辑页面号,而type则是指该页面在那一个文件中--是个序号
    if (offset >= p->max)//判断offset是否大于max,也就是整个offset页面号是否超出该文件或设备的最大边界。
        goto bad_offset;
    if (!p->swap_map[offset])//该数组是该页面的分配和使用计数,如果为0 ,表示尚未分配
        goto bad_free;
    swap_list_lock();
    if (p->prio > swap_info[swap_list.next].prio)
        swap_list.next = type;
    swap_device_lock(p);
    if (p->swap_map[offset] < SWAP_MAP_MAX) {//分配计数大于SWAP_MAP_MAX
        if (p->swap_map[offset] < count)
            goto bad_count;
        if (!(p->swap_map[offset] -= count)) {
            if (offset < p->lowest_bit)
                p->lowest_bit = offset;
            if (offset > p->highest_bit)
                p->highest_bit = offset;
            nr_swap_pages++;
        }
    }
    swap_device_unlock(p);
    swap_list_unlock();
out:
    return;

bad_nofile:
    printk("swap_free: Trying to free nonexistent swap-page/n");
    goto out;
bad_device:
    printk("swap_free: Trying to free swap from unused swap-device/n");
    goto out;
bad_offset:
    printk("swap_free: offset exceeds max/n");
    goto out;
bad_free:
    printk("VM: Bad swap entry %08lx/n", entry.val);
    goto out;
bad_count:
    swap_device_unlock(p);
    swap_list_unlock();
    printk(KERN_ERR "VM: Bad count %hd current count %hd/n", count, p->swap_map[offset]);
    goto out;
}

页面的周转有两方面的意思:其一是页面的分配,使用,和回收,并不射击页面的盘区交换。其二:盘区交换,而交换的目地最终也是页面的回收。
只有映射到的用户空间才会被换出。而内核,系统空间的页面则不会被换出。
按照页面的内容和性质,用户空间的页面有下面几种:
    普通的用户空间页面,包括进程的代码段,数据段,堆栈段,以及动态分配的存储堆。
    通过系统调用mmap()映射到用户空间的以打开的文件的内容
    进程间的共享内存区。

物理内存页面换入/换出的周转的要点如下:
    空闲,页面的page 数据结构通过其队列头结构list链入某个页面管理区zone 的空闲区队列free_area 页面的使用计数为0.
    分配,通过函数__alloc_pages()或__get_free_page()从某个空闲队列中分配内存页面,并将所分配页面使用计数count 1,其page 数据结构的队列头list 结构则变成空闲。
    活跃状态,页面的page数据结构通过其队列头结构lru 链入活跃页面队列active_list ,并且至少有一个进程的用户空间页面表项指向该页面,每当为页面建立或恢复映射时,都使页面引用
            计数count加1.
    不活跃状态,页面的page数据结构通过其队列头结构lru链入不活跃脏页面队列inactive_dirty_pages,但是原则上不在有任何进程的页面结构表项指向该页面,每当断开页面的映射时都使页面
            的使用计数count减1.
    将不活跃脏页面内容写入交换设备,并将page数据结构从不活跃脏中页面队列吗中转移到 某个不活跃干净 页面队列中。
    不活跃状态 干净,页面的page数据结构通过某队列头结构lru 链入某个不活跃 干净 页面队列,每个页面管理区都有一个不活跃干净页面队列inactive_clean_list。
    如果在转入不活跃状态以后的一段时间内额面受到访问,则又转入活跃状态并恢复映射。
    当有需要,就从干净页面队列中回收页面,或退回到空闲队列中,或直接另行分配。
struct address_space {
    struct list_head    clean_pages;    /* list of clean pages */
    struct list_head    dirty_pages;    /* list of dirty pages */
    struct list_head    locked_pages;    /* list of locked pages */
    unsigned long        nrpages;    /* number of total pages */
    struct address_space_operations *a_ops;    /* methods */
    struct inode        *host;        /* owner: inode, block_device */
    struct vm_area_struct    *i_mmap;    /* list of private mappings */
    struct vm_area_struct    *i_mmap_shared; /* list of shared mappings */
    spinlock_t        i_shared_lock;  /* and spinlock protecting it */
}这个数据结构,用来管理所有可交换的内存页面。每个可交换内存页面的page 数据结构都通过其队列头结构list链入其中的一个队列中。