内存管理篇:Part2-memory.c
0 写在前面
内存管理的核心代码——memory.c,主要实现了页异常处理的两个函数(do_no_page:缺页异常,do_wp_page:写保护异常(实现写时复制))
这里不按 memory.c 的代码顺序讲解,而是按功能划分为两部分
1)页异常处理函数
do_wp_page: 写保护页面处理函数——实现了“写时复制”
触发时机:fork后父子进程其一的写页面操作
do_no_page:缺页处理函数——实现了“需求加载(Load on demand)“
触发时机:从入口地址开始执行时,或执行过程中遇到未加载的代码或数据
2)页管理函数——显示、验证、复制、释放、表项设置
包括:以”页表对应的物理内存页(4MB)“为单位的复制和释放,即:copy_page_tables() 和 free_page_tables()
此外,还有对页表项的设置(put_dirty_page)、写权限验证(write_verify)和内存页的显示(show_mem())等
1 页异常处理函数
1.1 do_wp_page——“写时复制”机制
1.1.0 do_wp_page函数的主要功能
主要功能:执行写保护页面处理。参数address和error_code是CPU产生异常而自动产生的,在page.s程序中根据压入栈中的错误码和引起异常的地址被调用
当页面是共享的只读页面时,需复制页面("写时复制"),而当页面没有共享时,只需改变页表项的权限为可读写。
1.1.1 do_wp_page函数的流程图
1.1.2 do_wp_page及其调用的函数代码与注释
/* 写保护处理函数:wp<--->write-protect */
void do_wp_page(unsigned long error_code, unsigned long address)
{
if(address < TASK_SIZE) // 造成异常的线性地址在内核或任务0和1的线性地址范围内
printk("\n\rBAD! KERNEL MEMORY WP-ERR!\n\r");
if(address - current->start_code > TASK_SIZE){ //访问其它进程的线性地址空间
printk("Bad things happen: page error in do_wp_page\n\r");
do_exit(SIGSEGV); // 段错误而退出,不是讨论的重点,不跟进去了
}
un_wp_page( (unsigned long *) (((address >> 10) & 0xffc) + // 页表项的偏移地址
(0xfffff000 & *((unsigned long*) ((addresss>>20) & 0xffc)))) ); // 页目录项的页表地址
}
/*
根据线性地址address计算其页表项地址:
首先要明确32线性地址的三部分:页目录项的索引(31-22) + 页表项的索引(21-12) + 页内偏移地址(11-0)
1)求出页表项在页表中的偏移地址: 因为 address>>12 就是页表项的索引,但每项占4字节,故 x4 后,
(address>>12)<<2 = address>>10,即为页表项在页表中的偏移地址。又因为只移动了10位,最后2位是
线性地址低12位的最高2位,应该屏蔽掉。因此 (address>>10) & 0x3ff
2) 求页目录项中页表的地址值:首先取得页目录项的索引值(address>>22),但每项4字节,同理可得,页目录项在页目录中
的偏移地址为: (address>>22)<<2 = address>>20。再根据页目录项结构的划分:31-12是对应页表的地址值。
因此(0xfffff000 & (address>>20) )即为 页目录项中页表的地址值
最后: 1) + 2) = 页表项地址值
总结:求解的前提是理解分页机制中32线性地址的划分,以及页目录/页表项的结构划分
*/
/* 取消写保护页面函数,用于页异常中断过程中写保护异常的处理(写时复制) */
void un_wp_page(unsigned long* table_entry)
{
unsigned long old_page, new_page;
old_page = 0xfffff000 & *table_entry;
if(old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1){ //>1M 且 没有页面共享
*table_entry |= 2; // 设置页表项为可写
invalidate(); // 刷新页变换高速缓冲
return;
}
if(!(new_page=get_free_page()))
oom(); // out of memory 内存不够处理
if(old_page >= LOW_MEM) // 有页面共享,说明是父子进程,需“写时复制”
mem_map[MAP_NR(old_page)]--;
copy_page(old_page,new_page); // 页面复制
*table_entry = new_page | 7; // 打破页面只读,更改为User、页面可读写
invalidate();
}
#define copy_page(from, to)
__asm__("cld; rep; movsl"::"S"(from),"D"(to),"c"(1024):"cx","di","si");
// 输入:esi(from) edi(to) ecx(1024)
// movsl 从%esi复制一个长字(4 bytes)到%edi,并使得esi、edi分别+4
// rep 修饰movsl,即重复执行 %ecx(1024)次movsl,共移动 4KB
// invalidate在include/linux/mm.h
#define invalidate()
__asm__("mov %%eax, %%cr3"::"a"(0)); //即清零CR3
1.2 do_no_page——“需求加载”机制
1.2.0 do_no_page函数的主要功能
该函数首先查看所缺页是否在交换设备中,若是则交换进来,否则尝试与已加载的相同文件进行页面共享
或者只是由于进程动态申请内存页面而只需映射一页物理内存页即可。若共享操作不成功,那么只能从
相应文件中读入所缺的数据页面到指定线性地址处。
1.2.1 do_no_page函数的流程图
1.2.2 do_no_page及其调用的函数代码与注释
void do_no_page(unsigned long error_code, unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
struct m_inode* inode;
if(address < TASK_SIZE) //引起异常的地址在内核或任务0与1之间的线性地址范围内
printk("\n\rBAD!! KERNEL PAGE MISSING\n\r");
if(address - current->start_code > TASK_SIZE){ // 访问不是当前进程的线性地址空间
printk("Bad things happen: nonexistent page error in no_page\n\r");
do_exit(SIGSEGV); // 段错误退出
}
page = *(unsigned long*)((address >> 20) & 0xffc); // 取目录项内容
if(1 & page){ //目录项的P =1
page &= 0xfffff000; // 取二级页表地址
page += (address >> 10) & 0xffc; // 页表项指针
tmp = *(unsigned long*)page; // 页表项内容
if(tmp && !(1 & tmp)){ // 如果页表项的P=1,则说明所缺页在交换设备中
swap_in((unsigned long*)page); // 从交换设备读入页面
return;
}
}
address &= 0xfffff000; // address所缺页的物理页面地址
tmp = address - current->start_code; // 缺页页面对应的逻辑地址(相对进程基址的偏移长度值)
if(tmp >= LIBRARY_OFFSET){ // 所缺页在库文件中
inode = current->library;
block = 1 + (tmp-LIBRARY_OFFSET);
}
else if(tmp < current->end_data){ // 所缺页在当前执行文件中
inode = current->executable;
block = 1 + (tmp / BLOCKSIZE);
}else{ // 否则,所缺页是动态申请的数据或栈内存页面
inode = NULL;
block = 0;
}
if(!inode){ //若是动态申请引起的,则申请一空闲页
get_empty_page(address);
return;
}
if(share_page(inode, tmp)) //否则缺页在库文件/执行文件中,尝试共享页面
return; // 共享成功就功成身退了
if(!(page = get_free_page())) // 共享失败,只能申请一页空闲页
oom();
for(i=0; i < 4; block++,i++)
nr[i] = bmap(inode, block);
bread_page(page, inode->i_dev, nr); // 只能从执行文件中读入
i = tmp + 4096 - current->end_data; //从块设备读入时,会遇到执行文件末尾不到1页的情况
if(i > 4095) // 距离文件末尾大于1页,则i超出末尾部分为0
i = 0;
tmp = page + 4096; // 否则,tmp 执行申请页面的尾部
while(i-- > 0){ // 进行超出末尾部分清零工作
tmp--;
*(char *)tmp = 0;
}
if(put_page(page, address)) // 最后,把申请的页面映射到引起缺页的地址address
return;
free_page(page); // 操作失败,则释放申请的页,显示内存不够退出
oom();
}
void get_empty_page(unsigned long address)
{
unsigned long tmp;
if(!(tmp=get_free_page()) || !put_page(tmp,address)){ // get_free_page在swap.c中
free_page(tmp);
oom();
}
}
static unsigned long put_page(unsigned long page, unsigned long address)
{
unsigned long tmp, *page_table;
if(page < LOW_MEM || page > HIGH_MEMORY) // 判断给定物理页面page的有效性
printk("Trying to put page %p at %p\n",page, address);
if(mem_map[(page - LOW_MEM) >> 20] != 1) // 如果该page页面每申请,则发出警告
printk("mem_map disagrees with %p at %p\n",page, address);
page_table = (unsigned long*)((address >> 20) & 0xffc); //获取页目录项的页表地址
if( (*page_table) & 1) //如果页表项的P=1,则获取对应物理页面地址
page_table = (unsigned long*)(0xfffff000 & *page_table);
else { // 否则,P=0,申请一页空闲页并修改页表项P=1,可读写,User
if(! (tmp = get_free_page()))
return 0;
*page_table = tmp | 7;
page_table = (unsigned long*)tmp;
}
return page_table; //最后,返回物理页面地址
}
void free_page(unsigned long addr)
{
if(addr < LOW_MEM) return;
if(addr >= HIGH_MEMORY)
panic("trying to free nonexistent page");
addr -= LOW_MEM;
addr >>= 12; // 获取addr对应的页面号,因为一页大小2^(12)=4K
if(mem_map[addr]--) return; // 释放只需mem_map[]对应占用值-1即可
mem_map[addr] = 0; //注意,要清零,说明尝试释放空页,要警告后退出程序
panic("trying to free free page");
}
/*
bread_page在buffer.c中,用于从指定块设备读取一页大小内容到物理页面
*/
static int share_page(struct m_inode * inode, unsigned long address)
{
struct task_struct **p;
if(inode->i_count < 2 || !inode) //如果该节点只有一个进程在运行该执行文件,则共享失败,退出
return 0;
for(p = &LAST_TASK; p > &FIRST_TASK; --p){ //从最后一个进程/任务遍历共享该可执行文件的进程
if(!*p) //若是空进程,跳过
continue;
if(current == *p) //若是当前进程,也跳过
continue;
if(address < LIBRARY_OFFSET){ // 若共享页面在执行文件中
if(inode != (*p)->executable)
continue;
}else{ // 否则共享页面在库文件中
if(inode != (*p)->library)
continue;
}
if(try_to_share(address, *p)) // 尝试共享页面
return 1;
}
return 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); //当前进程目录项
from = *(unsigned long*) from_page; // p进程目录项内容
if(!(from & 1))
return;
from &= 0xfffff000; // p进程页表地址
from_page = from + ((address >> 10) & 0xffc); // p进程页表项地址
phys_addr = *(unsignedlong*)from page; // p进程页表项内容
if((phys_addr & 0x41) != 0x01) //如果页面不干净,则退出返回
return 0;
phys_addr &= 0xfffff000; // p进程物理页面地址
if(phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
return 0;
to = *(unsigen 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 & *(unsiged long*)to_page) // 如果当前进程物理页P=1已存在
panic("try_to_shre: to_page already exists");
// 对他们进行共享处理:写保护
*(unsigned long*)from_page &= ~2;
*(unsigned long*)to_page = *(unsigned long*)from_page;
invalidate();
// 最后,对应页面的mem_map值+1即可
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++;
return 0;
}
小结:
截至目前,介绍了memory.c的函数有:
1.do_wp_page
un_wp_page、copy_page、invalidate
2.do_no_page
try_to_share 、share_page、free_page、put_page、get_empty_page
共10个函数,下面介绍管理页面有关的函数
2 页面管理函数
2.1 以"页表对应物理页(共4M)"为单位的拷贝和释放
int free_page_tables(unsigned long from, unsigned long size)
{
unsigned long* pg_table;
unsigned long* dir, nr;
if(from & 0x3fffff) //复制的起始地址必须在4MB边界上
panic("free_page_tables called with wrong alignment");
if(!from) // 禁止释放内核和缓冲所占的空间
panic("Trying to free up swapper memory space");
size = (size + 0x3fffff) >> 22; // 计算页目录项数,即页表数(2^(22)=4MB, + 0x3fffff是为了得到进位整数倍结果)
dir = (unsigned long*)((from>>20) & 0xffc); //计算给出的线性基地址对应的起始目录项
for(; size-- > 0; dir++){
if(!( 1 & *dir)) // 页目录项的P=0时,则跳过
continue;
pg_table = (unsigned long*)(0xfffff000 & *dir); //获取页目录项中页表地址
for(nr = 0; nr < 1024; nr++){
if(*pg_table){ //所指页表项内容不为0,则该项有效
if(1 & *pg_table) //如果页表项的P=1,即页面存在,释放
free_page(0xfffff000 & *pa_table);
else // 否则,释放交换设备中对应的页
swap_free(*pg_table >> 1);
*pg_table = 0; // 该页表项内容清零
}
pg_table++; //指向页表中下一项
}
free_page(0xfffff000 & *dir); // 释放页表所占内存页面
*dir = 0; // 对应页表的页目录项清零
}
invalidate();
return 0;
}
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 new_page;
unsigned long nr;
if((from & 0x3fffff) || (to & 0x3fffff)) //源和目的地址必须在4MB边界上
panic("copy_page_tables called with wrong alignment");
from_dir = (unsigned long*)((from>>20) & 0xffc);
to_dir = (unsigned long*)((to>>20) & 0xffc);
size = ((unsigned)(size + 0x3fffff)) >> 22; // 需复制的页目录项(页表)数
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 = (unsigned long*)(0xfffff000 & *from_dir); //源目录项中的页表地址
if(!(to_page_table = (unsigned long*)get_free_page())) //为保存目的目录项对应的页表,需在主存区申请1页空闲页
return -1;
*to_dir = ((unsigned long)to_page_table) | 7;
nr = (from==0)?0xA0:1024; //设置需复制的页面项数
for(; nr-- > 0; from_page_table++,to_page_table++){
this_page = *from_page_table;
if(!this_page) //如果需复制的源页表项对应的物理页地址为空,则跳过,不需复制
continue;
if(!(1 & this_page)){ // 源物理页面不存在,则申请一空闲页
if(!(new_page = get_free_page()))
return -1;
read_swap_page(this_page>>1, (char*)new_page);
*to_page_table = this_page;
*from_page_table = new_page | (PAGE_DIRTY | 7);
continue;
}
this_page &= ~2;
*to_page_table = this_page;
if(this_page > LOW_MEM){
*from_page_table = this_page; //令源页表项也只读
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}
2.2 写页面验证、设置页面已修改两个函数
/* 写页面验证函数——在fork.c的内存验证通用函数verify_area()调用 */
void write_verify(unsigned long address)
{
unsigned long page;
// 若指定线性地址对应的页目录项的P=0,则退出
if(!(page = *((unsigned long*)((address >> 20) & 0xffc))) &1) )
return;
page &= 0xfffff000; //获取页目录项中页表地址
page += ((address>>10) & 0xffc) // 获取页表项地址
if((3 & *(unsigned long*)page) == 1) //如果P=1,且不可写,则执行共享检验和复制页面操作(写时复制)
up_wp_page((unsigend long*)page); // 否则,什么也不做,退出
return;
}
/* 设置页面已修改,在exec.c程序需要这种设置,因为exec.c中函数会在放置页面之前修改过页面内容 */
unsigned long put_dirty_page(unsigned long page, unsigned long address)
{
unsigned long tmp, *page_table;
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);
page_table = (unsigned long*)((address >> 20) & 0xffc);
if((*page_table) & 1) // 如果页表项的P=1,即页面存在
page_table=(unsigned long*)(0xfffff000 & *page_table); // 取得物理页表的地址
else{ // 若物理页面不存在,则申请一页,并修改表项内容为User 可读写,P=1
if(!(tmp =get_free_page()))
return 0;
*page_table = tmp | 7;
page_table = (unsigned long*)tmp;
}
page_table[(address>>12) & 0x3ff] = page | (PAGE_DIRTY | 7); // 最后设置D=1(页面已修改),User,可读写,P=1
return page;
}
2.3 显示系统内存信息函数
void show_mem(void)
{
int i,j,k,free=0,total=0;
int shared=0;
unsigned long * pg_tbl;
printk("Mem-ifno:\n\r");
// 下面统计主内存区页面总数total、空闲页面数free、被共享页面数shared
for(i=0; i < PAGING_PAGES; i++){
if(mem_map[i] == USED)
continue;
total++;
if(!mem_map[i])
free++;
else
shared += mem_map[i] - 1;
}
printk("%d free pages of %d\n\r",free, total);
printk("%d pages shared\n\r",shared);
k = 0; // k:一个进程占用页面统计值
for(i=4; i<1024; ){ // 从第5个目录项开始遍历目录表(前4个是内核的,不显示)
if(1 & pg_dir[i]){
if(pg_dir[i] > HIGH_MEMORY){
printk("page directory[%d]: %08X\n\r",i, pg_dir[i]);
continue;
}
if(pg_dir[i] > LOW_MEM)
free++,k++;
pg_tb1=(unsigned long*)(0xfffff000 & pg_dir[i]);
for(j=0; j<1024; j++) // 遍历页表每项
// 若物理页面地址大于最高物理内存地址,则说明页表项有问题,并显示出来
if((pg_tb1[j]>HIGH_MEMORY))
printk("page_dir[%d][%d]: %08X\n\r",i, j, pg_tb1[j]);
else // 否则,纳入占用页面数
k++,free++;
}
// 因每个进程线性空间长度是64MB,所以一个任务占用16个目录项。因此这里每统计了16个目录项,就把
// 进程的任务结构占用的页表统计进来。若此时k=0;则表示当前的16个页目录所对应的进程在系统中不存在
// (没有创建或已经终止)。在显示了对应进程号和其占用的物理内存页统计值k后,将k清零,以用于统计下一个
// 进程占用的内存页面数
i++;
if(!(i&15) && k){
k++, free++;
printk("Process %d: %d pages\n\r",(i>>4)-1, k);
}
}
//最后显示系统中正在使用的内存页面和主内存区中总的内存页面数
printk("Memory found: %d (%d)\n\r",free-shared, total);
}
3 总结与下篇预告
1.“三大机制”的实现:
分页机制——体现在: 线性地址——>物理地址, 物理地址——>线性地址 的转换上
写时复制机制——体现在父子进程共享只读页面进行写页面操作时,复制一份完全一样的页面,且恢复为可读写
需求加载机制——体现在从程序入口地址开始执行或执行过程中遇到为加载到内存的数据或代码触发的缺页异常
除此之外,内存管理还提供了页的复制、释放,页表项的设置等管理页面的函数。
2.数据结构与算法
数据结构:memory.c的mem_map[] ——字节“数组” 和 swap.c的 swap_bitmap ——相当于“字符数组”
算法:都采用了“位图映射技术”,mem_map字节数组每个元素映射一个页面,其元素值表名对应页面的占用情况
(空闲/共享/独享),并以此分配管理页面。而swap.c 的swap_bitmap是交换页面管理所使用的“位映射图”,
每位0/1表名是否驻留在交换设备上,由此可实现页面的换入/换出,从而实现“虚拟内存交换”功能
3. 下篇预告: bocsh下验证调试内存管理的三大机制...
参考资料
《Linux内核完全剖析——基于.12内核》第13章