
Linux内核-内存管理
关于虚拟内存更多的概念可以去看笔者以前的一篇文章:虚拟内存的运作-kidwjb的小站
地址概念
- 逻辑地址:程序看到的地址,Linux操作系统分配给每一个进程的独立地址
- 线性地址:总线地址ARM CPU采用分段机制(线性地址=逻辑地址+段基地址)
- 物理地址:CPU总线的直接地址
虚拟内存映射方式
主要采用了:分段和分页机制
分段机制
分段机制将逻辑地址转换成线性地址
分段机制和全局描述表GDT和局部描述表IDT有关,结构如下:

GDT表一共有256项,除去最前面的四项占用,剩余252项
在GDT中一个进程的tss段和idt各占一项,所以一个进程在GDT中共占用两项
- 其中tss0指向进程0状态段基地址
- idt0指向进程0代码段数据段的基地址
- 代码段指向进程0代码基地址
- 数据段指向进程0数据基地址
因此能够跑的最大进程数 = (256-4) / 2 = 126个进程
一个进程的逻辑地址分配虚拟地址64M,所以可分配的最大地址为126 * 64M = 8G
但是0.11内核规定最多为NR_TASKS = 64个,所以线性地址为64*64 = 4G
分页机制
分页机制将线性地址转换成物理内存映射
在操作系统中有多个页目录项,但是当前使用的只有一个,并且这个页目录项的存储地址在CR3寄存器(x86)中,在head.s中被创建

- 线性地址的31~22位共10个bit用来确定页目录项中的对应目录项
- 21~12位共10bit用来寻址页目录项指定的页表中的页表项
- 最后12个bit用来作为页表项指定的一页物理内存中的偏移地址
项结构
页目录项和页表项都是4个字节,并且结构是完全相同的

- 页框地址指定了一页内存的物理起始地址,因为内存页是位于4K边界上的,所以它的低12位永远是0 ,所以低12位就可以有其他的作用。在页目录项中页框地址是一个页表的起始地址;在页表项中的页框地址则包含期望内存操作的物理内存页地址
- P表示有效位,P=1表示当前项有效,可以直接用于地址转换,P=0表示地址无效,CPU会发出缺页异常,进行中断异常处理。中断处理完成后再次执行指令
- A是已访问位,D是已修改位;
- R/W读写位和U/S用户/超级用户位不用于地址转换,而是用于分页级的保护机制
mm源码分析
基础宏定义
// 刷新页变换高速缓冲。
// 为了提高地址转换的效率,CPU 将最近使用的页表数据存放在芯片中高速缓冲中。在修改过页表
// 信息之后,就需要刷新该缓冲区。这里使用重新加载页目录基址寄存器 cr3 的方法来进行刷新。
#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))
/* these are not to be changed without changing head.s etc */
#define LOW_MEM 0x100000 // 内存低端(1M)
#define PAGING_MEMORY (15*1024*1024) // 分页内存 15M
#define PAGING_PAGES (PAGING_MEMORY>>12) // 0xf00 = 3840 分页后的页数
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12) // 计算指定地址的页面号 这里的addr是物理地址
#define USED 100
//给定的线性地址是否在当前代码中
#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \
current->start_code + current->end_code)
// 复制页面
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si")
static unsigned char mem_map [ PAGING_PAGES ] = {0,}; // 内存映射字节位图(1 字节代表 1 页)
这里的mem_map就和之前的各种全局描述符表如file_talbe,inode_table,superblock_table一样
它是物理内存映射图(1字节代表1页内存),每个页面对应的字节用于标志页面当前被引用的次数,它最大可以映射15M的内存空间 。不能用作主内存页面的位置会被预先设置成USED(100)
获取空闲页面
/*
* 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(di=mem_map+PAGING_PAGES-1)。
// 输出:返回%0(ax=页面号)。
// 从内存映像末端开始向前扫描所有页面标志(页面总数为 PAGING_PAGES),如果有页面空闲(对应
// 内存映像位为 0)则返回页面地址。
unsigned long get_free_page(void)
{
// 在mem_map 中反向扫描得到空闲页面,故从16M高地址开始分配页面
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t"
"jne 1f\n\t"
"movb $1,1(%%edi)\n\t"
"sall $12,%%ecx\n\t"
"addl %2,%%ecx\n\t"
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"
"leal 4092(%%edx),%%edi\n\t"
"rep ; stosl\n\t"
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
:"di","cx","dx");
return __res;
}
释放一页内存
/*
* Free a page of memory at physical address 'addr'. Used by
* 'free_page_tables()'
*/
void free_page(unsigned long addr)
{
if (addr < LOW_MEM) return;
if (addr >= HIGH_MEMORY)
panic("trying to free nonexistent page");
addr -= LOW_MEM; // 物理地址-低端内存位置,再除以 4KB,得页面号
addr >>= 12;
if (mem_map[addr]--) return; // 如果对应内存页面映射字节不等于 0,则减 1 返回
mem_map[addr]=0;
panic("trying to free free page");
}
这里传入的参数是物理地址addr
首先判断参数给定的物理地址addr的合理性,如果小于内存低端或者大于系统最大物理内存都错误
然后通过(addr - LOW_MEM)/4096获取得到这块内存对应的页面号
如果该页面号对应的页面映射字节不等于0,就减1返回,此时该映射字节应该是0,表示页面已经释放,如果字节本身为0,那么就说明代码出现问题
释放页表连续的内存块
该函数用于释放页表连续的内存块(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.
*/
/**
* @brief 释放页表连续的内存块
*
* @param from 起始基地址
* @param size 释放的长度
* @return int 0-成功
*/
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
if (from & 0x3fffff)
panic("free_page_tables called with wrong alignment");
if (!from)
panic("Trying to free up swapper memory space");
size = (size + 0x3fffff) >> 22;
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
for ( ; size-->0 ; dir++) {
if (!(1 & *dir))
continue;
pg_table = (unsigned long *) (0xfffff000 & *dir);
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table)
free_page(0xfffff000 & *pg_table);
*pg_table = 0;
pg_table++;
}
free_page(0xfffff000 & *dir);
*dir = 0;
}
invalidate(); // 刷新页变换高速缓冲
return 0;
}
1.查看线性地址是否合法
if (from & 0x3fffff)
panic("free_page_tables called with wrong alignment");
if (!from)
panic("Trying to free up swapper memory space");
2.释放长度对齐4MB
size = (size + 0x3fffff) >> 22;
这里0x3fffff代表4MB,这里用(size + 4MB) / 4MB是为了向上取整。比如size=4.01MB,这样的话实际需要释放2*4MB内存
3.获取得到对应页目录项
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
这里其实是下面代码的简化:
dir = (unsigned long *) ( ( (from>>22) << 2) & 0xffc); /* _pg_dir = 0 */
如上面分页机制中表示线性地址空间的表示,线性地址空间的高10位表示的对应的页目录项,所以这里先将线性地址from >> 22
但是由于一个目录项是4个字节,而且整个一级页表基地址为0,那么第一个目录项地址为0 = 0 * 4,第二个为4 = 1 * 4,第三个为8 = 2 * 4,所以需要再向左移两位(乘以4),所以最终可以合并为(from>>20)
而为什么& 0xffc呢?是因为为了确保目录项指针范围有效。0xffc的低2位为0,由于上面操作是右移20位,那么还剩下两位页表项的内容,所以需要屏蔽掉
4.释放内存
获取得到正确的目录项后,就可以开始释放内存操作
注意这里的size已经是代表要释放几个4MB内存空间
for ( ; size-->0 ; dir++) {
if (!(1 & *dir))
continue;
pg_table = (unsigned long *) (0xfffff000 & *dir);
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table)
free_page(0xfffff000 & *pg_table);
*pg_table = 0;
pg_table++;
}
free_page(0xfffff000 & *dir);
*dir = 0;
}
整体使用一个for循环实现,每一次for循环代表释放了一片4M空间,并且页目录项指向下一个。(原因是1个页目录项指向4096个页表项,一个页表项指向一页内存4096KB)
- 首先判断当前页目录项的最低位P位是否已经为0,如上面项结构所述,如果P位为0就代表指向的空间本身就是无效,不需要再去专门进行释放操作,直接进行下一次循环
if (!(1 & *dir))
continue;
- 然后如果目录项P位有效,那么就获取得到指向的二级页表(页表项表)的基地址
pg_table = (unsigned long *) (0xfffff000 & *dir);
- 每一个二级页表内有4096个页表项,一项指向一页内存,所以使用一个4096的for循环;先去看每一个页表项的P位是否为1,为1说明需要手动释放,调用
free_page释放一页内存,并且该页表项指向内存置0;然后pg_table++指向下一个页表项
注意:页表项和页目录项指向的都是实际物理地址;
*dir(页目录项的值):里面存放的永远是 物理地址(页表的物理基址) + 页表属性(如存在位 P=1)。因为 MMU 硬件在查表时,只认物理地址。*pg_table(页表项的值):里面存放的永远是 物理地址(物理页框的基址) + 页面属性(如 R/W 等)。
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table)
free_page(0xfffff000 & *pg_table);
*pg_table = 0;
pg_table++;
}
- 如果二级页表内的所有页表项都释放万了内存,那么就释放调用二级页表本身占用的物理内存,并且让当前页目录项指向的地址置0,然后开启下一轮循环
free_page(0xfffff000 & *dir);
*dir = 0;
5.同步高速缓冲区
invalidate(); // 刷新页变换高速缓冲
return 0;
复制页目录项和页表项
/**
* @brief 复制进程的页目录页表 拷贝线性地址空间,从一个线性地址拷贝到另一个线性地址
*
* @param from 源地址的内存偏移
* @param to 目的地址的内存偏移
* @param size 需要复制的内存大小
* @return int
*/
int copy_page_tables(unsigned long from,unsigned long to,long size)
// 进程1创建时 from = 0, to = 64M, size = 640k或160个页面
unsigned long * from_page_table;
unsigned long * to_page_table;
unsigned long this_page;
unsigned long * from_dir, * to_dir;
unsigned long nr;
该函数用于复制指定线性地址和长度内存对应的页目录项和页表项,从而被复制的页目录和页表对应的原物理内存页面区被两套页表映射而共享使用
该函数区分了进程1创建时的拷贝和其他进程的创建拷贝
1.获取源/目的地址的目录项
if ((from&0x3fffff) || (to&0x3fffff)) // 源地址和目的地址都需要是 4Mb 的倍数。否则出错,死机
panic("copy_page_tables called with wrong alignment");
// 取得源地址和目的地址的目录项(from_dir 和 to_dir) 分别是0和64
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22; // 计算要复制的内存块占用的页表数 4M(1个页目录项管理的页面大小 = 1024*4K)的数量
首先是对两个地址进行了合法检测
然后使用上面free_page_tables用到的方法获取得到源地址和目的地址对应的页目录项
并且计算出要复制的内存大小占用多少个4MB
2.循环复制页目录项
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir) // 如果目的目录项指定的页表已经存在,死机
panic("copy_page_tables: already exist");
if (!(1 & *from_dir)) // 如果源目录项未被使用,不用复制,跳过
continue;
和上面释放操作类似,也是使用一个大的for循环
在开始会进行一个判断,如果目的目录项指定的页表已经存在,那么就会panic
然后还会判断要拷贝的页表是不是本身就无效没有被使用,如果是的话就直接跳过进行下一轮
2.1获取源页表基地址
//取得页表基地址
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
2.2为目的页表分配空闲内存
// // 为目的页表取一页空闲内存. 关键!!是目的页表存储的地址
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
2.3设置目的目录项信息
//设置目的目录项信息。7 是标志信息,表示(Usr, R/W, Present)
*to_dir = ((unsigned long) to_page_table) | 7;
to_dir是目的目录项 ;而*to_dir是存放的页表的基地址+标志位
这里将刚刚获取得到的空闲内存页作为目的页表(二级页表),这里| 7也就是让最低3位全部置1;他们分别是U/S, R/W, Present
| U/S | R/W | Present |
|---|---|---|
| 0 | 无效 | |
| 1 | 1(可写) | |
| 1 | 0(只读) |
这里设置当前的页表内存是可写的
2.4判断是否是0号进程fork
// 如果是进程0复制给进程1,则复制160个页面;否则将1024个页面全部复制
nr = (from==0)?0xA0:1024;
如果是0号进程fork,那么就不需要那么多内存页,160个页面就够了
2.5循环复制页表项
接下来操作就是循环复制页表项,这里和上面的释放操作其实是一样的思路
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table; // 复制!
if (!(1 & this_page))
continue;
this_page &= ~2; // ~(010), 代表用户,只读,存在
*to_page_table = this_page; // 复制!
// 如果该页表项所指页面的地址在 1M 以上,则需要设置内存页面映射数组 mem_map[]
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
- 这里是用来一个临时变量
this_page,先把源地址对应的页表项指向的内存物理地址传递给this_page
注意:这里
*from_page_table内容是当前页表存放的对应内存物理地址,而不是当前页表地址
- 然后
this_page &= ~2;,也就是最低位变为1 0 1:用户,只读,有效;这是因为此时两个进程都是共享着一块内存,共享内存只能读,若是要写会触发写保护异常然后写时复制机制 - 最后
*to_page_table接收最终的内存物理地址+标志位 - 如果当前复制的内存地址在1M以上,也就是说明不是内核进程0复制,内存地址在1~15M,那么就需要置位全局的映射数组
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
3.同步高速缓冲区
invalidate(); // 刷新页变换高速缓冲
return 0;
总结
在fork的时候并没有复制内存,而是复制的页目录项和页表项,让两个进程页表项都指向同一块内存 ;在这个过程中只创建了页表的内存
建立线性地址和物理地址映射
/*
* This function puts a page in memory at the wanted address.
* It returns the physical address of the page gotten, 0 if
* out of memory (either when trying to access page-table or
* page.)
*/
/**
* @brief 在指定的物理地址处申请一个页面, 主要工作是在页目录和页表中设置指定页面的信息
*
* @param page 物理地址中的内存偏移
* @param address 逻辑地址(线性地址)
* @return unsigned long 成功申请到的页面地址
*/
unsigned long put_page(unsigned long page,unsigned long address)
put_page函数的功能就是把一个物理内存页面映射到给定的线性地址处,最后返回页面的物理地址
该函数有一个很重要的一点就是不会调用invalidate刷新TLB缓冲
1.判断参数有效性
首先查看给定物理内存页面page的有效性,如果页面位置低于LOW_MEM或高于HIGH_MEMORY都警告
并且查看当前page是否是已经申请的页面
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);
2.获取页目录项
page_table = (unsigned long *) ((address>>20) & 0xffc);
3.获取页表基地址
if ((*page_table)&1) // 如果该目录项有效(P=1)(也即指定的页表在内存中)
page_table = (unsigned long *) (0xfffff000 & *page_table);
else {
if (!(tmp=get_free_page()))
return 0;
*page_table = tmp|7; // 否则,申请空闲页面给页表使用,并在对应目录项中置相应标志 7(User, U/S, R/W)。然后将 该页表的地址->page_table
page_table = (unsigned long *) tmp;
}
这里先查看当前页目录项所指向的页表是否有效,(*page_table)&1,如果有效就说明页表存在
如果无效,那么页表不存在,就需要去创建页表get_free_page,并且设置标志位, 然后目录项指向这个页表基地址
4.将物理内存映射到页表
page_table[(address>>12) & 0x3ff] = page | 7; // 在页表中设置指定地址页面的页表项内容。每个页表共可有 1024 项(0x3ff)
/* no need for invalidate */
return page;
这里此时page_table指向了页表基地址,那么page_table[]就相当于是某一个页表项
而(address>>12) & 0x3ff就是取线性地址的中间10位,得到页表项是第几个
最后让这个页表项指向这块物理页的地址+标志位
为什么不用调用invalidate
这里我们首先要知道TLB缓存的内容是**“虚拟地址 → 物理地址”的最终转换结果(包括物理页框号和属性)**
之所以 put_page 末尾注释写 /* no need for invalidate */,真正的原因不是因为“操作的是二级页表”,而是因为“该线性地址之前绝对没有有效的 TLB 缓存条目”。
invalidate()(刷新 TLB)的作用是:当内存中的页表/页目录内容发生改变时,让 CPU 丢弃 TLB 中过时的旧缓存,强制去内存重新读取。
put_page 的逻辑只有两种分支:
- 分支 1(目录项无效):
*page_table(页目录项)的 P 位为 0。这意味着该线性地址对应的页目录表都不存在。- 既然目录项不存在,MMU 在访问这个地址时,根本不可能在 TLB 中命中任何有效的转换结果(因为硬件查表卡在第一步就结束了)。
- 所以,新分配的页目录项和页表项,都是从“不存在”变成“存在”。TLB 里没有这个地址的旧缓存,自然不需要刷掉任何东西。
- 分支 2(目录项有效,但页表项不存在):
*page_table存在,但取出的*page_table(指向页表的物理地址)被赋值给了page_table指针,随后执行:page_table[(address>>12) & 0x3ff] = page | 7;
这说明该目录项虽然有效,但这个具体的页表项(PTE)之前是无效的(P=0)。- 既然这个具体的线性地址之前是“缺页”状态,那么 CPU 之前试图访问它时,TLB 里也绝对没有该地址的有效缓存(否则 MMU 早就命中并把物理地址给你了,根本不会触发缺页中断来调用
put_page)。
- 既然这个具体的线性地址之前是“缺页”状态,那么 CPU 之前试图访问它时,TLB 里也绝对没有该地址的有效缓存(否则 MMU 早就命中并把物理地址给你了,根本不会触发缺页中断来调用
写时复制(取消写保护页面)
这里传递进入的参数是页表项指针,不是页目录项指针
/**
* @brief 取消写保护页面(undo write protect page),即写时复制
*
* @param table_entry 页表项指针
*/
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;
old_page = 0xfffff000 & *table_entry; // 取原页面地址
// 如果原页面地址大于内存低端 LOW_MEM(1Mb)并且其在页面映射字节数组中值为 1(表示仅被
// 引用 1 次,页面没有被共享),则在该页面的页表项中置 R/W 标志(可写),并刷新页变换
// 高速缓冲,然后返回。
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2;
invalidate();
return;
}
// 否则,申请一页空闲页面
if (!(new_page=get_free_page()))
oom();
// 如果原页面大于内存低端(则意味着 mem_map[]>1,页面是共享的),则将原页面的页面映射
// 数组值递减 1。然后将指定页表项内容更新为新页面的地址,并置可读写等标志(U/S, R/W, P)。
// 刷新页变换高速缓冲。最后将原页面内容复制到新页面。
if (old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--;
*table_entry = new_page | 7;
invalidate();
copy_page(old_page,new_page);
}
1.检查老的物理内存是否是共享内存
old_page = 0xfffff000 & *table_entry; // 取原页面地址
// 如果原页面地址大于内存低端 LOW_MEM(1Mb)并且其在页面映射字节数组中值为 1(表示仅被
// 引用 1 次,页面没有被共享),则在该页面的页表项中置 R/W 标志(可写),并刷新页变换
// 高速缓冲,然后返回。
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2;
invalidate();
return;
}
这里先通过指定的页表项指针获取得到物理内存页
然后检查当前物理内存页映射,查看引用次数是否为1,如果为1说明没有被共享,就直接刷新一下标志位为写,然后刷新高速缓冲区再返回就行了
2.分配新内存页给当前页表项
// 否则,申请一页空闲页面
if (!(new_page=get_free_page()))
oom();
// 如果原页面大于内存低端(则意味着 mem_map[]>1,页面是共享的),则将原页面的页面映射
// 数组值递减 1。然后将指定页表项内容更新为新页面的地址,并置可读写等标志(U/S, R/W, P)。
// 刷新页变换高速缓冲。最后将原页面内容复制到新页面。
if (old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--;
*table_entry = new_page | 7;
invalidate();
如果不为1,就说原本内存被共享,那么就为当前页表项分配一个新的内存页,然后将原本内存页的全局映射-1
然后让当前指定的页表项指向新的内存页并且设置标志位为用户+可写+有效
3.拷贝旧内存页到新内存页
copy_page(old_page,new_page);
在最后拷贝旧内存页内容到新的内存页 ;因为这里调用时机是写时复制,所以肯定是除了分配新的内存页还要复制内容
页面异常调用函数
/**
* @brief 页面异常中断处理调用函数,即写共享页面,需要取消写保护,并进行写时复制
* 它是通过将页面复制到一个新地址上并递减原页面的共享页面计数值实现的
*
* @param error_code CPU 自动产生的错误码
* @param 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 */
if (CODE_SPACE(address))
do_exit(SIGSEGV);
#endif
// 处理取消页面保护。参数指定页面在页表中的页表项指针,其计算方法是:
// ((address>>10) & 0xffc):计算指定地址的页面在页表中的偏移地址;
// (0xfffff000 &((address>>20) &0xffc)):取目录项中页表的地址值,
// 其中((address>>20) &0xffc)计算页面所在页表的目录项指针;
// 两者相加即得指定地址对应页面的页表项指针
un_wp_page((unsigned long *)
(((address>>10) & 0xffc) + (0xfffff000 &
*((unsigned long *) ((address>>20) &0xffc)))));
}
该函数是写保护页时触发的页面异常调用函数,里面只做了一件事就是调用上面的写时复制函数un_wp_page
这里的un_wp_page传递进去的是线性地址对应的页表项地址,计算方式如下:
(address>>20) &0xffc):获取得到线性地址对应的目录项地址*((unsigned long *) ((address>>20) &0xffc):对刚刚获取得到的页目录项地址解引用,获取得到页目录项的内容(0xfffff000 & *((unsigned long *) ((address>>20) &0xffc)):页目录项内容的高20位是页帧地址(详见最上面的项结构),也就是指向的页表基地址((address>>10) & 0xffc):线性地址的中间10bit用来寻址页目录项指定的页表中的页表项(页表偏移)((address>>10) & 0xffc)+(0xfffff000 & *((unsigned long *) ((address>>20) &0xffc)):页表偏移+页表基址得到对应的页表地址
写页面验证
//// 写页面验证。若可写则直接写,否则写时复制
void write_verify(unsigned long address)
{
unsigned long page;
if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
return;
page &= 0xfffff000;
page += ((address>>10) & 0xffc);
// 如果该页面不可写(标志 R/W 没有置位),则执行共享检验和复制页面操作(写时复制)
if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present */
un_wp_page((unsigned long *) page);
return;
}
该函数用于验证要写的页面,如果可以写则直接写,否则就调用写时复制函数
函数首先根据线性地址address得到目录项地址,并且&1判断是否是指向有效的页表,如果不是就可以直接返回了
然后获取得到对应的页表项,方式如上面函数,这里不赘述
然后使用3 &页表项内容查看读写位是否置位,如果没有置位就说明是共享的,需要进行写时复制
获取指定地址空页面
//// 取指定地址的空页面。
// 与 get_free_page()不同。get_free_page()仅置内存页面映射数组 mem_map[]中的引用标志。
// 而这里 get_empty_page()不仅是取指定地址处的页面,还进一步调用 put_page(),将页面信息
// 添加到页目录和页表中。
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();
}
}
该函数就是调用了两个函数:get_free_page和put_page ,获取一个空闲页并且建立页目录项和页表项的映射
共享指定进程的指定页面
如同注释所写的,在任务p中检查位于地址address处的页面,看页面是否存在,是否干净。如果是干净的话,就与当前任务共享
注意:这里的address是当前进程的虚拟地址
/**
* @brief 在任务p中检查位于地址address处的页面,看页面是否存在,是否干净。
* 如果是干净的话,就与当前任务共享
*
* @param address 指定的内存地址
* @param p 指定的任务
* @return int 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;
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;
if (!(from & 1)) // 如果该目录项无效(P=0)
return 0;
from &= 0xfffff000; // 则取该目录项对应页表地址
from_page = from + ((address>>10) & 0xffc); // 计算地址对应的页表项指针值
phys_addr = *(unsigned long *) from_page;
/* is the page clean and present? */
if ((phys_addr & 0x41) != 0x01)
return 0;
phys_addr &= 0xfffff000;
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
return 0;
to = *(unsigned long *) to_page;
if (!(to & 1))
if (to = get_free_page())
*(unsigned long *) to_page = to | 7;
else
oom();
to &= 0xfffff000;
to_page = to + ((address>>10) & 0xffc);
if (1 & *(unsigned long *) to_page)
panic("try_to_share: to_page already exists");
/* share them: write-protect */
*(unsigned long *) from_page &= ~2; // 对 p 进程中页面置写保护标志(置 R/W=0)。并且当前进程中的对应页表项指向它
*(unsigned long *) to_page = *(unsigned long *) from_page;
invalidate();
// 计算所操作页面的页面号,并将对应页面映射数组项中的引用递增 1
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++;
return 1;
}
1.获取address在进程中的目录项
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); // 计算地址在当前进程中对应的页目录项
第一行代码是计算出address在当前进程和目标进程的目录项偏移,因为address是虚拟地址所以需要下面两行通过start_code代码段基址获取得到目录项基址;偏移+基址就得到了对应的目录项地址
2.获取from的页表项
/* is there a page-directory at from? */
from = *(unsigned long *) from_page;
if (!(from & 1)) // 如果该目录项无效(P=0)
return 0;
from &= 0xfffff000; // 则取该目录项对应页表地址
from_page = from + ((address>>10) & 0xffc); // 计算地址对应的页表项指针值
这里首先判断一下目录项指向的页表是否有效
然后获取得到对应的页表项
3.获取from对应的页面地址
phys_addr = *(unsigned long *) from_page;
/* is the page clean and present? */
if ((phys_addr & 0x41) != 0x01)
return 0;
phys_addr &= 0xfffff000;
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
return 0;
phys_addr获取得到页表项的内容,然后&0x41也就是1000001查看**dirt位和有效位**,如果dirt位为1,那么就说明当前页面有改动,不能共享,直接返回失败
如果页面是未改动,那就获取得到页面地址
4.获取to的页表项
to = *(unsigned long *) to_page;
if (!(to & 1))
if (to = get_free_page())
*(unsigned long *) to_page = to | 7;
else
oom();
to &= 0xfffff000;
to_page = to + ((address>>10) & 0xffc);
这里也是和上面很多函数一样的指针操作,这里做了判断是否指向的页表存在,不存在就申请一块页面去存放页表(注意不是页面)
5.共享页面
if (1 & *(unsigned long *) to_page)
panic("try_to_share: to_page already exists");
/* share them: write-protect */
*(unsigned long *) from_page &= ~2; // 对 p 进程中页面置写保护标志(置 R/W=0)。并且当前进程中的对应页表项指向它
*(unsigned long *) to_page = *(unsigned long *) from_page;
invalidate();
// 计算所操作页面的页面号,并将对应页面映射数组项中的引用递增 1
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++;
return 1;
这里最开始会去检查当前页表项是否已经指向了一页内存,如果是那么就报错崩溃
然后将目标内存页的读写标志位置位只读,并且让当前进程的指定页表内容填充为目的进程的指定页表的内容,实现两个页表指向同一页内存
最后将全局映射内存表+1;表示这块内存多了一个进程引用
寻找一个进程和当前进程共享页面
/**
* @brief 试图找到一个进程,它可以与当前进程共享页面
*
* @param address 期望共享的某页面地址
* @return int 1-成功,0-失败
*/
static int share_page(unsigned long address)
{
struct task_struct ** p;
if (!current->executable)
return 0;
if (current->executable->i_count < 2)
return 0;
for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
if (!*p)
continue;
if (current == *p)
continue;
if ((*p)->executable != current->executable)
continue;
if (try_to_share(address,*p))
return 1;
}
return 0;
}
share_page函数在缺页异常中被调用,目的是找到一个和当前进程运行相同可执行文件的进程,然后调用try_to_share去共享指定地址的内存
这里的executable指向当前进程正在执行程序文件的inode ,并且如果这个inode的i_count小于2,那就说明当前只有它一个进程执行这个文件,不能共享,直接返回失败
缺页异常处理函数
/**
* @brief 缺页异常处理函数
*
* @param error_code CPU自动产生的错误码
* @param 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; // 页面地址
tmp = address - current->start_code;
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 */
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);
i = tmp + 4096 - current->end_data;
tmp = page + 4096;
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
if (put_page(page,address))
return;
free_page(page);
oom();
}
1.获取虚拟地址(逻辑地址)
address &= 0xfffff000; // 页面地址
tmp = address - current->start_code;
address - current->start_code就是当前线性地址在当前进程中的偏移,也就是逻辑地址(虚拟地址)
2.动态申请内存情况
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address);
return;
}
- 如果当前进程的
executable为空 ,也就是没有执行文件,那么就为当前地址申请一块内存;executable为空一般是进程0和进程1以及进程1派生的进程的情况,进程0和进程1的代码在内核中,没有执行文件节点 - 如果当前逻辑地址大于代码段+数据段长度,则表明进程在申请新的内存页存放堆或栈中数据
所以直接调用get_empty_page申请一块物理内存页并映射到指定的线性地址处
3.共享内存页面
if (share_page(tmp))
return;
如果不是上述两种情况,就说明缺页页面是在进程执行文件内,那么就尝试共享页面成功则退出
4.读取设备内容映射到线性地址
如果上面共享内存页面失败了,那就说明可能当前没有进程和当前进程执行同一个文件,或者他们都没有映射这块地址在内存上,那就需要申请一页空闲内存get_free_page ,然后从设备上读取执行文件中的相应页面并进行映射
if (!(page = get_free_page()))
oom();
读取线性地址对应的文件内容并拷贝到申请的内存中
/* remember that 1 block is used for header */
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);
这里block获取的是当前逻辑地址在文件中对应的i_zone块号,由于块设备上存放的执行文件映像第一块是程序头结构,所以在读取文件时需要跳过第一块内容
然后由于BLOCK_SIZE是1024KB,所以一页有4个块大小,所以需要读取四块盘块
使用bmap获取得到i_zone块号对应的设备块号
最后通过设备块号将内容读取到高速缓冲区,然后拷贝到申请的内存中
清理超过文件尾的内存
在读设备块时,由于一次读取的4KB,所以可能出现执行文件读取页面的位置到文件末尾不足4kB,那么就会读入一些无用信息,所以需要把超出部分清零
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
5.进行内存和线性低值映射
if (put_page(page,address))
return;
free_page(page);
oom();
物理内存管理初始化
// 假定内存为16M
// start_mem = 4 * 1024 * 1024
// end_mem = 16 * 1024 * 1024
// PAGING_PAGES = 15*1024*1024 >> 12 mem_map 大小为3k,每一项映射一个4k的页面,数组较大,用以适配不同的内存大小
// 注意mem_map支队1MB以上的内存分页进行管理
// 将0~2M的内存标记为USED, 剩下的清零
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem;
for (i=0 ; i<PAGING_PAGES ; i++)
mem_map[i] = USED;
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12;
while (end_mem-->0)
mem_map[i++]=0;
}
page.s代码
该汇编代码就只有一个,就是发生page_fault中断;
在中断触发时CPU会自动把错误玛压入栈,然后调用中断这里就一开始从esp栈顶获取到错误玛
线性地址存放在CR2寄存器
_page_fault:
xchgl %eax,(%esp) //esp存放错误玛
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
//置内核数据段选择符
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
//把当前线性地址存入edx
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
error code结构:
后三位结构:
| 0 | 0 为页不存在 | 1表示页保护 |
|---|---|---|
| 1 | 0表示读 | 1表示写 |
| 2 | 0表示在超级用户运行 | 1表示在普通用户运行 |
testl $1,%eax将错误码和1作比较,如果页不存在就调用_do_no_page ,否则调用_do_wp_page
