内存管理
linux内核的内存管理采用了分页管理的方式,它利用页目录和页表结构处理内核中其他部分代码对内存的申请和释放操作。内存的管理是以内存页面为单位进行的,一个内存页面指的是地址连续的4K字节物理地址,通过页目录项和页表项,可以寻址和管理指定页面的使用情况
程序在寻址过程中使用的是由段和段内偏移构成的地址。该地址并不能直接用来寻址物理内存地址,因此被称为虚拟地址。为了寻址物理地址,需要将虚拟地址映射到物理地址
内存管理主要功能:地址变换机制+内存的寻址保护机制
虚拟地址通过段管理机制首先变成一种中间地址形式--CPU 32位的线性地址,然后使用分页管理机制将此线性地址映射到物理地址
分页机制
内存分页管理通过页目录表和内存页表组成的二级表实现
页目录表中的每个表项(页目录项,4字节)用来寻址一个页表,而每个页表项用来指定一个物理内存页
因此,通过一个页目录项和一个页表项,就可以确定所对应的物理内存页。
页目录表占用一页内存,因此最多可以寻址1024个页表,而每个页表也同样占用一页内存,因此一个页表可以寻址最多1024个物理内存页面
在32位CPU中,一个页目录表所寻址的所有页表项可以寻址102410244096=4G的内存空间
在linux0.12中,所有进程共同使用一个页目录表,而每个进程都有自己的页表
内核代码和数据段长度规定为16MB,使用4个页表(即4个页目录项),内核代码和数据段位于线性地址空间的头16MB范围内,经过分页机制变换直接被映射到16MB物理内存上。因此对内核段来讲,线性地址就是物理地址
线性地址的位 31-22 共 10 个比特用来确定页目录中的目录项,位 21-12 用来寻址页目录项指定的页表中的页表项,最后的 12 个比特正好用作页表项指定的一页物理内存中的偏移地址。
一个系统中可以同时存在多个页目录表,而在某个时刻只有一个页目录表可用。系统当前所使用的页目录表是用CPU的寄存器CR3来确定的,存储当前页目录表的物理内存地址
linux内核memory.c代码分析
内存释放
//释放已经分配的物理内存页面,并确保释放之前,该页面确实已经被分配
void free_page(unsigned long addr){
if(addr<LOW_MEM) return;
if(addr>HIGH_MEMORY)
panic("trying to free noneexistent page");
addr -= LOW_MEM; //将地址转换为内存基址的偏移量
addr>>=12; //地址右移12位得到页面索引。12是页面大小(4K),也就是将地址从字节转换为页数
if(mem_map[addr]--) return; //更新内存并释放页面
mem_map[addr] = 0;
panic("trying to free free pages")
}
//释放虚拟内存中的页表和页目录
int free_page_tables(unsigned long from, unsigned long size){
unsigned long *pg_tables;
unsigned long *dir, nr;
if(from & 0x3fffff) //确保地址from 2MB对齐
panic("free_pages called with wrong alignment");
if(!from) //确保from地址不为0,地址0通常被用作交换内存的占位符,不能释放
panic("Trying to free up swapper memory space");
size = (size+0x3fffff)>>22; //计算需要释放的页表和页目录的数量,每个页目录条目覆盖2MB
dir = (unsigned long*)((from>>20)&0xffc); //计算页目录基地址,右移20位获得页目录基地址。0xffc用于清楚页目录的偏移部分
//遍历并释放页表
for(;size-->0; dir++){
if(!(1&*dir)) //页目录存在,即*dir最低位位1
continue;
pg_tables = (unsigned long*)(0xfffff000 & *dir); //转换为页表地址
for(nr=0; nr<1024;nr++){ //遍历页表,释放每个页表项引用的页面
if(1&pg_tables)
free_page(0xfffff000 & *pg_tables);
*pg_tables = 0;
pg_tables++;
}
free_page(0xfffff000 & *dir); //将页表项清零
*dir = 0; //释放页目录所引用的页表页面,将其清零
}
invalidate(); //刷新TLB,确保系统使用最新的页表映射
}
//刷新页表缓存: CR3 寄存器的内容指向当前页目录的物理地址。通过将 CR3 寄存器设置为新的地址,CPU 会重新加载页表映射。这个操作会使 CPU 更新其内部的地址映射,从而使新的页表映射生效。
//刷新CPU的页表缓存,将新的页目录基地址加载到CR3,从而使CPU更新其页表映射
#define invalidate() __asm__("movl %%eax, %%cr3"::"a"(0))
内存分配
//在内存中寻找一个可用页面并返回其物理地址
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
//std:设置方向为1,字符串操作指令地址从高地址到低地址移动
//repne;scasw: 重复执行后面的操作直到条件满足,scaw是字符串比较指令,比较edi寄存器指向的地址中的字与ax寄存器中的值
__asm__("std ; repne ; scasw\n\t"
//在内存中查找一个特定的模式,scasw将查找0x0001,即标记为已分配的页面
"jne 1f\n\t" #如果scasw找到的不匹配0x0001,则跳转到标签1,这个标签是内存中可用页面的标志
"movw $1,2(%%edi)\n\t" #将1存储到edi寄存器所指向的地址的偏移2位置,标记该页为已分配
"sall $12,%%ecx\n\t" #将ecx寄存器左移12位,计算页的地址
"movl %%ecx,%%edx\n\t"
"addl %2,%%edx\n\t" #将LOW_MEM添加到edx,将其设置为物理地址的基地址
"movl $1024,%%ecx\n\t" #设置ecx寄存器的大小,通常是页大小
"leal 4092(%%edx),%%edi\n\t" #计算edx地址加上4092,将结果存储到edi
"rep ; stosl\n\t" #重复执行stols指令,将eax寄存器的值存储到edi寄存器指向的地址中
"movl %%edx,%%eax\n" #将edx寄存器的地址(物理地址)移动到eax,这也是最终返回的值
"1:" #跳转的目的地
:"=a" (__res) #输出操作数,将结果存储到eax寄存器,即__res变量
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES), #“0”:输入操作数,0赋值给eax寄存器。“i”:立即数操作数,用于设置内存地址,“c”:输入操作数,用于设置edi寄存器的值
"D" (mem_map+PAGING_PAGES-1) #“D”:输入操作数,用于设置edi寄存器的值
:"di","cx","dx"); #指定edi,ecx,edx寄存器被内联汇编代码修改
return __res;
}
//将虚拟地址空间的页表内容复制到另一个地址空间
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;
if((from&0x3fffff) || (to&0x3fffff))//对齐检查
panic("copy_page_table 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) //若目标页目录已存在
panic("copy page table: alreay exist");
if(!(1&*from_dir)) //如果源目录项不存在
continue;
from_page_table = (unsigned long *)(0xfffff000&*from_dir); //获取源页表地址
if(!(to_page_table = (unsigned long *)get_free_page())) //为目标页表分配一个新页面
return -1;
*to_dir = ((unsigned long)to_page_table)|7; //将页表地址写入目标页目录项,并设置标志位为7,表示页表存在且可读写
nr = (from==0)?0xA0:1024; //与系统页表的结构有关
for(;nr-->0; from_page_table++, to_page_table++){ //遍历源页表项,并将有效的页表项目录复制到目标页表中
this_page = *from_page_table;
if(!(1&this_page))
continue;
this_page &= ~2; //清除标志位
*to_page_table = this_page;
if(this_page>LOW_MEM){ //更新mem_map中的引用计数,表明页面被使用
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >> 12;
mem_map[this_page]++;
}
}
}
invalidate(); //刷新TLB
return 0;
}
物理内存和虚拟内存的映射
//put a page in memory at the wanted address
//return physical address of the page gotten ,0 if out of memory
//物理页面映射到虚拟地址空间
unsigned long put_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 disagreee eith %d at %p\n", page, 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; //将新分配的页表地址写入页目录项,并设置标志7
page_table = (unsigned long*)tmp; //将page_table设为新分配的页表的地址
}
page_table[(address>>12) & 0x3ff] = page|7; //更新页表
return page;
}
内存写时复制
//复制物理页面内容
#define copy_page(from, to) __asm__("cld"; "movsl"::"S" (from), "D" (to), "c"(1024):"cx", "di", "si")
static unsigned short mem_map[PAGING_PAGES] = {0,};
//处理写时复制机制中的页面错误
//将写时复制的页面处理成可写的页面,通常在发生页面缺页异常时调用
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){ //检查旧页面状态,MAP_NR(old_page)将物理页面地址转换为mem_map中的索引
*table_entry |= 2; //以上条件满足,说明页面未被共享,页表项的只读标志位清除,使页面变为可写
return;
}
//旧页面不能直接修改,需要分配一个新的物理页面
if(!(new_page=get_free_page()))
do_exit(SIGSEGV);
if(old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--; //更新页面映射
*table_entry = new_page | 7; //更新页表项,将新页面的地址和标志位7(表示页面可写)写入*table_entry
copy_page(old_page, new_page); //旧页面内容复制到新页面
}
//处理写时复制页面错误,在内存管理中,当试图修改一个标记为只读的页面时,会触发一个页面错误异常
do_wp_page函数用于处理这个异常,将只读页面转换为可写的
void do_wp_page(unsigned long error_code,unsigned long address)
{
un_wp_page((unsigned long *)
(((address>>10) & 0xffc) + (0xfffff000 &
*((unsigned long *) ((address>>20) &0xffc)))));
}
//在写操作前验证和处理页表项,以确保写时复制(COW)机制中的页面能够正确处理。
//确保在写操作时,写时复制机制能够正确处理页面,使其变为可写,从而避免数据损坏或系统异常。
void write_verify(unsigned long address)
{
unsigned long page;
if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
return;
page &= 0xfffff000;
page += ((address>>10) & 0xffc);
if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present */
un_wp_page((unsigned long *) page);
return;
}
内存缺页处理
//处理缺页异常,访问一个不存在的页面时触发的异常
void do_no_page(unsigned long error_code, unsigned long address){
unsigned long tmp;
if(tmp=get_free_page())
if(put_page(tmp, address))
return;
do_exit(SIGSEGV);
}
查看内存使用情况
//计算和打印当前内存使用情况
void calc_mem(void){
int i,j,k,free=0;
long *pg_tbl;
for(i=0;i<PAGING_PAGES;i++)
if(!mem_map[i]) free++;
printk("%d pages free (of %d)\n\r",free,PAGING_PAGES);
for(i=2; i<1024;i++){
if(l&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);
}
}
}
可以看到,Linux内存管理,主要是通过维护页表来管理内存。通过一个静态全局变量mem_map数组,来记录内存的使用情况
page_fault
_page_fault 函数是一个用于处理页面错误的中断处理程序。其主要功能是:
- 保存和设置处理页面错误所需的寄存器值。
- 读取并保存触发页面错误的虚拟地址。
- 根据错误代码决定处理方式:如果是写时复制错误,调用 _do_wp_page;否则调用 _do_no_page。
- 恢复寄存器和堆栈状态,然后返回到异常发生的代码位置。
- 这样处理页面错误可以确保系统正确地响应内存访问错误,并根据需要分配和映射页面。
ref: 《linux内核完全注释》