1:信息
linux - 0.11
memory.c
page.s
2: 内存分页管理
基本名词
页目录:指的是一个4K大小的内存,有1024个页目录项
页目录项:每个页目录项指向一个页表(4K)
页表:指的是一个4K大小的内存,里面有1024个页表项
页表项:每个页表项,指向一页(4K)
2.1 基础信息
//显示内存已用完出错信息,并退出
static inline volatile void oom(void)
{
printk("out of memory\n\r");
do_exit(SIGSEGV); //退出码SIGSEGV(11)相同值的出错码含义是:资源暂时不可用
}
//刷新页变换缓冲(一般位于cache中的TLB)宏函数
//为了提高地址的转换效率,cpu将最近使用的页表数据存放在芯片中高速缓冲中。在修改过页表信息后,就需要刷新该缓冲区
//这里使用重新加载页目录基址寄存器cr3的方法来进行刷新
#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))//eax是页目录的地址
/* these are not to be changed without changing head.s etc */
//下面定义若要变动,则需要修改与head.s等文件中的相关信息
#define LOW_MEM 0x100000 //内存低端(1MB)
#define PAGING_MEMORY (15*1024*1024)//分页内存15MB 。主内存最多15MB
#define PAGING_PAGES (PAGING_MEMORY>>12)//分页后物理内存页面数(3840)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)//指定地址的对应页号
#define USED 100//页面被占用标志
//该宏用于判断给的的地址是否位于当前进程的代码段中,其中(addr)+4095)&~4095)用于取得线性地址addr所在内存页面的末端地址
#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \
current->start_code + current->end_code)
static long HIGH_MEMORY = 0;//全局变量,存放实际物理内存呢最高端地址
//从form处复制1页内存到to处(4K字节)
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si")
//物理内存映射字节图(1字节代表一页内存)。每个页对应的字节用于标志页面当前被引用(占用)次数。它最大可以映射15MB的空间
//在初始化函数mem_init()中,对于不能用作内存区页面的位置均先预被设置成USD(100)
static unsigned char mem_map [ PAGING_PAGES ] = {0,};
2.2 get_free_page和free_page
/*
* Get physical address of first (actually last :-) free page, and mark it
* used. If no free pages left, return 0.
*/
//在主内存中取空闲物理页面。如果已经没有可用的物理内存页面,则返回0
//输入: %1(ax = 0) - 0;%2(LOW—MEM)内存字节位图管理的起始位置
//%3(cx = PAGING_PAGES)%4(edi = mem_map + PAGING_PAGES - 1)
//输出:返回%0 (ax = 物理页面的起始地址)
//上面%4寄存器实际指向mem_map[]内存字节位图中的最后一个字节。
//本函数从位图末端开始向前扫描所有的页面标志(页面总数为PAGING_PAGES),若有页面空闲(内存位图字节为0)则返回页面地址。
//注意!本函数只是指出在主内存区的一页空闲物理页面,但并没有映射到某个进程的地址空间中去。
//后面的put_page()函数即用于把指定页面映射到某个进程的地址空间中去。
//当然对于内核使用本函数并需要再使用put_page()进行映射,因为内核代码和数据空间(16MB)已经对等的映射到物理地址空间
//下面函数定义了一个局部寄存器变量。该变量将被保存再eax寄存器中,已便高效访问和操作。这种定义变量的方法主要用于内嵌汇编程序中。
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t"//置方向位,al(0)与对于每个页面的(di)内容比较
"jne 1f\n\t"//如果没有等于0的字节,则跳转结束(返回0)
"movb $1,1(%%edi)\n\t"//1=>[1 + edi],将对应页面映像bit位置1
"sall $12,%%ecx\n\t"//页面数*4K = 相对页面的起始地址
"addl %2,%%ecx\n\t"//再加上地端内存地址,得页面实际物理起始地址
"movl %%ecx,%%edx\n\t"//将页面实际起始地址->edx寄存器
"movl $1024,%%ecx\n\t"//寄存器ecx置计数值1024
"leal 4092(%%edx),%%edi\n\t"//将4092+edx的位置 ->edi(该页面末端)
"rep ; stosl\n\t"//将edi所指向内存清0(反方向,即将该页面清0)
"movl %%edx,%%eax\n"//将页面起始地址->eax(返回值)
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
:"di","cx","dx");
return __res;//返回空闲物理页面地址(若无空闲页面则返回0)
}
/*
* Free a page of memory at physical address 'addr'. Used by
* 'free_page_tables()'
*/
//释放物理addr开始的1页面的内存
void free_page(unsigned long addr)
{
//首先参数给定的物理地址addr的合理性。如果物理地址下雨内存的低端(1MB),则表示在内核程序或高速缓冲区中,对此不予理会
//若大于等于系统所含物理内存最高端,则显示出错信息并且停止工作
if (addr < LOW_MEM) return;
if (addr >= HIGH_MEMORY)
panic("trying to free nonexistent page");
//如果对参数add验证通过,则根据物理地址换算出从内存低端开始记起的内存页面号。页面号 = (addr - LOW_MEM)/ 4096。可见页面号从0号开始计起
//此时addr存放页面号,如果该页面号对应的页面映射字节不等0,则减一返回。此时该映射字节值应该为0,表示已经释放。
//如果对于页面字节本来就是0,表示该物理页面本来就是空闲的,说明内核代码出问题。于是显示出错信息并停机。
addr -= LOW_MEM;
addr >>= 12;
if (mem_map[addr]--) return;
mem_map[addr]=0;
panic("trying to free free page");
}
2.3 free_page_tables
根据指定的线程地址和限长(页表的个数),释放对于页表指定的内存块并置表项空闲。页目录位于物理地址0开始处,共1024项,每项4字节,共占4K字节。每个目录项指定一个页表。
内核页表从物理地址0x1000处开始,(紧挨着目录空间),共4个页表。每个页表有1024项,每项4字节。因此也占4K(1页)内存。
各进程(除了内核进程0和1)的页表所占的页面在进程创建时,由内核为其在主内存区中申请得到。每个页表项对应1页物理内存,因此一个页表最多可以映射4MB的物理内存。
参数:
- from - 起始线性基地址
- size - 释放的字节长度
/*
* This function frees a continuos block of page tables, as needed
* by 'exit()'. As does copy_page_tables(), this handles only 4Mb blocks.
* 释放页表连续的内存块, exit()需要这个函数。和copy_free_tables() 类似。该函数仅能处理4MB长度的内存块
*/
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
//首先检测参数from给出的线性机制是否在4MB的边界处,因为该函数只能出路这种情况
if (from & 0x3fffff)
panic("free_page_tables called with wrong alignment");
if (!from)//from为0、则出错。说明试图释放内核和缓冲区所占的空间
panic("Trying to free up swapper memory space");
//计算size给定的长度所占的页目录项数(4MB的进数整数倍),也即占的页表数
//因为1个页表可以管理4MB物理内存,所以这里用右移动22位的方式把需要复制的内存长度值除以4MB
//其中将加上0x3fffff(即4Mb-1)用于得到进位整数倍效果。即除操作若有余数则进1
//例如,如果siez = 4.01Mb 则得到size = 2
size = (size + 0x3fffff) >> 22;
//计算给定线性地址对应的起始目录项。对应的目录号 = from >> 22, 因为每项占4个字节,并且由于也目录表从物理地址0开始存放,
//因为时间的目录项指针=目录号<<2即(from >> 20)“与”上0xffc 确保目录项指针范围有效,即用于屏蔽目录项指针最后2位
//因为只移动了20位,因此最后2位时也表项索引的内容(确保地址是4的倍数),应屏蔽掉
//例如,from >> 22 = 6, 说明地址高10位代表在页目录的第6项, 那么取出第6项的数据
//要先获取第6项所在的地址= 页目录起始地址 + 项数*4= 0 + 6 * 4 = 24。
//由此可得 0 + ((form >> 22) * 4)就是第6项所在的地址。那么也就等于 from >> 22 << 2 = form >> 20
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
//此时size是释放的页表个数,即页目录项数,而dir是起始目录项指针。
//现在开始循环操作页目录项,依次释放每个页表中的页表项。
for ( ; size-->0 ; dir++) {
if (!(1 & *dir))//如果当前目录项无效(P=0),表示该目录项没有使用(对应的页表不存在)
continue;//则继续处理下一个目录项
//否则从目录项中取出页表地址pg_table
pg_table = (unsigned long *) (0xfffff000 & *dir);//
//对该页表中1024个页表进行处理
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table)//判断页表释放有效
free_page(0xfffff000 & *pg_table);//释放有效的页表项对应的物理内存
*pg_table = 0;//对应页表的内容清0
pg_table++;//指向页表中下一项
}
free_page(0xfffff000 & *dir); //释放页表所占的内存页面
*dir = 0;//对应页表的目录项清0
}
invalidate();//刷新页变换高速缓冲
return 0;
}
2.4 copy_page_tables
复制页目录项和页表项
复制指定线性地址和长度内存对应的页目录项和页表项,从而被复制的页目录项和页表对应的原物理内存面区被两套页表映射而共享使用。
复制时,需申请新页面来存放新页表,原物理内存将被共享。此后两个进程(父进程和子进程)将共享内存区,直到有一个进程执行写操作时,内核才会为写操作分配新的内存页(写时复制机制)
参数:
- form 源线性地址
- to - 目的线性地址
- size - 需要复制(共享)的内存长度,单位时字节
//拷贝线性地址空间,从一个线性地址拷贝到另一个线性地址(fork)
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
unsigned long * from_page_table;
unsigned long * to_page_table;
unsigned long this_page;
unsigned long * from_dir, * to_dir;
unsigned long nr;
//首先检测给处的源地址form和目的地址to的有效性。
//源地址和目的地址都需要在4MB内存边界地址上。否则会出错死机
//做这样的要求时因为一个页表1024项可以管理4MB的内存
//源地址和目的地址只有满足这个要求才能满足从一个页表的第1项开始复制页表,并且新页表的最初所有项都是有效的。
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
//取得源地址和目的地址的起始目录项指针
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
//根据参数给定长度size计算出要占用页表数(即目录项数)
size = ((unsigned) (size+0x3fffff)) >> 22;
//下面开始对每个页目录项依次申请1页内存来保存对应的页表,并且开始页表项复制操作
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)//如果目录项知道的页表已经存在(P = 1),则出错死机
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))//如果源目录项无效,即指定的页表不存在(P = 0),则继续循环下一个页目录项的
continue;
//验证了源地址和目的地址否正常之后,我们取源目录项中的页表地址from_page_table
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
//为了保存目的目录项对应的页表,我们需要在主内存申请一页空闲内存页
if (!(to_page_table = (unsigned long *) get_free_page()))//如果返回0,说明申请失败,可能是内存不够
return -1; /* Out of memory, see freeing */
//设置目录项信息,把最后3位置位,表示页表映射的内存是用户级别,可读写,存在 (U/S, R/W, Present)
//如果U/S是0,则R/W没有作用。如果U/S是1,而R/W是0,说明运行在用户的代码只能读取页面。如果都为1,就有读写权限
*to_dir = ((unsigned long) to_page_table) | 7;//将申请页地址,放到页目录中的页目录项中
//针对当前处理的页目录对应的页表,设置需要复制的页面项数。
//如果是内核空间,则只需要复制160页对应的页表项(nr = 160),对于开始640KB物理内存,则需要复制1024页表项(nr = 1024)
nr = (from==0)?0xA0:1024;
//对于当前页表,开始循环复制指定的nr个内存页表项。
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;//首先取出源页表的内存
if (!(1 & this_page))//如果当前源页表没有使用,则不用复制该表项,继续处理下一项
continue;
this_page &= ~2;//设置其为只读权限 (R/W 位置1)
*to_page_table = this_page;//把页表项复制到目的页表中
//如果页表项所指的物理页面的地址在1MB以上,则需要设置内存页面映射数组mem_map[]
//于是计算页面号,并以它为索引在页面映射数组相应项中添加引用次数。
//对于1MB一下页面,说明是内核页面,因此不需要对数组进行设置。(内核中的线程地址和实际内存是一一对应的)
//因此对于内核任务0中调用fork创建任务1是,此时复制的页面还在内核代码区域,因此下面的判断不会执行,任务0的页面仍然可以随时读写
//只有当调用fork的父进程处于主内存区(页面位置大于1MB)时才会执行。这种情况需要在进程调用execve并装载了新执行程序代码时才会出现
if (this_page > LOW_MEM) {
*from_page_table = this_page;//令源页表项所指内存页为只读
//此时有两个进程共用内存区了,若其中一个进程需要进行写操作,则可以通过页异常写保护处理为执行写操作的进程分配一页新空闲页面,即写时复制
//下面时找个这个也在页面映射数组中的位置,并进行加1的操作
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();//刷新页变换高速缓冲
return 0;
}
所以在fork是复制了
1:复制了目录项里面的内容
2:复制了页表项中的内容
3:设置共享内存
2.5 put_page
把一物理内存映射到线性地址空间指定处
或者说把线性地址空间中指定的地址address处也页面映射到主内存页面page中
主要工作是把相关页目录项和页表项中设定指定的页面信息。若成功则返回物理页面地址。
在缺页异常的c函数do_no_page()中会调用此函数。对于缺页引起的异常,由于任何却页缘故而对页表进行修改时,并不需要刷新CPU的页变换缓冲(TLB)
即使页表项中标志P从0修改成1。因为无效页不会被缓冲。因此当修改一个无效的页表项时不需要刷新,在此表现为不调用invalidate函数
参数:
- page - 物理内存的某一个页面的指针
- address - 要映射的线性地址
unsigned long put_page(unsigned long page,unsigned long address)
{
unsigned long tmp, *page_table;
/* NOTE !!! This uses the fact that _pg_dir=0 */
//首先判定给定物理内存页面page的有效性
if (page < LOW_MEM || page >= HIGH_MEMORY)
printk("Trying to put page %p at %p\n",page,address);
//查看page页面是否已经申请页面,即判断页面映射字节图mem_map[]中相应的字节是否置位,字节应该置位,若没有则需要发出警告
if (mem_map[(page-LOW_MEM)>>12] != 1)
printk("mem_map disagrees with %p at %p\n",page,address);
//根据参数指定线性地址address计算在页目录表中对应的目录项,并从中取得二级页表地址
page_table = (unsigned long *) ((address>>20) & 0xffc);
if ((*page_table)&1)//如果目录项有效,即指定的页表在内存中
page_table = (unsigned long *) (0xfffff000 & *page_table);//则从中取得页表地址page_table
else {//否者
if (!(tmp=get_free_page()))//申请一个空闲页给页表使用
return 0;
*page_table = tmp|7;//设置目录项中相应的标志 111b
page_table = (unsigned long *) tmp;//把页表地址放到page_table
}
//最后在页表page_tage中设置相关页表项中内容,即把物理页page的地址填入到表项同时置3个标志位。
//该页表项在页表中方的索引值等于线程地址位21 - 12位组成的10位的值,每个页表可有1024项
page_table[(address>>12) & 0x3ff] = page | 7;
/* no need for invalidate 不需要刷新页变换高速缓冲*/
return page;//返回物理页面地址
}
2.6 un_wp_page
取消保护函数,用于页面异常中断过程中写保护异常处理(写时复制)
1:检查老的物理内存是否是共享内存
2:为table_entry申请新的内存页,并设置为可读可写
3:拷贝老的内存页给新的内存页
在内核创建进程时,新进程与父进程被设置成共享代码和数据内存页面,并且所有这些页面均被设置为只读页面
当新进程或原进程需要向内存页面写数据时,cpu就会检测到这个情况并产生页面写保护异常。
于是在这个函数中内核首先判断要写的页面是否被共享,若没有则把页面设置成可写然后退出。
若页面是处于共享状态,则需要申请一新页面并复制被写页面的内容,以供写进程单独使用。共享被取消
输入参数为页表指针
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;
//首先取参数指定的页表项中的物理页面位置,判断该页面是否是共享页面
old_page = 0xfffff000 & *table_entry;
//如果原页面的地址大于内存低端LOW_MEM(表示在主内存中),并且该页面映射字节数组中值为1(表示页面仅被引用一次,页面没有被共享)
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2; //把页面的页表项中置R/W标志位(可写)
invalidate();//刷新页面高速缓冲,然后返回
return;//即该内存页面此时值被一个进程使用并且不是内核中的进程,就直接把属性改成可写即可,不需要在重新申请一个页面
}
//否则就需要在主内存区内申请一页空闲页面给执行写操作的进程单独使用,取消页面共享
if (!(new_page=get_free_page()))
oom();
if (old_page >= LOW_MEM)//如果原页面大于内存低端(则一位着mem_map[] > 1,页面是共享的)
mem_map[MAP_NR(old_page)]--;//将原页面的页面映射字节数减1
*table_entry = new_page | 7;//置可读写等标志(U/S, R/W, P)
invalidate();//刷新页面变换高速缓冲
copy_page(old_page,new_page);//将原页面的内容复制新页面上
}
/*
* This routine handles present pages, when users try to write
* to a shared page. It is done by copying the page to a new address
* and decrementing the shared-page counter for the old page.
*
* If it's in code space we exit with a segment error.
*/
//执行写保护页面处理
//是写共享页面处理函数。是页面异常中断处理函数过程中调用的C函数。
//在page.s中被调用,参数error_code是进程在写写保护页面时由CPU产生的,addres是页面线性地址
//写共享页面时,需要复制页面
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
if (CODE_SPACE(address))
do_exit(SIGSEGV);
#endif
//调用un_wp_page来处理取消页面保护。但是首先需要为其准备好参数。
//参数时线程地址address指定的页面中的页表项指针,其计算方法
//1.(address>>10) & 0xffc) 计算指定线性地址中页表项在页表中的偏移地址
//2.((unsigned long *) ((address>>20) &0xffc) 用于区目录项中页表的地址值
//3. 1中页表项在页表中的偏移地址 加上 2中目录项内容对于的页表的物理地址,即可得到页表项指针(物理地址),这里对共享页面进行复制
un_wp_page((unsigned long *)
(((address>>10) & 0xffc) + (0xfffff000 &
*((unsigned long *) ((address>>20) &0xffc)))));
}
2.7 write_verify
写页面验证
若页面不可写,则复制页面,在fork.c中34行中被内存验证通用函数verify_area()调用。
参数:
- address 指的时页面线程地址
void write_verify(unsigned long address)
{
unsigned long page;
//首先取指定线性地址对于的页目录项,根据存在位P判断目录项对于的页表是否存在
// 这样处理是因为对于不存在的页面没有共享和写时复制可言,并且若程序对此不存在的页面执行写操作时,系统就会因为缺页异常而执行do_no_page,并为这个地方使用put_page()函数映射一个物理页面
if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
return;//为0则直接返回
page &= 0xfffff000;//从程序中取出页目录项的地址
page += ((address>>10) & 0xffc);//加上页面在页面中的偏移,得到对应地址的页表指针,在还页表指针中包含着给定线性地址对应的物理页面
//判断页表项中的bit1(R/W) 和bit0(P)如果页面不可写(R/W = 0)且存在,那么就执行共享检验和复制页面的操作。否则什么也不做,直接退出
if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present */
un_wp_page((unsigned long *) page);
return;
}
2.8 get_empty_page
取得一页空闲页并映射到指定线性地址处
get_free_page()仅是取得主内存中一页物理内存。而本函数不仅取得一页物理内存,还进一步调用put_page()。将物理内存映射到指定的线性地址处。
参数:
- address - 指定页面的线性地址
void get_empty_page(unsigned long address)
{
unsigned long tmp;
if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
free_page(tmp); /* 0 is ok - ignored 若不能取得一页空闲页面,或者不能将所有页面放到指定地址处,则显示内存信息不够*/
oom();
}
}
2.9 try_to_share
尝试对当前进程指定地址处的页面进行共享处理
当前进程与进程p是同一执行代码,也可以认为当前进程是由p进程fork操作产生的进程
因此他们的代码内容是一样的。如果未对数据段的内容进行修改那么数据段的内容也是一样的
参数:
- address - 进程中的逻辑地址,即是当前进程欲与p进程共享页面的逻辑页面地址。
如果p进程的address处的页面存在并且没有被修改过的话,就让当前进程与p进程共享。同时还需要验证指定的地址处是否已经申请了页面,若是则出错,死机返回
1:页面共享处理成功
0:失败
/*
* try_to_share() checks the page at address "address" in the task "p",
* to see if it exists, and if it is clean. If so, share it with the current
* task.
*
* NOTE! This assumes we have checked that p != current, and that they
* share the same executable.
*/
static int try_to_share(unsigned long address, struct task_struct * p)
{
unsigned long from;
unsigned long to;
unsigned long from_page;
unsigned long to_page;
unsigned long phys_addr;
from_page = to_page = ((address>>20) & 0xffc);//获取逻辑地址的目录项偏移
from_page += ((p->start_code>>20) & 0xffc);//计算出p进程的目录项
to_page += ((current->start_code>>20) & 0xffc);//计算出当前进程的目录项
/* is there a page-directory at from? */
from = *(unsigned long *) from_page;//获取p进程目录项的内容
if (!(from & 1))//bit 0 (P) = 0 ,说明二级页表不存在,返回
return 0;
from &= 0xfffff000;//取出目录项对应的页表地址from
from_page = from + ((address>>10) & 0xffc);//获取页表项指针
phys_addr = *(unsigned long *) from_page;//得到页表内容
/* is the page clean and present? */
if ((phys_addr & 0x41) != 0x01)//0x41 对应的页表项D(Dirty) 和 P 标志。如果页表不干净或者无效则返回
return 0;
phys_addr &= 0xfffff000;//取出页表项的物理页地址
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM) //判断地址的范围(在最小内存(1MB)到最大物理内存之间)
return 0;
//下面对当前进程的表项进行操作,目的是取得当前进程中address对应的页表项地址,并且该页表项地址还没有映射到物理页面,即P = 0
to = *(unsigned long *) to_page;//取得当前进程的目录项内容to
if (!(to & 1))//bit0 (P) = 0, 即目录项对应的页表不存在
if (to = get_free_page())//申请一空闲页来存放页表
*(unsigned long *) to_page = to | 7;//并更新到目录项to_page中,让其指向页表
else
oom();//申请失败
to &= 0xfffff000;//获取页表的地址
to_page = to + ((address>>10) & 0xffc);//页表地址 + 页表项的偏移地址 = 页表项地址
if (1 & *(unsigned long *) to_page)//bit0 (P) = 1, 页表项对应的物理页面已存在,说明内核出错
panic("try_to_share: to_page already exists");
/* share them: write-protect */
//现在对两个进程进行共享处理
*(unsigned long *) from_page &= ~2; //首先进写保护
*(unsigned long *) to_page = *(unsigned long *) from_page;//将p进程address对应的页表中的页表项(指向实际的物理地址)映射给当前进进程addrss对应的页表中的页表项。
invalidate();//刷新页表变换高速缓冲
//下面是计算,所操作的物理页面的页面号。并将对应页面映射数组中引用加1
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++;
return 1;
}
2.10 share_page
共享页面处理
在发生缺页异常时,首先看看是否能与运行同一执行文件的其他进程进行页面共享处理。
该函数首先判断系统是否有另一个进程页运行当前进程一样的执行文件。若有,则子系统当前所有任务中寻找这样的任务。
若找到了这样的任务就尝试与其共享指定地址处的页面。
若没有,则共享页面的前提就不存在,因此函数立即退出。
判断系统是否有另一个进程也在执行同一个执行文件的方法是利用进程任务数据结构体中的executable字段,该字段是进程正在执行程序内存中的i节点
根据i节点的引用次数i_count,我们可以得出这样的判断
若executable->i_count的值大于1,则表明系统中可能有两个进程在运行同一个执行文件
于是可以再对任务结构体数组中所有任务比较是否有相同的executable字段来确定最后多个进程运行相同执行文件的情况
参数:
- address 进程中逻辑地址,即是当前进程欲与p进程共享页面的逻辑页面地址
返回 1-成功 0-失败
static int share_page(unsigned long address)
{
struct task_struct ** p;
if (!current->executable)//检查当前进程的executable字段是否指向某执行文件的i节点
return 0;
if (current->executable->i_count < 2)//查看该节点的引用指数值,小于2,就表明,只有当前进程运行该执行文件,就无共享可言
return 0;
for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {//搜索任务数组中的所有任务
if (!*p)//如果任务空闲,继续寻找
continue;
if (current == *p)//如果就是当前任务,继续寻找
continue;
if ((*p)->executable != current->executable)//executable不相等,表示运行的不是与当前进程相同的执行文件,继续寻找
continue;
if (try_to_share(address,*p))//如果找到,就尝试共享页面
return 1;
}
return 0;
}
2.11 do_no_page
执行缺页处理
页异常中断处理函数,在page.s程序中被调用
参数error_code和address,是进程在访问页面时由CPU因缺页异常而自动生成的
error_code -指出出错类型
address - 是产生异常的页面线性地址
该函数首先尝试与已加载相同文件的进行进行页面共享,或者只是进程动态申请内存页面页只需要映射一页物理内存页即可。
若共享操作不成,那么只能从相应文件中读入所却的数据页面到指定的线性地址处
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
//首先取线程地址
address &= 0xfffff000;//address处缺页的页面地址
tmp = address - current->start_code; //缺页页面对应的逻辑地址
//如果(当前进程的executable 节点指针空 或者指定地址超过(代码 加 数据的长度))
//则申请一页物理内存,并映射到线性地址address处
//executable 是进程正在运行的执行文件的i节点结构。由于任务0和1的代码在内核内,因此任务0和1,以及任务1派生的没有调用execve的所有任务的executable 都是0
//若这个值是0或者线性地址超过代码加数据的长度 说明 进程在申请新的内存页面存放堆或栈中的数据,因此直接为进程申请一页物理内存并映射到指定线性地址处
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address);
return;
}
//否则则说明缺页面在进程执行文件范围内,于是尝试共享操作,成功则突出
if (share_page(tmp))
return;
if (!(page = get_free_page()))//不成功就只能申请一页物理内存
oom();
/* remember that 1 block is used for header 记住,程序头要使用一个数据块*/
//因为块设备上存放的执行文件映像第一块数据是程序头结构,因此在读取该文件时需要跳过第1块数据
//所以需要首先计算缺页所在的数据块号,因为每个数据块长度为1KB,因此一页可存放4个数据块,
//进程逻辑地址tmp除以数据块的大小再加1即可得出缺少的页面再执行映像文件的起始块号block。
//根据这个块号和执行文件的i节点,我们就可以从映射位图中找到对应块设备中找到对应的设备逻辑块号(保存再nr[]数组中)
//利用bread_page即可把4个逻辑块读入到物理页面page中
block = 1 + tmp/BLOCK_SIZE;
for (i=0 ; i<4 ; block++,i++)
nr[i] = bmap(current->executable,block);
bread_page(page,current->executable->i_dev,nr);
//再设备逻辑操作时,可能会有这样的情况,即在执行文件中读取的页面位置可能文件尾不到1页页面的长度。因此就可能读入一些无用的信息,
//下面操作就是把这部分超出执行文件end_data以后的部分清0
i = tmp + 4096 - current->end_data;//超出字节长度值
tmp = page + 4096;//tmp指向页面末端
while (i-- > 0) {//页面末端i字节清0
tmp--;
*(char *)tmp = 0;
}
//最后把引起缺页异常的一页物理内存映射到指定线性地址address处,成功就返回
if (put_page(page,address))
return;
//失败就释放内存页,显示内存不够
free_page(page);
oom();
}
2.12 mem_init
物理内管理处理化
该函数堆1MB以上内存区域 以页面为单位进行管理前的处理化设置工作。
一个页面长度是4KB字节。该函数把1MB以上所有物理内存划分一个个页面,并使用一个页面映射字节数组mem_map来进行管理。
对于具有16MB内存容量的机器,该数组共有3840项((16MB - 1MB) / 4KB),即可管理3840个物理页面。
每当一个物理内存被占用时就把mem_map[]中对应的字节值增1,若释放一个物理页面,就把对应字节数值减1
若字节值为0,则表示对应页面空闲;若字节值大于等于1,则表示对应页面被占用或这被不同程序共享占用
具有16MB物理内存和512KB虚拟磁盘区机器的mem_map[]数组初始化的情况
参数:
- start_mem - 可以用作页面分配的主内存区起始地址
- end_mem - 实际物理内存最大地址
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
void mem_init(long start_mem, long end_mem)
{
int i;
//首先将1MB到16MB范围内所有内存页面对应的内存映射字节数组项置为已占用状态,即各项字节值全部设置为USED(100)
//PAGING_PAGES = (15MB / 4KB = 3840)即1MB以上所有物理内存后的内页页面数
HIGH_MEMORY = end_mem;
for (i=0 ; i<PAGING_PAGES ; i++)
mem_map[i] = USED;
//计算主内存的起始内存 start_mem处页面对应内存映射字节数组中项号i和主内存区页面数
//此时i对应主内存区中的第一个页面
i = MAP_NR(start_mem); //主内存起始位置出的页面号
end_mem -= start_mem;
end_mem >>= 12;//主内存中总页面数
while (end_mem-->0)
mem_map[i++]=0;//主内存页面对应的字节清0
}
2.13 calc_mem
计算内存空闲页面数并显示
void calc_mem(void)
{
int i,j,k,free=0;
long * pg_tbl;
//扫描内存页面映射数组mem_map[],获取空闲页面数并显示
for(i=0 ; i<PAGING_PAGES ; i++)
if (!mem_map[i]) free++;
printk("%d pages free (of %d)\n\r",free,PAGING_PAGES);
//扫描页目录项(除了0项和1项),如果页目录有效,则统计对应页表中中有效页面数,并显示。
//页目录项0-3被内核使用,因此从第5个目录项(i = 4)开始扫描
for(i=2 ; i<1024 ; i++) {//初始值i应该等于4
if (1&pg_dir[i]) {
pg_tbl=(long *) (0xfffff000 & pg_dir[i]);
for(j=k=0 ; j<1024 ; j++)
if (pg_tbl[j]&1)
k++;
printk("Pg-dir[%d] uses %d pages\n",i,k);
}
}
}
3 页异常中断处理程序
page.s
error_code:错误码,当发生页错误的时候,CPU直接把错误码压入栈堆
线性地址:从控制寄存器CR2中获取,CR2专门用来存放页出错时的线性地址
error_code的结构
bit0:(p) 0-页不存在 , 1-页保护
bit1:0-读操作,1-写操作
bit2:0-超级用户复制
该文件包括页异常处理中断处理程序(中断14),主要分为两种情况处理。
一是由于缺页引起的页异常中断。通过调用do_no_page(error_code, address)来处理
二是由于写保护引起的页异常。此时调用do_wp_page(error_code, address)来进行处理
/*
* linux/mm/page.s
*
* (C) 1991 Linus Torvalds
*/
/*
* page.s contains the low-level page-exception code.
* the real work is done in mm.c
*/
.globl _page_fault
_page_fault:
xchgl %eax,(%esp) //esp是栈堆顶,当发生错误时,cpu把错误码压入栈中,所以esp存放错误码。取得出错码eax
//入栈
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
//置内核数据段选择符
movl $0x10,%edx //0x10特殊地址,edx数据段地址
mov %dx,%ds//当前程序的数据段
mov %dx,%es//当前程序的额外段
mov %dx,%fs//当前程序的fs段
movl %cr2,%edx//把当前的线性地址存放到edx中
pushl %edx//把线性地址压入栈,作为将调用函数的参数
pushl %eax//把错误码压入栈,作为将调用函数的参数
testl $1,%eax//错误码的bit0(位0)是否为1 ,
jne 1f //不为1跳到1处, f代表后面
call _do_no_page //否则调用函数缺页处理函数
jmp 2f //跳到2处
1: call _do_wp_page//调用写保护处理函数
2: addl $8,%esp //丢弃压入栈的两个参数,弹出栈中寄存器并退出中断
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret