前言
页异常中断处理程序(中断 14),主要分两种情况处理。一是由于缺页引起的页异常中断,通过调用 do_no_page(error_code, address) 来处理;二是由页写保护引起的页异常,此时调用页写保护处理函数 do_wp_page(error_code, address) 进行处理。其中的出错码(error_code)是由 CPU 自动产生并压入堆栈的,出现异常时访问的线性地址是从控制寄存器 CR2 中取得的。CR2 是专门用来存放页出错时的线性地址。
一、页异常中断处理
页异常中断处理实现在 mm/page.s 文件的 page_fault 函数中。
.globl page_fault page_fault: xchgl %eax,(%esp) pushl %ecx pushl %edx push %ds push %es push %fs movl $0x10,%edx mov %dx,%ds mov %dx,%es mov %dx,%fs movl %cr2,%edx pushl %edx pushl %eax testl $1,%eax jne 1f call do_no_page jmp 2f 1: call do_wp_page 2: addl $8,%esp pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret
1、页写保护引起的页异常 do_wp_page 函数
do_wp_page 函数定义在 mm/memory.c 文件中。
/* * 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 自动产生,address 是页面线性地址。 // 写共享页面时,需复制页面(写时复制)。 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 */ /* 我们现在还不能这样做:因为 estdio 库会在代码空间执行写操作 */ /* 真是太愚蠢了。我真想从 GNU 得到 libc.a 库。*/ if (CODE_SPACE(address)) // 如果地址位于代码空间,则终止执行程序。 do_exit(SIGSEGV); #endif // 调用上面函数 un_wp_page()来处理取消页面保护。但首先需要为其准备好参数。参数是 // 线性地址 address 指定页面在页表中的页表项指针,其计算方法是: // (1) ((address>>10) & 0xffc);计算指定线性地址中页表项在页表中的偏移地址;因为。 // 根据线性地址结构,(address>>12) 就是页表项中的索引,但每项占 4 个字节,因此乘 // 4 后:(address>>12)<<2 = (address>>10)&0xffc 就可得到页表项在表中的偏移地址。 // 与操作&0xffc 用于限制地址范围在一个页面内。 又因为只移动了 10 位,因此最后 2 位 // 是线性地址低 12 位中的最高 2 位,也应屏蔽掉。 因此求线性地址中页表项在页表中偏 // 移地址直观一些的表示方法是(((address>>12)& 0x3ff)<<2 )。 // (2)(0xfffff000 & *((address>>20) &0xffc)):用于取目录项中页表的地址值;其中, // ((address>>20) &0xffc)用于取线性地址中的目录索引项在目录表中的偏移位置。因为 // address>>22 是目录项索引值,但每项 4 个字节,因此乘以 4 后: (address>>22)<<2 // = (address>>20) 就是指定项在目录表中的偏移地址。 &0xffc 用于屏蔽目录项索引值 // 中最后 2 位。因为只移动了 20 位,因此最后 2 位是页表索引的内容,应该屏蔽掉。而。 // *((address>>20)&0xffc) 则是取指定目录表项内容中对应页表的物理地址。最后与上。 // 0xffffff000 用于屏蔽掉页目录项内容中的一些标志位(目录项低 12 位)。直观表示为 // (0xffffff000 & *((unsigned long *) (((address>>22) & 0x3ff)<<2)))。 //)由中页表项在页表中偏移地址加上 2中目录表项内容中对应页表的物理地址即可 // 得到页表项的指针(物理地址)。这里对共享的页面进行复制。 un_wp_page((unsigned long *) (((address>>10) & 0xffc) + (0xfffff000 & *((unsigned long *) ((address>>20) &0xffc))))); }
un_wp_page 函数
un_wp_page 函数定义在 mm/memory.c 文件中。
取消写保护页面函数。用于页异常中断过程中写保护异常的处理(写时复制)。 // 在内核创建进程时,新进程与父进程被设置成共享代码和数据内存页面,并且所有这些页面 // 均被设置成只读页面。而当新进程或原进程需要向内存页面写数据时,CPU 就会检测到这个。 // 情况并产生页面写保护异常。于是在这个函数中内核就会首先判断要写的页面是否被共享。 // 若没有则把页面设置成可写然后退出。若页面是出于共享状态,则需要重新申请一新页面并 // 复制被写页面内容,以供写进程单独使用。共享被取消。本函数供下面 do_wp_page () 调用。 // 输入参数为页表项指针,是物理地址。[ un_wp_page -- Un-Write Protect Page] void un_wp_page(unsigned long * table_entry) { unsigned long old_page,new_page; // 首先取参数指定的页表项中物理页面位置(地址)并判断该页面是否是共享页面。如果原 // 页面地址大于内存低端 LOW_MEM(表示在主内存区中),并且其在页面映射字节图数组中。 // 值为 1(表示页面仅被引用 1 次,页面没有被共享),则在该页面的页表项中置 R/W 标志 // (可写),并刷新页变换高速缓冲,然后返回。即如果该内存页面此时只被一个进程使用, // 并且不是内核中的进程,就直接把属性改为可写即可,不用再重新申请一个新页面。 old_page = 0xfffff000 & *table_entry; if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) { *table_entry |= 2; invalidate(); return; } // 否则就需要在主内存区内申请一页空闲页面给执行写操作的进程单独使用,取消页面共享。 // 如果原页面大于内存低端(则意味着 mem_map > 1,页面是共享的),则将原页面的页 // 面映射字节数组值递减 1。然后将指定页表项内容更新为新页面地址,并置可读写等标志。 // (II/S、R/W、P)。在刷新页变换高速缓冲之后,最后将原页面内容复制到新页面上。 if (!(new_page=get_free_page())) oom(); // Out of Memory。内存不够处理。 if (old_page >= LOW_MEM) mem_map[MAP_NR(old_page)]--; *table_entry = new_page | 7; invalidate(); copy_page(old_page,new_page); }
2、缺页引起的页异常 do_no_page 函数
do_no_page 函数定义在 mm/memory.c 文件中。
// 执行缺页处理。 // 是访问不存在页面处理函数。页异常中断处理过程中调用的函数。在 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 处页面地址。从而可算出指定线性地址在进程空间中 // 相对于进程基址的偏移长度值 tmp,即对应的逻辑地址。 address &= 0xfffff000; // address 处缺页页面地址。 tmp = address - current->start_code; // 缺页页面对应逻辑地址。 // 若当前进程的 executable 节点指针空,或者指定地址超出(代码 + 数据)长度,则申请 // 一页物理内存,并映射到指定的线性地址处。executable 是进程正在运行的执行文件的 i // 节点结构。由于任务 0 和任务 1 的代码在内核中,因此任务 0、任务 1 以及任务 1 派生的 // 没有调用过 execve()的所有任务的 executable 都为 0。若该值为 0,或者参数指定的线性。 // 地址超出代码加数据长度,则表明进程在申请新的内存页面存放堆或栈中数据。因此直接 // 调用取空闲页面函数 get_empty_page() 为进程申请一页物理内存并映射到指定线性地址 // 处。进程任务结构字段 start_code 是线性地址空间中进程代码段地址,字段 end_data // 是代码加数据长度。对于 Linux 0.11 内核,它的代码段和数据段起始基址相同。 if (!current->executable || tmp >= current->end_data) { get_empty_page(address); return; } // 否则说明所缺页面在进程执行映像文件范围内,于是就尝试共享页面操作,若成功则退出 // 若不成功就只能申请一页物理内存页面 page,然后从设备上读取执行文件中的相应页面并 // 放置(映射)到进程页面逻辑地址 tmp 处。 if (share_page(tmp)) //尝试逻辑地址 tmp 处页面的共享。 return; if (!(page = get_free_page())) oom(); /* remember that 1 block is used for header */ /* 记住, (程序)头要使用 1 个数据块 */ // 因为块设备上存放的执行文件映像第 1 块数据是程序头结构,因此在读取该文件时需要跳过 // 第 1 块数据。所以需要首先计算缺页所在的数据块号。因为每块数据长度为 BLOCK_SIZE = // 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); // 读设备上 4 个逻辑块。 // 在读设备逻辑块操作时,可能会出现这样一种情况,即在执行文件中的读取页面位置可能离 // 文件尾不到 1 个页面的长度。因此就可能读入一些无用的信息。下面的操作就是把这部分超 // 出执行文件 end_data 以后的部分清零处理。 i = tmp + 4096 - current->end_data; // 超出的字节长度值。 tmp = page + 4096; // tmp 指向页面末端。 while (i-- > 0) { // 页面末端 i 字节清零。 tmp--; *(char *)tmp = 0; } // 最后把引起缺页异常的一页物理页面映射到指定线性地址 address 处。若操作成功就返回。 // 否则就释放内存页,显示内存不够。 if (put_page(page,address)) return; free_page(page); oom(); }
share_page 函数
share_page 函数定义在 mm/memory.c 文件中。
/* * share_page()试图找到一个进程,它可以与当前进程共享页面。参数 address 是 * 当前进程数据空间中期望共享的某页面地址。 * * 首先我们通过检测 executable->i_count 来查证是否可行。如果有其他任务已共享。 * 该 inode,则它应该大于 1。 */ // 共享页面处理。 // 在发生缺页异常时,首先看看能否与运行同一个执行文件的其他进程进行页面共享处理。 // 该函数首先判断系统中是否有另一个进程也在运行当前进程一样的执行文件。若有,则在 // 系统当前所有任务中寻找这样的任务。若找到了这样的任务就尝试与其共享指定地址处的 // 页面。若系统中没有其他任务正在运行与当前进程相同的执行文件,那么共享页面操作的 // 前提条件不存在,因此函数立刻退出。判断系统中是否有另一个进程也在执行同一个执行 // 文件的方法是利用进程任务数据结构中的 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; // 首先检查一下当前进程的 executable 字段是否指向某执行文件的 i 节点,以判断本进程。 // 是否有对应的执行文件。如果没有,则区回 0。如果 executable 的确指向某个 i 节点, // 则检查该 i 节点引用计数值。如果当前进程运行的执行文件的内存 i 节点引用计数等于 // 1(executable->i_count =1),表示当前系统中只有 1 个进程(即当前进程)在运行该 // 执行文件。因此无共享可言,直接退出函数。 if (!current->executable) return 0; if (current->executable->i_count < 2) return 0; // 否则搜索任务数组中所有任务。寻找与当前进程可共享页面的进程,即运行相同执行文件 // 的另一个进程,并尝试对指定地址的页面进行共享。如果找到某个进程 p,其 executable。 // 字段值与当前进程的相同,则调用 try_to_share() 尝试页面共享。若共享操作成功,则 //函数返回 1。否则返回 0,表示共享页面操作失败。 for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) { if (!*p) // 如果该任务项空闲,则继续寻找。 continue; if (current == *p) // 如果就是当前任务,也继续寻找。 continue; // 如果 executable 不等,表示运行的不是与当前进程相同的执行文件,因此也继续寻找。 if ((*p)->executable != current->executable) continue; if (try_to_share(address,*p)) // 尝试共享页面。 return 1; } return 0; }
try_to_share 函数
try_to_share 函数定义在 mm/memory.c 文件中。
/* * try_to_share()在任务"p"中检查位于地址"address"处的页面,看页面是否存在, * 是否干净。如果是干净的话,就与当前任务共享。 * 注意!这里我们已假定 p !=当前任务,并且它们共享同一个执行程序。 */ // 尝试对当前进程指定地址处的页面进行共享处理。 // 当前进程与进程 p 是同一执行代码,也可以认为当前进程是由 p 进程执行 fork 操作产生的 // 进程,因此它们的代码内容一样。如果未对数据段内容作过修改那么数据段内容也应一样。 // 参数 address 是进程中的逻辑地址,即是当前进程欲与 p 进程共享页面的逻辑页面地址。 // 进程 p 是将被共享页面的进程。如果 p 进程 address 处的页面存在并且没有被修改过的话, // 就让当前进程与 p 进程共享之。同时还需要验证指定的地址处是否已经申请了页面,若是 // 则出错,死机。返回:1 - 页面共享处理成功;0 - 失败。 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; // 首先分别求得指定进程 p 中和当前进程中逻辑地址 address 对应的页目录项。为了计算方便 // 先求出指定逻辑地址 address 处的'逻辑'页目录项号,即以进程空间(0 - 64MB)算出的页 // 目录项号。该' 逻辑'页目录项号加上进程 p 在 CPU 4G 线性空间中起始地址对应的页目录项, // 即得到进程 p 中地址 address 处页面所对应的 4G 线性空间中的实际页目录项 from_page。 // 而'逻辑'页目录项号加上当前进程 CPU 4G 线性空间中起始地址对应的页目录项,即可最后 // 得到当前进程中地址 address 处页面所对应的 4G 线性空间中的实际页目录项 to_page。 from_page = to_page = ((address>>20) & 0xffc); from_page += ((p->start_code>>20) & 0xffc); // p 进程目录项 to_page += ((current->start_code>>20) & 0xffc); // 当前进程目录项 // 在得到 p 进程和当前进程 address 对应的目录项后,下面分别对进程 p 和当前进程进行处理。 // 下面首先对 p 进程的表项进行操作。目标是取得 p 进程中 address 对应的物理内存页面地址, // 并且该物理页面存在,而且干净(没有被修改过,不脏)。 // 方法是先取目录项内容。如果该目录项无效(P=0),表示目录项对应的二级页表不存在, // 于是返回。否则取该目录项对应页表地址 from,从而计算出逻辑地址 address 对应的页表项。 // 指针,并取出该页表项内容临时保存在 phys_addr 中。 /* is there a page-directory at from? */ /* 在 from 处是否存在页目录项?*/ from = *(unsigned long *) from_page; // p 进程目录项内容。 if (!(from & 1)) return 0; from &= 0xfffff000; // 页表指针(地址)。 from_page = from + ((address>>10) & 0xffc); // 页表项指针。 phys_addr = *(unsigned long *) from_page; // 页表项内容。 /* is the page clean and present? */ /* 物理页面干净并且存在吗? */ // 接着看看页表项映射的物理页面是否存在并且干净。 0x41 对应页表项中的 D(Dirty)和 // P(Present)标志。如果页面不干净或无效则返回。然后我们从该表项中取出物理页面地址。 // 再保存在 phys_addr 中。最后我们再检查一下这个物理页面地址的有效性,即它不应该超过 // 机器最大物理地址值,也不应该小于内存低端(1MB)。 if ((phys_addr & 0x41) != 0x01) return 0; phys_addr &= 0xfffff000; // 物理页面地址。 if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM) return 0; // 下面首先对当前进程的表项进行操作。目标是取得当前进程中 address 对应的页表项地址, // 并且该页表项还没有映射物理页面,即其 P=0。 // 首先取当前进程页目录项内容 to。如果该目录项无效(P=0),即目录项对应的二级页表 // 不存在,则申请一空闲页面来存放页表,并更新目录项 to_page 内容,让其指向该内存页面。 to = *(unsigned long *) to_page; // 当前进程目录项内容。 if (!(to & 1)) { if ((to = get_free_page())) *(unsigned long *) to_page = to | 7; else oom(); } // 否则取目录项中的页表地址 t0,加上页表项索引值<<2,即页表项在表中偏移地址,得到 // 页表项地址 to_page。 针对该页表项,如果此时我们检查出其对应的物理页面已经存在, // 即页表项的存在位 P=1,则说明原本我们想共享进程 p 中对应的物理页面,但现在我们自己。 // 已经占有了(映射有)物理页面。于是说明内核出错,死机。 to &= 0xfffff000; // 页表地址。 to_page = to + ((address>>10) & 0xffc); // 页表项地址。 if (1 & *(unsigned long *) to_page) panic("try_to_share: to_page already exists"); // 在找到了进程 p 中逻辑地址 address 处对应的干净且存在的物理页面,而且也确定了当前 // 进程中逻辑地址 address 所对应的二级页表项地址之后,我们现在对他们进行共享处理。 // 方法很简单,就是首先对 p 进程的页表项进行修改,设置其写保护(R/W=0,只读)标志, // 然后让当前进程复制 p 进程的这个页表项。此时当前进程逻辑地址 address 处页面即被 // 映射到 p 进程逻辑地址 address 处页面映射的物理页面上。 /* share them: write-protect */ /* 对它们进行共享处理:写保护 */ *(unsigned long *) from_page &= ~2; *(unsigned long *) to_page = *(unsigned long *) from_page; // 随后刷新页变换高速缓冲。计算所操作物理页面的页面号,并将对应页面映射字节数组项中 // 的引用道增 1。最后返回 1,表示共享处理成功。 invalidate(); phys_addr -= LOW_MEM; phys_addr >>= 12; // 得页面号。 mem_map[phys_addr]++; return 1; }
bmap 函数
函数在文件 fs/inode.c 中。
// 文件数据块映射到盘块的处理操作。(block 位图处理函数,bmap - block map) // 参数:inode - 文件的 i 节点指针;block - 文件中的数据块号;create - 创建块标志。 // 该函数把指定的文件数据块 block 对应到设备上逻辑块上,并返回逻辑块号。如果创建标志 // 置位,则在设备上对应逻辑块不存在时就申请新磁盘块,返回文件数据块 block 对应在设备。 // 上的逻辑块号(盘块号)。 static int _bmap(struct m_inode * inode,int block,int create) { struct buffer_head * bh; int i; // 首先判断参数文件数据块号 block 的有效性。如果块号小于 0,则停机。如果块号大于直接 // 块数 + 间接块数 + 二次间接块数,超出文件系统表示范围,则停机。 if (block<0) panic("_bmap: block<0"); if (block >= 7+512+512*512) panic("_bmap: block>big"); // 然后根据文件块号的大小值和是否设置了创建标志分别进行处理。如果该块号小于 7,则使 // 用直接块表示。如果创建标志置位,并且i 节点中对应该块的逻辑块(区段)字段为 0,则 // 向相应设备申请一磁盘块(逻辑块),并且将盘上逻辑块号(盘块号)填入逻辑块字段中。 // 然后设置 i 节点改变时间,置 i 节点已修改标志。 最后返回逻辑块号。 函数 new_block() // 定义在 bitmap. c 程序中第 75 行开始处。 if (block<7) { if (create && !inode->i_zone[block]) if ((inode->i_zone[block]=new_block(inode->i_dev))) { inode->i_ctime=CURRENT_TIME; inode->i_dirt=1; // 设置已修改标志。 } return inode->i_zone[block]; } // 如果该块号>=7,且小于 7+512,则说明使用的是一次间接块。下面对一次间接块进行处理。 // 如果是创建,并且该 i 节点中对应间接块字段 i_zone[7] 是 0,表明文件是首次使用间接块, // 则需申请一磁盘块用于存放间接块信息,并将此实际磁盘块号填入间接块字段中。 然后设 // 置 i 节点已修改标志和修改时间。 如果创建时申请磁盘块失败,则此时 i 节点间接块字段 // i_zone[7]为 0,则返回 0。或者不是创建,但 i_zone[7]原来就为 0,表明 i 节点中没有间 // 接块,于是映射磁盘块失败,返回 0 退出。 block -= 7; if (block<512) { if (create && !inode->i_zone[7]) if ((inode->i_zone[7]=new_block(inode->i_dev))) { inode->i_dirt=1; inode->i_ctime=CURRENT_TIME; } if (!inode->i_zone[7]) return 0; // 现在读取设备上该 i 节点的一次间接块。并取该间接块上第 block 项中的逻辑块号(盘块 // 号)i。每一项占 2 个字节。如果是创建并且间接块的第 block 项中的逻辑块号为 0 的话, // 则申请一磁盘块,并让间接块中的第 block 项等于该新逻辑块块号。然后置位间接块的已 // 修改标志。如果不是创建,则 i 就是需要映射(寻找)的逻辑块号。 if (!(bh = bread(inode->i_dev,inode->i_zone[7]))) return 0; i = ((unsigned short *) (bh->b_data))[block]; if (create && !i) if ((i=new_block(inode->i_dev))) { ((unsigned short *) (bh->b_data))[block]=i; bh->b_dirt=1; // 最后释放该间接块占用的缓冲块,并返回磁盘上新申请或原有的对应 block 的逻辑块块号。 } brelse(bh); return i; } // 若程序运行到此,则表明数据块属于二次间接块。其处理过程与一次间接块类似。下面是对 // 二次间接块的处理。首先将 block 再减去间接块所容纳的块数(512),然后根据是否设置。 // 了创建标志进行创建或寻找处理。如果是新创建并且i节点的二次间接块字段为 0,则需申 // 请一磁盘块用于存放二次间接块的一级块信息,并将此实际磁盘块号填入二次间接块字段。 // 中。之后,置 i 节点已修改编制和修改时间。同样地,如果创建时申请磁盘块失败,则此。 // 时 i 节点二次间接块字段 i_zone[8] 为 0,则返回 0。 或者不是创建,但 i_zone[8] 原来就 // 为 0,表明 节点中没没有间接块,于是映射磁盘块失败,返回 0 退出。 block -= 512; if (create && !inode->i_zone[8]) if ((inode->i_zone[8]=new_block(inode->i_dev))) { inode->i_dirt=1; inode->i_ctime=CURRENT_TIME; } if (!inode->i_zone[8]) return 0; // 现在读取设备上该 i 节点的二次间接块。并取该二次间接块的一级块上第(block/512) // 项中的逻辑块号 i。 如果是创建并且二次间接块的一级块上第(block/512) 项中的逻辑 // 块号为 0 的话,则需申请一磁盘块(逻辑块)作为二次间接块的二级块 i,并让二次间接。 // 块的一级块中第 (block/512) 项等于该二级块的块号 i。然后置位二次间接块的一级块已。 // 修改标志。并释放二次间接块的一级块。如果不是创建,则 i 就是需要映射(寻找)的逻 // 辑块号。 if (!(bh=bread(inode->i_dev,inode->i_zone[8]))) return 0; i = ((unsigned short *)bh->b_data)[block>>9]; if (create && !i) if ((i=new_block(inode->i_dev))) { ((unsigned short *) (bh->b_data))[block>>9]=i; bh->b_dirt=1; } brelse(bh); // 如果二次间接块的二级块块号为 0,表示申请磁盘块失败或者原来对应块号就为 0,则返 // 回 0 退出。否则就从设备上读取二次间接块的二级块,并取该二级块上第 block 项中的逻 // 辑块号(与上 511 是为了限定 block 值不超过 511)。 if (!i) return 0; if (!(bh=bread(inode->i_dev,i))) return 0; i = ((unsigned short *)bh->b_data)[block&511]; // 如果是创建并且二级块的第 block 项中逻辑块号为 0 的话,则申请一磁盘块(逻辑块), // 作为最终存放数据信息的块。并让二级块中的第 block 项等于该新逻辑块块号(i)。然后 // 置位二级块的已修改标志。 if (create && !i) if ((i=new_block(inode->i_dev))) { ((unsigned short *) (bh->b_data))[block&511]=i; bh->b_dirt=1; // 最后释放该二次间接块的二级块,返回磁盘上新申请的或原有的对应 block 的逻辑块块号。 } brelse(bh); return i; } 取文件数据块 block 在设备上对应的逻辑块号。 // 参数:inode - 文件的内存i节点指针;block - 文件中的数据块号。 // 若操作成功则返回对应的逻辑块号,否则返回 0 int bmap(struct m_inode * inode,int block) { return _bmap(inode,block,0); }
bread_page 函数
函数在文件 fs/buffer.c 中
/* * bread_page 一次读四个缓冲块数据读到内存指定的地址处。它是一个完整的函数, * 因为同时读取四块可以获得速度上的好处,不用等着读一块,再读一块了。 */ /// 读设备上一个页面(4 个缓冲块)的内容到指定内存地址处。 // 参数 address 是保存页面数据的地址;dev 是指定的设备号;b[4] 是含有 4 个设备数据块号 // 的数组。 该函数仅用于 mm/memory.c 文件的 do_no_page ( 函数中(第 386 行)。 void bread_page(unsigned long address,int dev,int b[4]) { struct buffer_head * bh[4]; int i; // 该函数循环执行 4 次,根据放在数组 b 口中的 4 个块号从设备 dev 中读取一页内容放到指定 // 内存位置 address 处。 对于参数 b[i]给出的有效块号,函数首先从高速缓冲中取指定设备。 // 和块号的缓冲块。如果缓冲块中数据无效(未更新)则产生读设备请求从设备上读取相应数。 // 据块。对于 b[i]无效的块号则不用去理它了。因此本函数其实可以根据指定的 b [中的块号 // 随意读取 1-4 个数据块。 for (i=0 ; i<4 ; i++) if (b[i]) { // 若块号有效。 if ((bh[i] = getblk(dev,b[i]))) if (!bh[i]->b_uptodate) ll_rw_block(READ,bh[i]); } else bh[i] = NULL; // 随后将 4 个缓冲块上的内容顺序复制到指定地址处。在进行复制(使用)缓冲块之前我们。 // 先要睡眠等待缓冲块解锁(若被上锁的话)。另外,因为可能睡眠过了,所以我们还需要 -// 在复制之前再检查一下缓冲块中的数据是否是有效的。复制完后我们还需要释放缓冲块。 for (i=0 ; i<4 ; i++,address += BLOCK_SIZE) if (bh[i]) { wait_on_buffer(bh[i]); // 等待缓冲块解锁(若被上锁的话)。 if (bh[i]->b_uptodate) // 若缓冲块中数据有效的话则复制。 COPYBLK((unsigned long) bh[i]->b_data,address); brelse(bh[i]); // 释放该缓冲区。 } } // 复制内存块 // 从 from 地址复制一块(1024 字节)数据到 to 位置 #define COPYBLK(from,to) \ __asm__("cld\n\t" \ "rep\n\t" \ "movsl\n\t" \ ::"c" (BLOCK_SIZE/4),"S" (from),"D" (to) \ )
put_page 函数
函数在文件 mm/memory.c 中
/* * 下面函数将一内存页面放置(映射)到指定线性地址处。它返回页面 * 的物理地址,如果内存不够(在访问页表或页面时),则返回 0。 */ // 把一物理内存页面映射到线性地址空间指定处。 // 或者说是把线性地址空间中指定地址 address 处的页面映射到主内存区页面 page 上。主要 // 工作是在相关页目录项和页表项中设置指定页面的信息。若成功则返回物理页面面地址。 在。 // 处理缺页异常的 C函数 do_no_page() 中会调用此函数。对于缺页引起的异常,由于任何缺。 // 页缘故而对页表作修改时,并不需要刷新 CPU 的页变换缓冲(或称 Translation Lookaside // Buffer - 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 */ /* 注意!!!这里使用了页目录基址_pg_dir=0 的条件 */ // 首先判断参数给定物理内存页面 page 的有效性。如果该页面位置低于 1.OW_MEM(1MB)或 // 超出系统实际含有内存高端 HIGH_MEMORY,则发出警告。LOW_MEM 是主内存区可能有的最。 // 小起始位置。当系统物理内存小于或等于 6MB 时,主内存区起始于 LOW_MEM 处。再查看一 // 下该 page 页面是否是已经申请的页面,即判断其在内存页面映射字节图 mem_map 中相。 // 应字节是否已经置位。若没有则需发出警告。 if (page < LOW_MEM || page >= HIGH_MEMORY) printk("Trying to put page %p at %p\n",page,address); if (mem_map[(page-LOW_MEM)>>12] != 1) printk("mem_map disagrees with %p at %p\n",page,address); // 然后根据参数指定的线性地址 address 计算其在页目录表中对应的目录项指针,并从中取得 // 二级页表地址。 如果该目录项有效(P=1),即指定的页表在内存中,则从中取得指定页表。 // 地址放到 page_table 变量中。否则就申请一空闲页面给页表使用,并在对应目录项中置相。 // 应标志(7 - User、U/S、R/W)。然后将该页表地址放到 page_table 变量中。参见对 115 // 行语句的说明。 page_table = (unsigned long *) ((address>>20) & 0xffc); if ((*page_table)&1) page_table = (unsigned long *) (0xfffff000 & *page_table); else { if (!(tmp=get_free_page())) return 0; *page_table = tmp|7; page_table = (unsigned long *) tmp; } // 最后在找到的页表 page_table 中设置相关页表项内容,即把物理页面 page 的地址填入表 // 项同时置位 3 个标志(U/S、W/R、P)。该页表项在页表中的索引值等于线性地址位 21 - // 位 12 组成的 10 比特的值。每个页表共可有 1024 项(0 -- 0x3ff)。 page_table[(address>>12) & 0x3ff] = page | 7; /* no need for invalidate */ /* 不需要刷新页变换高速缓冲 */ return page; // 返回物理页面地址。 }
free_page 函数
函数在文件 mm/memory.c 中
/* * 释放物理地址' addr'处的一页内存。用于函数'free_page_tables()'。 */ // 释放物理地址 addr 开始的 1 页面内存。 // 物理地址 1MB 以下的内存空间用于内核程序和缓冲,不作为分配页面的内存空间。因此 // 参数 addr 需要大于 1MB。 void free_page(unsigned long addr) { // 首先判断参数给定的物理地址 addr 的合理性。如果物理地址 addr 小于内存低端(IMB), // 则表示在内核程序或高速缓冲中,对此不予处理。如果物理地址 addr >= 系统所含物理 // 内存最高端,则显示出错信息并且内核停止工作。 if (addr < LOW_MEM) return; if (addr >= HIGH_MEMORY) panic("trying to free nonexistent page"); // 如果对参数 addr 验证通过,那么就根据这个物理地址换算出从内存低端开始计起的内存 // 页面号。页面号 = (addr - LOW_MEM)/4096。可见页面号从 0 号开始计起。此时 addr // 中存放着页面号。如果该页面号对应的页面映射字节不等于 0,则减 1 返回。此时该映射 // 字节值应该为 0,表示页面已释放。如果对应页面字节原本就是 0,表示该物理页面本来 // 就是空闲的,说明内核代码出问题。于是显示出错信息并停机。 addr -= LOW_MEM; addr >>= 12; if (mem_map[addr]--) return; mem_map[addr]=0; panic("trying to free free page"); }
oom 函数
函数在文件 mm/memory.c 中
#define SIGSEGV 11 /* Segmentation violation (ANSI). */ // 函数名前的关键字 volatile 用于告诉编译器 gcc 该函数不会返回。这样可让 gcc 产生更好一 // 些的代码,更重要的是使用这个关键字可以避免产生某些(未初始化变量的)假警告信息。 volatile void do_exit(long code); // 进程退出处理函数,在 kernel/exit.c,102 行。 // 显示内存已用完出错信息,并退出。 static inline void oom(void) { printk("out of memory\n\r"); do_exit(SIGSEGV); // do_exit()应该使用退出代码,这里用了信号值 SIGSEGV(11) // 相同值的出错码含义是"资源暂时不可用",正好同义。 }