2.1 Linux内存管理基本框架
32位cpu上的页式内存管理是采用两层映射方式,但在64位cpu上采用两层映射方式就不太合理了,所以在Linux中页式管理采用的是三层映射方式:页面目录(PGD)、中间目录(PMD)、页面表(PT),页面表中的各项称为PTE。页式管理采用三层映射的过程如下(几乎就和两层的一样):
前边说了为了64位的cpu着想,Linux将其页式映射模式设置为了三层,那么Linux遇到只要两层映射的32位cpu(i386)时怎么处理呢?如下:
/*
* traditional i386 two-level paging structure:
*/
#define PGDIR_SHIFT 22 //PGD 的起始位
#define PTRS_PER_PGD 1024 //每个PGD有多少个成员
/*
* the i386 is two-level, so we don't really have any
* PMD directory physically.
*/
#define PMD_SHIFT 22 // PMD 的起始位
#define PTRS_PER_PMD 1 //每个PMD有多少个成员
#define PTRS_PER_PTE 1024
这是《Linux内核源代码情景分析》中获得的代码,但是我在2.6.22版本中源代码中看到的PGDIR_SHIFT 和 PMD_SHIFT为21,想必内核有所变动了。先就依据书上来看,从上注释很容易发现中间目录 PMD 是无用的(因为起始位和PGD起始位重复),三层就变成了两层。
32位地址意味着有4G内存空间,Linux内核将4G内存空间分为1G系统空间(0xC0000000-0xFFFFFFFF)、3G用户空间(0x00000000-0xBFFFFFFF),其中描述的地址范围是指的虚地址。进程都有自己的3G用户空间,但是当进程通过系统调用进入内核就使用系统空间了,所以每个进程都拥有4G虚拟内存空间。系统空间从虚地址看是占了0xC0000000-0xFFFFFFFF的1G空间,但是在实际物理地址中是占了0x00000000-0x3FFFFFFF的1G空间。那么0xC0000000就成了Linux用来转化物理地址和虚拟地址的一个中间值了,转换方式如下代码:
#define __PAGE_OFFSET (0xC0000000)
==================== include/asm-i386/page.h 113 116 ====================
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) //将系统空间的虚拟地址转换为物理地址
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) //将系统空间的物理地址转换为虚拟地址
2.2地址映射全过程
这里以一段简单程序为实例将前边讲的页式管理和段式管理串起来理解:
#include <stdio.h>
greeting()
{
printf("Hello, world!\n");
}
main()
{
greeting();
}
objdump 是一个反汇编工具,很实用,但是我不会...。从书上给出的上述程序的反汇编中可以看出:在链接(ld)过程给函数greeting( )分配的函数地址是0x08048380,且在执行main函数时会call 8048380。那么要访问到 0x08048380 这个虚拟地址,整个过程是如何的呢:先段式后页式,如下。
先进行段式内存管理,这个过程只适用于i386这类cpu,因为其他cpu是没有段式内存管理映射过程的。段式映射过程(其实也只是为了跳过这个映射过程而已):依据相应性质的段寄存器为下标在全局寄存器中找到相应的描述表项。要访问 0x08048380 这个地址要是使用的是cs寄存器,温习一下段寄存器的格式:
然后我们再看看Linux内核给各寄存器赋值的:
==================== include/asm-i386/processor.h 408 417 ====================
#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)
上述是给各寄存器赋值,这些硬件操作应该是在写驱动时涉猎过的。上述代码中不难发现:SS堆栈段寄存器被设置为__USER_DS,从中可以说明intel想将进程分为:代码段、数据段、堆栈段,但是Linux却并没有那么做,Linux中只有代码段和数据段。这里的宏如下定义:
#define __KERNEL_CS 0x10
#define __KERNEL_DS 0x18
#define __USER_CS 0x23
#define __USER_DS 0x2B
各宏展开为2进制的结果如下:
从上图可知:TI 都是0,表示他们都使用GDT。事实上Linux只在模拟windows及windows上软件时才用到LDT。现在使用进程空间而非系统空间,所以想要访问 0x08048380虚拟地址,将在段式内存管理中将使用__USER_DS,将在GDT表中找到下标为4的全局段描述项。GDT表如下:
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)
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 */
我们将上述4个寄存器对应的这4个段描述项展开为2进制如图:
对照第一章讲的全局段描述项格式,会发现4个描述项的共同点:1. 段基地址全为0。 2. 段长度单位为4k 3.段长上限为0xffffffff,乘上段长单位刚好4G。 4. 4个段访问都是32位。 5. 描述项都在内存中。这样看来,每个段都是从0 ~ 4G空间完整映射的,相当于虚拟地址和线性地址两着一致。
当然4个段描述项也有不同点在bit40 ~ bit46,对应于type字段、S标志位、DPL标志位。也就是内核、用户权限不同和段的类型不同(代码段、数据段)。
上边说了那么多也就是说明:Linux只是在忽悠cpu,其实段式映射根本就没用。那么程序访问 0x08048380 这个虚拟地址经过段式映射,也还是还是访问 0x08048380 本身。
下面终于说到页式内存管理了:
每个进程都有自己的页面目录,这个目录保存在每个进程的mm_struct结构体中,每当调度一个进程进行运行,内核通过mm_struct结构体为即将运行的进程设置好 CR3寄存器,MMU总是从 CR3中取得页面目录指针。下面这段汇编就是将页面目录物理地址设置到CR3寄存器中。
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk,
unsigned cpu)
{
......
==================== include/asm-i386/mmu_context.h 44 44 ====================
asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
......
==================== include/asm-i386/mmu_context.h 59 59 ====================
}
下面来介绍怎么通过页式管理找到 0x08048380这个虚拟地址的物理地址的,我们知道现在 CR3 寄存器已经设置好了。现看看 0x08048380 这个地址的二进制码,毕竟是依据此来寻找到对应的页面目录、页面表、页面。(每个进程的CR3寄存器设置的值是不同的,所以同一虚拟地址在不同进程对应不同物理地址)
0x08048380(十六进制):0000 1000 00(目录中找到页表地址)00 0100 1000(页表中找到页面地址) 0011 0110 1000(页面中找到物理地址)
依据高10位(红字)为下标在CR3指向的页面目录(结构体数组)中找“结构体x”(因为有该结构体使得虚拟地址和物理地址不一一对应),这个结构体“结构体x”中含有某些信息,如:指向页面表的地址 等。例如:本例中高10位是 0000 1000 00,表示十进制为32,那么就以32位为下标在页面目录中去找,找到对应”结构体x“,在”结构体x“中得到页面表地址的信息,在其高20位后的的低位补上12个全0就得到指向某页面表的指针。这里就有两个疑问了,为什么只要”结构体x“的高20位就能得到指向某页面表的指针呢?”结构体x“剩下还有12位有什么用呢?答案是:因为每个页表组有1024个成员,每个成员4字节,一个页表组就是4k字节,刚好就是12位,所以在”结构体x“高20位后补上12位0就是对应页表组的指针。而剩下的12位则另作它用,比如最低位是P位,为1表示页表在内存中。
我们已经通过高十位找到了对应的页面表,现在我们应该继续在页表中找到对应的页面了,其过程与上述几乎一模一样,在页面表中以 00 0100 1000 的值为下表找到”结构体y“,然后在”结构体y“高20位后的的低位补上12个全0就是指向某个页面的指针(为什么只用高20位就不再累述),这里的页面代表内存的某块物理地址。
页面找到了,最后操作就是以页面为基地址加上最后12位偏移量找到具体物理地址了,以 0011 0110 1000 的值为偏移量在页面中找到对应物理地址。
上述整个过程经过3次映射,看似比较复杂,实际上访问过程是依赖高速缓存来实现的,虽然第一次访问目录组和页表组需要通过内存访问,但是一旦装入高速缓存以后,就可以在高速缓存中找到而不用去内存中读取,再加上整个过程式硬件完成的,所以是非常快的。
2.3几个重要的结构体和函数
上述内容讲的是内存映射的流程,但是具体细节并未细说,并且其也仅只是内存管理的一部分。而内存管理中有很多数据结构是很重要的,下面将详细介绍这些数据结构。
页面目录、中间目录、页表分别是由pgd_t、pmd_t、pte_t 结构体构成的结构体数组。其中结构体定义如下:
/*
* These are used to make use of C type-checking..
*/
#if CONFIG_X86_PAE //64位机
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 //32位机
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
前边说过pte_t(页表)中的最后12位用来存放访问权限和状态信息,具体的有关位段是如何定义的呢?内核另定义了一个页面保护结构体pgprot_t来表示页面后12位:
typedef struct { unsigned long pgprot; } pgprot_t;
上述低12位中有9位是标志位,在内核中定义如下:
#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 ,这一位在Intel手册上规定为保留不用,所以实际没啥用*/
页表项由高20位和低12位组合,找到某页表代码如下:
#define pgprot_val(x) ((x).pgprot)
#define __pte(x) ((pte_t) { (x) } )
#define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot)) //将页表序号左移12位然后或上pgprot,并将这个值赋值给pte_t结构体。
内核代码中设置页面的方式:
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
找到某页表代码如下,内核中mem_map是一个page数组的指针,可以通过该指针找到代表目标物理地址的数据结构:
#define pte_page(x) (mem_map + ((unsigned long)(( (x).pte_low >> PAGE_SHIFT ))))
内核中通过检测页表低12位值来判断页面的访问权限和状态信息,是通过和上述宏定义的标志位进行按位与来实现的。特别应注意第0位p标志位,为0表示虚拟映射以建立但是物理页面不在内存中(不常用到被置换了出去)。
每个物理页面都有与之对应的page数据结构,mem_map是一个page数组的指针,是系统在初始化是根据物理内存的大小建立的:
typedef struct page {
struct list_head list; //和free_area_struct有关联的双向链表
struct address_space *mapping; //下文2.6节会提到
unsigned long index; /* 当页面内容来自一个文件时,index代表页面在该文件的序号 */
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;
} mem_map_t;
系统在初始化时根据物理内存的大小建立一个page数组mem_map。
这个页面数组被分为两个管理区:ZONE_DMA、ZONE_NORMAL(还可能有第三个管理区ZONE_HIGHMEM)。管理区 ZONE_DMA 供DMA使用,因为:(1)必须留这个管理区的内存进行页面和盘区的交换。(2)在i386cpu中页式映射是cpu内部实现,没有单独的mmu,这样外设就要直接访问物理内存,而有的外设能访问的地址不能太高,所以设置这么个管理区。 (3)DMA需要的缓冲区超过一个物理页面时,就需要两个页面连续,依靠cpu内部的mmu不能办到,只能单独设个管理区。
每个管理区都有一个zone_struct数据结构,这个结构体中有两个队列 free_area_struct,一个用于保持离散的物理页面(单位为一),另一个用于保持连续的物理页面(单位为2的偶数倍)。
/* Free memory management - zoned buddy allocator.*/
#define MAX_ORDER 10
typedef struct free_area_struct
{
struct list_head free_list;
unsigned int *map;
} free_area_t;
struct pglist_data;
typedef struct zone_struct
{
/*
* Commonly accessed fields:
*/
spinlock_t lock;
unsigned long offset; /*管理区在mem_map[]中起始页面号*/
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];
/*
* 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;
#define ZONE_DMA 0
#define ZONE_NORMAL 1
#define ZONE_HIGHMEM 2
#define MAX_NR_ZONES 3
内存管理中还有一重点。就是当cpu想通过PCI总线访问其他cpu的物理内存时,或者访问PCI总线连接的公用储存模块(rom、ram...),所用时间是比直接访问本地内存慢的,这样的系统称之为“非均质存储结构”(NUMA)。所以在管理区上有更高一层的“节点”,页面数组也不是全局数组了,而是属于某个节点。节点数据结构:
typedef struct pglist_data
{
zone_t node_zones[MAX_NR_ZONES]; //代表本节点的最多管理区
zonelist_t node_zonelists[NR_GFPINDEX]; // 数组最大值有限制。#define NR_GFPINDEX 0x100,也就是最多100种分配策略
struct page *node_mem_map;50
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;
struct pglist_data *node_next; //若干节点形成的单链
} pg_data_t;
typedef struct zonelist_struct
{
zone_t * zones [MAX_NR_ZONES+1]; // NULL delimited,每一项代表具体的某个节点管理区,包含有其他节点的管理区,数组的第一个管理区不符合要求则去第二个..以此类推
int gfp_mask;
} zonelist_t; //表示一种分配策略
以上是物理空间管理的数据结构,下边是虚拟空间管理结构。因为进程使用虚存空间的各部分未必是连续的,所以一个各部分需要用“虚存区间”来管理(一个进程有多个虚存区间):
/*
* 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; //虚存地址高低次序连接
pgprot_t vm_page_prot; //访问权限
unsigned long vm_flags; //其他属性
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height; //AVL树
struct vm_area_struct * vm_avl_left; //AVL树
struct vm_area_struct * vm_avl_right; //AVL树
/* 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;
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) */
};
虚存区间的划分,并不仅取决于物理地址的连续性,还有区间的属性:访问权限等。
mmap(); 函数是将一块虚拟区间和已打开的文件或设备建立映射,就可以想直接访问内存中的字符数组一样来访问文件或设备内容。
两种情况虚存区间会和磁盘文件发生关系:(1)将久未使用的页面交换到磁盘上去。 (2)将磁盘文件映射到用户空间(mmap)。
关于虚存区间还有很重要的一个数据结构:
/*
* 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_area_struct中还有个vm_mm指针,其指向mm_struct:
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct vm_area_struct * mmap_avl; /* tree of VMAs */ //当虚存区间的过多时线性队列中搜索效率太低,就需要树
struct vm_area_struct * mmap_cache; /* last find_vma result,指向最后一次用过的虚存区间 */
pgd_t * pgd;53
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;
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;
};
mm_struct比vm_area_struct高一个层次,是整个用户空间的抽象,且一个进程只有一个mm_struct,但是可能多个进程共享一个mm_struct,如父子进程。VMA表示虚存区间。
虚存区间的页面和物理页面是不同的东西。
上述的结构体之间的关系图如下:
下面这个函数十分常用并会在本章后文中用到, find_vma();。函数功能:寻找第一个结束地址高于给定虚拟地址的vm_area_struct结构体,函数的注释将解释函数的执行过程:
/* 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. */55
/* (Cache hit rate is typically around 35%.) */
vma = mm->mmap_cache; //检测地址是否在最近一次访问的区间之中,找到的概率约35%
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) //寻找所属区间
{
if (!mm->mmap_avl) //在链表中查找所属区间
{
/* Go through the linear list. */
vma = mm->mmap;
while (vma && vma->vm_end <= addr)
vma = vma->vm_next;
}
else // //在AVL树中中查找所属区间
{
/* Then go through the AVL tree quickly. */
struct vm_area_struct * tree = mm->mmap_avl;
vma = NULL;
for (;;)
{
if (tree == vm_avl_empty)
break;
if (tree->vm_end > addr)
{
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,以便下一次可以快速查找
}
}
return vma;
}
如果上述 find_vma();函数返回值为NULL,表示所属的区间未建立,则需要建立新的虚存区间结构并通过insert_vm_struct();插入mm_struct的队列或AVL树中。
void insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)
{
lock_vma_mappings(vmp);
spin_lock(¤t->mm->page_table_lock);
__insert_vm_struct(mm, vmp); //实际上是此函数完成,上述不过是加了两把锁以保证整个过程不被中断
spin_unlock(¤t->mm->page_table_lock);
unlock_vma_mappings(vmp);
}
__insert_vm_struct(mm, vmp):
/* Insert vm structure into process list sorted by address
* and into the inode's i_mmap ring. If vm_file is non-NULL
* then the i_shared_lock must be held here.
*/
void __insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)
{
struct vm_area_struct **pprev;
struct file * file;
if (!mm->mmap_avl) {
pprev = &mm->mmap;
while (*pprev && (*pprev)->vm_start <= vmp->vm_start)
pprev = &(*pprev)->vm_next;
} else {
struct vm_area_struct *prev, *next;
avl_insert_neighbours(vmp, &mm->mmap_avl, &prev, &next);
pprev = (prev ? &prev->vm_next : &mm->mmap);
if (*pprev != next)
printk("insert_vm_struct: tree inconsistent with list\n");
}
vmp->vm_next = *pprev;
*pprev = vmp;
mm->map_count++;
if (mm->map_count >= AVL_MIN_MAP_COUNT && !mm->mmap_avl)
build_mmap_avl(mm);
......
}
2.4 越界访问
如果因为某些原因导致cpu无法访问到对应的物理地址,就表示虚拟地址映射失败了,就会产生页面异常,并调用异常处理函数。其中某些原因是指以下原因:
1.虚拟地址和物理地址未建立映射或已经取消
2.物理页面不在内存中
3.访问物理页面指令权限不匹配
假如已经发生了上述原因中的某一种,这里我们先略过中断的产生、传递、响应机制,假设cpu成功运行到了页面异常处理函数 do_page_fault():
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)); //导致失败的线性地址存放在cr2寄存器中,将cr2寄存器的值赋值给address
tsk = current; //current是一个宏操作,获得task_struct结构体,该结构体中有个指向mm_struct的指针
/*
* We fault-in kernel-space virtual memory on-demand. The
* 'reference' page table is init_mm.pgd.58
*
* 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)
goto vmalloc_fault;
mm = tsk->mm;
info.si_code = SEGV_MAPERR;
/*
* If we're in an interrupt or have no user
* context, we must not take the fault..
*/
if (in_interrupt() || !mm) //in_interrupt()结果返回非0表示映射失败是在某中断发生;mm为空即是映射还未建立
goto no_context;
down(&mm->mmap_sem); //满足互斥要求
vma = find_vma(mm, address); //上文中介绍过,找该映射失败地址所在区间
if (!vma) //未找到,表示不属于进程映射的虚拟区间(进程映射地址包括堆栈段、数据段、代码段、bss段,未找到说明该地址高于用户层最高地址区间--堆栈段,就肯定是系统空间),即越界访问。这个情况不细讲
goto bad_area;
if (vma->vm_start <= address) //找到了,也不细讲
goto good_area;
if (!(vma->vm_flags & VM_GROWSDOWN)) //落在两区间间隔的空洞中。两种空洞:1.堆栈下用来供给动态分配的未使用区间 2.之前映射的地址空间后来被撤销了。VM_GROWDOWN标志位为0表示上方是非堆栈。
goto bad_area;
我们主要关心落在空洞中的情况 goto bad_area;
[do_page_fault ]
/*
* 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) //当 error_code 为 0100 时进入if语句
{
tsk->thread.cr2 = address;
tsk->thread.error_code = error_code;
tsk->thread.trap_no = 14;
info.si_signo = SIGSEGV;60
info.si_errno = 0;
/* info.si_code has been set above */
info.si_addr = (void *)address;
force_sig_info(SIGSEGV, &info, tsk); //想进程发出软中断,SIGSEGV这个软中断时强制接受的,会产生“segment fault”
return;
}
error_code的代表的意思:
================= arch/i386/mm/fault.c 96 105 ====================
/*
* This routine handles page faults. It determines the address,
* and the problem, and then passes it off to one of the appropriate
* routines.
*
* 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
*/
2.5 用户堆栈扩展
当我们程序映射了的堆栈用光时是怎么处理的?答案是程序会自动扩展堆栈空间,是怎么扩展的?先看一下进程地址空间示意图:
当cpu将返回地址压入堆栈时,堆栈已经用光,就会产生一次页面错误异常:
[do_page_fault()]
151 if (!(vma->vm_flags & VM_GROWSDOWN))
152 goto bad_area;
153 if (error_code & 4) { // 此时的VM_GROWSDOWN 标志位为1表示上方为堆栈
154 /*
155 * accessing the stack below %esp is always a bug.
156 * The "+ 32" is there due to some instructions (like
157 * pusha) doing post-decrement on the stack and that
158 * doesn't show up until later..
159 */
160 if (address + 32 < regs->esp) //通常一次压入堆栈是4字节,有个特殊的pusha指令一次可压入32字节,如果更多就表示不是紧挨着堆栈区,就goto bad_area;
161 goto bad_area;
162 }
163 if (expand_stack(vma, address)) //扩展堆栈
164 goto bad_area;
下面来看expand_stack( ):
[do_page_fault()>expand_stack()]
487 /* vma is the first one with address < vma->vm_end,
488 * and even address < vma->vm_start. Have to extend vma. */
489 static inline int expand_stack(struct vm_area_struct * vma, unsigned long address)
490 {
491 unsigned long grow;
492
493 address &= PAGE_MASK;
494 grow = (vma->vm_start - address) >> PAGE_SHIFT; //根据上边进程地址示意图可知vm_start是堆栈的底部,vm_start - address即要堆栈要扩增多少
495 if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur || //vm_end是堆栈的顶部,vm_end - address即要堆栈扩展后整个堆栈的大小。RLIMIT_STACK是对用户堆栈的限制。
496 ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur)
497 return -ENOMEM; //超过堆栈最大限制
498 vma->vm_start = address;
499 vma->vm_pgoff -= grow;
500 vma->vm_mm->total_vm += grow;
501 if (vma->vm_flags & VM_LOCKED)
502 vma->vm_mm->locked_vm += grow;
503 return 0;
504 }
expand_stack()只是改变了堆栈区的vm_area_struct结构体,继续阅读do_page_fault代码可知:扩展页面和物理内存的映射是在good_area中完成的
[do_page_fault()]
165 /*
166 * Ok, we have a good vm_area for this memory access, so
167 * we can handle it..63
168 */
169 good_area:
170 info.si_code = SEGV_ACCERR;
171 write = 0;
172 switch (error_code & 3) {
173 default: /* 3: write, present */
174 #ifdef TEST_VERIFY_AREA
175 if (regs->cs == KERNEL_CS)
176 printk("WP fault at %08lx\n", regs->eip);
177 #endif
178 /* fall through */
179 case 2: /* write, not present */ //堆栈是写操作所以将进入这个语句
180 if (!(vma->vm_flags & VM_WRITE)) //堆栈是允许写入的所以直接break
181 goto bad_area;
182 write++;
183 break;
184 case 1: /* read, present */
185 goto bad_area;
186 case 0: /* read, not present */
187 if (!(vma->vm_flags & (VM_READ | VM_EXEC)))
188 goto bad_area;
189 }
190
191 /*
192 * If for any reason at all we couldn't handle the fault,
193 * make sure we exit gracefully rather than endlessly redo
194 * the fault.
195 */
196 switch (handle_mm_fault(mm, vma, address, write)) { //见下文
197 case 1:
198 tsk->min_flt++;
199 break;
200 case 2:
201 tsk->maj_flt++;
202 break;
203 case 0:
204 goto do_sigbus;
205 default:
206 goto out_of_memory;
207 }
handle_mm_fault():
[do_page_fault()>handle_mm_fault()]
1189 /*
1190 * By the time we get here, we already hold the mm semaphore
1191 */
1192 int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
1193 unsigned long address, int write_access)
1194 {
1195 int ret = -1;
1196 pgd_t *pgd;
1197 pmd_t *pmd;
1198
1199 pgd = pgd_offset(mm, address); //计算出该地址属于的页面目录中的哪一项的地址,页面目录是一直就有的也就不需要我们后边再分配。
/* 312 #define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
* ......
*/ 316 #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address)) //转换成页面目录中的某下标
1200 pmd = pmd_alloc(pgd, address); //本是确定一个中间目录,但是i386cpu架构只有两层映射,所以这个过程就百分百成功。
1201
1202 if (pmd) { //进入语句
1203 pte_t * pte = pte_alloc(pmd, address); //页面目录项可能是未指向任何页面表的,空的就需要分配,见下文
1204 if (pte)
1205 ret = handle_pte_fault(mm, vma, address, write_access, pte);
1206 }
1207 return ret;
1208 }
pte_alloc():
[do_page_fault()>handle_mm_fault()>pte_alloc()]
120 extern inline pte_t * pte_alloc(pmd_t * pmd, unsigned long address)
121 {
122 address = (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1); //转换成页面表中某下标
123
124 if (pmd_none(*pmd)) //假设页面目录项未指向任何页面表,需分配页面表
125 goto getnew; //提示:一个页面表刚好占一个物理页面大小
126 if (pmd_bad(*pmd))
127 goto fix;
128 return (pte_t *)pmd_page(*pmd) + address;
129 getnew:
130 {
131 unsigned long page = (unsigned long) get_pte_fast(); //内核会将释放的页面保存在缓冲池,到缓冲池满后才真正开始将物理页面释放,所以分配页面表应先看缓冲池中有没有未释放的物理页面
132
133 if (!page)
134 return get_pte_slow(pmd, address); //缓冲池为空时,就只有如此分配了。slow表示:可能物理内存页面已经用光,需要进行磁盘交换,所以比较慢。
135 set_pmd(pmd, __pmd(_PAGE_TABLE + __pa(page))); //将起始地址和一些属性标志位写入页面表。
136 return (pte_t *)page + address;
137 }
138 fix:
139 __handle_bad_pmd(pmd);
140 return NULL;
141 }
页面表中表项还是空的?不担心,交由handle_pte_fault()完成:
[do_page_fault()>handle_mm_fault()>handle_pte_fault()]
1135 /*
1136 * These routines also need to handle stuff like marking pages dirty
1137 * and/or accessed for architectures that don't do it in hardware (most
1138 * RISC architectures). The early dirtying is also good on the i386.
1139 *
1140 * There is also a hook called "update_mmu_cache()" that architectures
1141 * with external mmu caches can use to update those (ie the Sparc or
1142 * PowerPC hashed page tables that act as extended TLBs).
1143 *
1144 * Note the "page_table_lock". It is to protect against kswapd removing
1145 * pages from under us. Note that kswapd only ever _removes_ pages, never
1146 * adds them. As such, once we have noticed that the page is not present,66
1147 * we can drop the lock early.
1148 *
1149 * The adding of pages is protected by the MM semaphore (which we hold),
1150 * so we don't need to worry about a page being suddenly been added into
1151 * our VM.
1152 */
1153 static inline int handle_pte_fault(struct mm_struct *mm,
1154 struct vm_area_struct * vma, unsigned long address,
1155 int write_access, pte_t * pte)
1156 {
1157 pte_t entry;
1158
1159 /*
1160 * We need the page table lock to synchronize with kswapd
1161 * and the SMP-safe atomic PTE updates.
1162 */
1163 spin_lock(&mm->page_table_lock);
1164 entry = *pte;
1165 if (!pte_present(entry)) { //测试表项所映射的物理页面是否在内存中--不在,进入语句。提示:如果结果是页面在内存中,那错误就一定是访问权限问题了。
1166 /*
1167 * If it truly wasn't present, we know that kswapd
1168 * and the PTE updates will not touch it later. So
1169 * drop the lock.
1170 */
1171 spin_unlock(&mm->page_table_lock);
1172 if (pte_none(entry)) //测试页面表项是否为空--为空,进入语句
1173 return do_no_page(mm, vma, address, write_access, pte); //见下文
1174 return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access);
1175 }
1176
1177 if (write_access) {
1178 if (!pte_write(entry))
1179 return do_wp_page(mm, vma, address, pte, entry);
1180
1181 entry = pte_mkdirty(entry);
1182 }
1183 entry = pte_mkyoung(entry);
1184 establish_pte(vma, address, pte, entry);
1185 spin_unlock(&mm->page_table_lock);
1186 return 1;
1187 }
do_no_page():
[do_page_fault()>handle_mm_fault()>handle_pte_fault()>do_no_page()]
1080 /*
1081 * do_no_page() tries to create a new page mapping. It aggressively
1082 * tries to share with existing pages, but makes a separate copy if
1083 * the "write_access" parameter is true in order to avoid the next
1084 * page fault.
1085 *
1086 * As this is called only for pages that do not currently exist, we
1087 * do not need to flush old virtual caches or the TLB.
1088 *
1089 * This is called with the MM semaphore held.
1090 */
1091 static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
1092 unsigned long address, int write_access, pte_t *page_table)
1093 {
1094 struct page * new_page;
1095 pte_t entry;
1096
1097 if (!vma->vm_ops || !vma->vm_ops->nopage) //这两个条件是和页面共享和文件系统有关的,这里无关所以进入语句
1098 return do_anonymous_page(mm, vma, page_table, write_access, address); //见下文
......
1133 }
do_anonymous_page():
[do_page_fault()>handle_mm_fault()>handle_pte_fault()>do_no_page()>do_anonymous_page()]
1058 /*
1059 * This only needs the MM semaphore
1060 */
1061 static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table,int write_access, unsigned long addr)
1062 {
1063 struct page *page = NULL;
1064 pte_t entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot)); //如果页面异常是读操作引起,就通过pte_wrprotect()加以修正
1065 if (write_access) { //可写则进入语句
1066 page = alloc_page(GFP_HIGHUSER); //分配物理页面
1067 if (!page)
1068 return -1;
1069 clear_user_highpage(page, addr);
1070 entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot))); //如果页面异常是写操作引起,就通过pte_mkwrite()加以修正
1071 mm->rss++;
1072 flush_page_to_ram(page);
1073 }
1074 set_pte(page_table, entry); //将alloc_page()分配的物理页面连同状态及标志位设置进page_table指向的页面表项
1075 /* No need to invalidate - it was non-present before */
1076 update_mmu_cache(vma, addr, entry);
1077 return 1; /* Minor fault */
1078 }
到这虚拟页面到物理页面的映射就建立完成,就这样用户堆栈在用户透明的情况下就已经扩展了。这里上述程序的两个函数:pte_wrprotect()、pte_mkwrite():
277 static inline pte_t pte_wrprotect(pte_t pte) { (pte).pte_low &= ~_PAGE_RW; return pte; } //将标志位置0,表示只读
271 static inline int pte_write(pte_t pte) { return (pte).pte_low & _PAGE_RW; } //表示可读写
中断和自陷(trap)指令发生,cpu会将下一跳指令地址压入堆栈;异常发生时,cpu会将当前指令地址压入堆栈。所以更精确的来讲“缺页中断”改为“缺页异常”比较合适。
2.6 物理页面的使用和周转
内存中和磁盘中的物理页面分别称之为:内存页面 和 盘上页面。
回顾:物理上存在的页面都有一个page结构体,mem_map全局指针指向page结构数组。将页面拼成物理地址连续的页面块,将多个页面块拼接成若干管理区。每个管理区都有一个空闲队列,空闲队列用于存放空闲的内存页面page。
在内存中每个页面都在内核中有相应的数据结构,同样设备、文件中的物理页面在内核中也有对应的数据结构,但数据结只是简单的计数而已,表示页面是否被分配使用,以及有几用户共享这个页面。
内核中用来描述一个设备、文件的数据结构是:swap_info_struct():
49 struct swap_info_struct {
50 unsigned int flags;
51 kdev_t swap_device;
52 spinlock_t sdev_lock;
53 struct dentry * swap_file;
54 struct vfsmount *swap_vfsmnt;
55 unsigned short * swap_map; //指向数组,数组中每个无符号整型表示设备、文件中的物理页面共享计数,数组下标表示该页面在设备、文件的位置,数组大小为设备、文件大小。通常第一个页面swap_map[0]是不用于
换的,因为它含有设备、文件的信息以及表明哪些页面可供使用的位图。
56 unsigned int lowest_bit; //设备、文件可供页面交换的起始点
57 unsigned int highest_bit; //设备、文件可供页面交换的结束点
58 unsigned int cluster_next; //分配盘上页面是按照集群方式(蔟 cluster)进行,为此而设置的
59 unsigned int cluster_nr; //同上
60 int prio; /* swap priority */
61 int pages;
62 unsigned long max; //设备、文件的最大页面号
63 int next; /* next entry on swap list */
64 };
linux允许多个页面交换设备,所以构成了数组:
25 struct swap_info_struct swap_info[MAX_SWAPFILES];
还为交换设备建立链表,可以看到是没有指针的链表(也很不解),系统调用swap_on()指定将文件用于页面交换时,就将该文件swap_info_struct链入该链表
23 struct swap_list_t swap_list = {-1, -1};
153 struct swap_list_t {
154 int head; /* head of priority-ordered swapfile list */
155 int next; /* swapfile to be used next */
156 };
内存中通过pte_t建立物理与虚拟之间的关系,同样在盘上物理页面也有这样类似的结构体:swp_entry_t。当页面不在内存中,页面表项便不再是pte_t而变为swp_entry_t指向页面去向,此时最低位是0,mmu将忽视它,Linux用swp_entry_t唯一确定页面在哪个盘上及其位置。
8 /*
9 * A swap entry has to fit into a "unsigned long", as
10 * the entry is hidden in the "index" field of the
11 * swapper address space.
12 *
13 * We have to move it here, since not every user of fs.h is including
14 * mm.h, but m.h is including fs.h via sched .h :-/
15 */
16 typedef struct {
17 unsigned long val; //前24位称为offset,之后7位为type,最后一位始终为0
18 } swp_entry_t;
还为offset和type的访问及其与pte_t结构体之间的关系定义了如下宏:
336 /* Encode and de-code a swap entry */
337 #define SWP_TYPE(x) (((x).val >> 1) & 0x3f) //offset是指页面在那个文件,是个序号
338 #define SWP_OFFSET(x) ((x).val >> 8) //type是磁盘或文件自身的位置,以此为下标可以在 swap_info_struct 结构的 swap_map[] 数组中找到相应的交换设备
339 #define SWP_ENTRY(type, offset) ((swp_entry_t) { ((type) << 1) | ((offset) << 8) })
340 #define pte_to_swp_entry(pte) ((swp_entry_t) { (pte).pte_low })
341 #define swp_entry_to_pte(x) ((pte_t) { (x).val })
介绍一个释放磁盘的函数:
141 /*
142 * Caller has made sure that the swapdevice corresponding to entry
143 * is still around or has not been recycled.
144 */
145 void __swap_free(swp_entry_t entry, unsigned short count)
146 {
147 struct swap_info_struct * p;
148 unsigned long offset, type;
149
150 if (!entry.val) //任何设备或文件中页面0是不做交换的。
151 goto out;
152
153 type = SWP_TYPE(entry); //返回设备或文件的序号,也是swap_info[ ]数组下标
154 if (type >= nr_swapfiles)
155 goto bad_nofile;
156 p = & swap_info[type];
157 if (!(p->flags & SWP_USED))
158 goto bad_device;2016-01-01
继续看这个函数:
159 offset = SWP_OFFSET(entry);
160 if (offset >= p->max)
161 goto bad_offset;
162 if (!p->swap_map[offset]) //offset是页面在设备或文件中位置,swap_map[offset] = 0表示页面未分配
163 goto bad_free;
164 swap_list_lock();
165 if (p->prio > swap_info[swap_list.next].prio)
166 swap_list.next = type;
167 swap_device_lock(p);
168 if (p->swap_map[offset] < SWAP_MAP_MAX) { //分配计数应该小于SWAP_MAP_MAX,如果小于进入循环
169 if (p->swap_map[offset] < count) //count表示几个使用者释放了页面
170 goto bad_count;
171 if (!(p->swap_map[offset] -= count)) { //没有使用者使用页面,进入循环
172 if (offset < p->lowest_bit) //将页面位置调整到可交换范围内
173 p->lowest_bit = offset;
174 if (offset > p->highest_bit) //将页面位置调整到可交换范围内
175 p->highest_bit = offset;
176 nr_swap_pages++; //可交换页面加1
177 }
178 }
179 swap_device_unlock(p);
180 swap_list_unlock();
181 out:
182 return;
物理内存页面换入换出要点:
(1) 空闲。页面page通过对列头list链入某页面管理区空闲队列free_area。页面使用计数count = 0。
(2) 分配。通过 __alloc_pages或__get_free_page 从某个空闲队列分配页面,将count置1,并从空闲对列取出,page结构队list结构指向空闲。
(3) 活跃状态。page结构通过队列头结构lru链(least recent used)入活跃链表active_list,并至少有一个用户空间页面指向该物理页面,每当恢复和建立映射count都++。
(4) 不活跃脏状态。page结构通过队列头结构lru链入不活跃脏状态链表inactive_dirty_list。原则上无用户空间页面指向该物理页面,每有进程断开映射count--。
(5) 将“脏”页面移入交换设备,并将其从不活跃脏状态链表移入不活跃干净状态链表。
(6) 不活跃干净状态。page结构通过队列头结构lru链入不活跃干净状态链表inactive_clean_list。
(7) 如果转入不活跃状态后一段时间内受到访问,则又转入活跃状态并恢复映射。
(8) 当有页面需要:条件满足时就从干净队列回收页面。 或 另行分配。
上述队列中active_list、inactive_dirty_list是内核全局变量,inactive_clean_list是每个管理区内的局部变量。
为加快在暂存队列中搜索,又设置了杂凑表:page_hash_cache。
内核中全局的address_space数据结构swapper_space,用来管理内核中可交换的页面,每个page结构都有相应的 struct address_space *mapping 成员,在swapper_space结构中的页面都是在交换设备中有映射的。address_space数据结构、swapper_space定义如下:
365 struct address_space {
366 struct list_head clean_pages; /* list of clean pages */
367 struct list_head dirty_pages; /* list of dirty pages */
368 struct list_head locked_pages; /* list of locked pages 暂时锁住的页面不让换出*/
369 unsigned long nrpages; /* number of total pages */
370 struct address_space_operations *a_ops; /* methods */
371 struct inode *host; /* owner: inode, block_device */
372 struct vm_area_struct *i_mmap; /* list of private mappings */
373 struct vm_area_struct *i_mmap_shared; /* list of shared mappings */
374 spinlock_t i_shared_lock; /* and spinlock protecting it */
375 };
31 struct address_space swapper_space = {
32 LIST_HEAD_INIT(swapper_space.clean_pages),
33 LIST_HEAD_INIT(swapper_space.dirty_pages),
34 LIST_HEAD_INIT(swapper_space.locked_pages),
35 0, /* nrpages */
36 &swap_aops, 结构体指针,该机构体中包含很多swap相关的函数指针
37 };
我们来看看,内核将需要换入的页面写入刚分配好的空闲页面的后,刚写入的"clean"页面链入相应的队列的具体过程:
54 void add_to_swap_cache(struct page *page, swp_entry_t entry)
55 {
56 unsigned long flags;
57
58 #ifdef SWAP_CACHE_INFO
59 swap_cache_add_total++;
60 #endif
61 if (!PageLocked(page)) //将页面锁住防干扰
62 BUG();
63 if (PageTestandSetSwapCache(page))
64 BUG();
65 if (page->mapping) //用于管理空闲页面的,分配的页面已经从该队列中取出,所以mapping指向NULL
66 BUG();
67 flags = page->flags & ~((1 << PG_error) | (1 << PG_arch_1));
68 page->flags = flags | (1 << PG_uptodate); //因为页面是“clean”的,所以PG_uptodate 将置 1。
69 add_to_page_cache_locked(page, &swapper_space, entry.val); //这里才是链入队列函数
70 }
将刚写入的"clean"页面链入相应的队列的函数: add_to_page_cache_locked( );
476 /*
477 * Add a page to the inode page cache.
478 *
479 * The caller must have locked the page and
480 * set all the page flags correctly..
481 */
482 void add_to_page_cache_locked(struct page * page, struct address_space *mapping, unsigned long index)
483 {
484 if (!PageLocked(page))
485 BUG();
486
487 page_cache_get(page); //实质上是将page -> count 加1
488 spin_lock(&pagecache_lock);
489 page->index = index;
490 add_page_to_inode_queue(mapping, page); //这3个函数将在后文中介绍
491 add_page_to_hash_queue(page, page_hash(mapping, index));
492 lru_cache_add(page);
493 spin_unlock(&pagecache_lock);
494 }
page_cache_get( ):
150 #define get_page(p) atomic_inc(&(p)->count)
31 #define page_cache_get(x) get_page(x)
add_to_page_cache_locked( );中的3个函数:add_page_to_inode_queue(mapping, page);
72 static inline void add_page_to_inode_queue(struct address_space *mapping, struct page * page)
73 {
74 struct list_head *head = &mapping->clean_pages; //链入swapprer_space中的clean队列,通常一个文件由一个address_space数据结构来管理
75
76 mapping->nrpages++;
77 list_add(&page->list, head);
78 page->mapping = mapping;
79 }
add_page_to_hash_queue(page, page_hash(mapping, index));
58 static void add_page_to_hash_queue(struct page * page, struct page **p) //将page添入队列,这个队列有个队列尾**p,每个加入的page都在队列尾之前
59 {
60 struct page *next = *p;
61
62 *p = page;
63 page->next_hash = next;
64 page->pprev_hash = p;
65 if (next)
66 next->pprev_hash = &page->next_hash;
67 if (page->buffers)
68 PAGE_BUG(page);
69 atomic_inc(&page_cache_size);
70 }
链入具体哪个杂凑队列取决于杂凑值:
68 #define page_hash(mapping,index) (page_hash_table+_page_hashfn(mapping,index))
lru_cache_add(page);
226 /*
227 * lru_cache_add: add a page to the page lists
228 * @page: the page to add
229 */
230 void lru_cache_add(struct page * page)
231 {
232 spin_lock(&pagemap_lru_lock);
233 if (!PageLocked(page))
234 BUG();
235 DEBUG_ADD_PAGE
236 add_page_to_active_list(page);
237 /* This should be relatively rare */
238 if (!page->age)
239 deactivate_page_nolock(page);
240 spin_unlock(&pagemap_lru_lock);
241 }
209 #define add_page_to_active_list(page) { \
210 DEBUG_ADD_PAGE \
211 ZERO_PAGE_BUG \
212 SetPageActive(page); \
213 list_add(&(page)->lru, &active_list); \
214 nr_active_pages++; \
215 }
mmap( )将文件映射到用户空间。
特权用户进程设置的:swapon( );、swapoff( );用于开始或终止某个特定的盘区或文件用于页面交换。
嵌入式系统中对flash memory的擦除比较麻烦(一擦就是一整块块),所以就将磁盘交换关闭。
共享内存的系统调用将在进程间通信章节介绍。