Linux 0.11的内存管理总结。许多其它内容参见“地址翻译”部分
1、分页相关的页目录表和页表
Linux 0.11中的内存管理是分页式的内存管理。程序中的逻辑地址,在经过地址翻译后会被转成线性地址。内存管理部分所接触的地址,基本上都以线性地址为主。
在分页式的内存管理中,将物理内存看成是一个个的连续的页组成。在Linux 0.11中,这个页的大小是4KB。因此,每个页的起始地址是4KB对齐的,也就是说在进行寻址内存页时,物理地址的低12位是没有帮助的。所以,这地址的低12位被用于其它特殊的作用。
页目录表中存放着页表所在页的物理起始地址,页表中存放着物理页的起始地址。简单来说,就是线性地址通过高10位,在页目录表中找到页表地址。然后使用页表和中间10位找到页的地址。最后使用页的地址和低12位找到字节地址。
页目录表在Linux 0.11中只有一个,它在物理地址为0的内存页上。所以,要计算页目录表中某一个项的地址就很容易,用这个项在页目录表中的索引乘以4即可。页目录项在页目录表中的索引由线性地址的高10位可以得到。如果这里有线性地址addr,那么计算这个线性地址在页目录表中的项的索引为addr>>22。最后,计算这个项的地址为(addr>>22)*4,也就是addr>>20。(在Linux 0.11中所有的进程共用一个页目录表,因为各个进程的线性地址之间没有重复)。
2、相关的变量数据
在memory.c中定义了与内存管理相关的变量和常量。
#define LOW_MEM 0x100000 进程可用的物理内存的最低地址,0x100000=1M。低于1M的物理地址,供系统使用
#define PAGING_MEMORY (15*1024*1024) 分页的内存大小,共15M
#define PAGING_PAGES (PAGING_MEMORY>>12) ,PAGING_PAGES内存分页之后全部的页数,总大小15M/每页大小4K
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12) 物理地址addr对应的内存页的编号。物理地址addr与分页编号nr之间的关系为addr=LOW_MEM+nr*4K
static unsigned char mem_map [ PAGING_PAGES ] = {0,}; mem_map数组用来标识内存页的使用情况。对于编号为nr的内存页,如果mem_map[nr]为0,则表示这个内存页未被占用。如果mem_map[nr]大于0,则表示这个内存页被占用,并且mem_map[nr]的值为这个内存页被引用的次数
static long HIGH_MEMORY = 0; HIGH_MEMORY表示可用物理内存地址的最大值
3、内存相关函数
内存相关的函数很多,这里随便说几个。
[内存初始化]
内存的初始化函数是mem.init。这个函数的工作量主要集中在mem_map上。调用时需要传递两个参数,由start_mem和end_mem指定内存的范围。将这个范围内的内存页在mem_map中都标记为未占用。未在这个范围以外的则设为默认值(#define USED 100)
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; //将内存页标记为未占用
}
mem_init在main.c中被调用,mem_init(main_memory_start,memory_end)。其中main_memory_start和memory_end指出了内存范围
[写时复制]
memory.c/un_wp_page是理解写时复制的核心。
在fork调用时,子进程会使用与父进程相同的内存页。这个是通过copy_page_tables来实现的,大致原理是让子进程的页表与父进程的页表中的数据一致。那么在进行读操作时,相同的逻辑地址,就会寻址到相同的物理页,这样就使得父子进程地址空间中数据保持一致,同时这样页避免了在复制进程的时候,复制大量的内存页。但当父进程或子进程其中一个先需要进行写操作时,就会为这个进程这个要进行写操作的地址配上一个新的页,并且将原有的数据复制过去,然后进行写操作。这样就将真正的数据复制工作进行了延迟,并且是根据需要进行复制。这就是写时复制的原理。
要理解这个原理的实现,主要在于un_wp_page函数的理解。un_wp_page函数需要一个页表项地址作为参数,取消这个页表项中存储的页的写保护机制,如果有必要,就让这个页表项保存一个新的页地址。
/*
取消页的写保护机制,使页变为可写的。
因为在fork新进程时,父子进程使用的是相同的物理页。先进行写操作的这个进程需要重新寻找新的页
如果这个页的引用数为1,则只有这一个进程在使用,直接将这个页设成可写的即可
如果这个页的应用数大于1,那就需要当前进程重新申请新的页(实现linux写时复制部分的代码),并且设置这个页表项的值
*/
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) { //如果这个页只有本进程自己使用
*table_entry |= 2; //使页变为可写
invalidate();
return;
}
/*
如果这个页在多个进程共享(父子进程,fork带来的效果)
那就需要当前进程(先进行写操作的进程)新申请一个内存页,使用这个新的内存页去写
*/
if (!(new_page=get_free_page()))
oom();
if (old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--;
*table_entry = new_page | 7; //设置本进程的页表
invalidate();
copy_page(old_page,new_page); //复制页的内容
}
[共享内存]
内存共享的作用即对于不同进程的同一个逻辑地址,使得对这个逻辑地址寻址找到同一个物理页。在内存管理中,与内存共享相关的函数是try_to_share。
/*
address不是一个线性地址,它是一个逻辑地址,即相对于代码段基地址的偏移地址
进程p指向一个非当前进程的进程
这个函数的作用就是,给定一个逻辑地址address
实现在进程p和当前进程对这个逻辑地址共享一个内存页
如果当前进程的这个逻辑地址address对应的页已经在内存中,那么就出错
或者进程p对应这个逻辑地址address的页不在内存中,返回0。共享成功返回1
*/
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;
/*
由于address是一个逻辑地址
所以在得到其页目录表项地址时需要相应地加上进程的代码段基地址start_code
from_page和to_page分别保存逻辑地址address在p指向的进程和当前进程中的页目录表项的地址
*/
from_page = to_page = ((address>>20) & 0xffc);
from_page += ((p->start_code>>20) & 0xffc);
to_page += ((current->start_code>>20) & 0xffc);
/* is there a page-directory at from? */
/*
from保存页表地址
如果页表不在内存中,直接返回
*/
from = *(unsigned long *) from_page;
if (!(from & 1))
return 0;
from &= 0xfffff000;
/*
获取页表项的地址,from_page保存页表项的地址
照理说应该是将address转成线性地址后,再获取页表项地址
不过from已经保存了页表的基地址,那么只需要获取页表项相对于页表的偏移值即可
在转成线性地址时,基地址(nr*64M)对于计算相对于页表的偏移值没有贡献
所以address>>10,然后取低12位即可得到相对于页表的偏移值
所以,这里from_page保存页表项的地址,phys_addr保存页的地址
*/
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); //to_page保存页表项地址
if (1 & *(unsigned long *) to_page)
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; //实现页的共享
invalidate();
//在mem_mep中将phys_addr页标记位已占用
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++;
return 1;
}
[释放内存页]
释放内存页要做的事情也很简单,在mem_map数组中,将对应的内存页的引用数减1。还会做一些异常检查
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=LOW_MEM+PAGE_NUM*4K
*/
addr >>= 12; //计算页号
if (mem_map[addr]--) return ;
mem_map[addr]=0;
panic( "trying to free free page" );
}
free_page主要在free_page_tables中使用。free_page_tables也是释放内存页,不过free_page_tables释放的是一个地址范围内的内存页。并且free_page_tables在释放的时候,会同时释放页表和内存页。通过free_page_tables可以很清晰地理解线性地址到物理地址之间的转换过程。这个函数需要2个参数,from为待释放的起始线性地址,size为释放的地址范围大小。
/*
释放由from指定的线性地址对应的页目录表中的项,释放的空间大小由size指定,不足一页时以一页计算
通过页目录表,找到页表,然后通过页表,释放页;然后释放页表所在的页
*/
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
if (from & 0x3fffff) //页应该是4KB对齐的,也就是物理地址的低22位应该都是0
panic("free_page_tables called with wrong alignment");
if (!from)
panic("Trying to free up swapper memory space");
size = (size + 0x3fffff) >> 22; //将释放的空间大小换成页数,不足一页以一页来计算
/*
在Linux 0.11中各个进程的线性地址空间是没有重复的,
这也是为什么在Linux 0.11中可以只使用一个全局页目录表的原因
那线性地址与页目录表之间的关系是什么呢?
线性地址的最高12位是对应页目录表中的索引,由于页目录表的物理起始地址是0
所以,这个索引值刚好也就是对应页目录表项的物理地址
(from>>20)&0xffc的结果也就是from线性地址对应的页目录表项的物理地址
*/
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
for ( ; size-->0 ; dir++) {
/*
页是4K对齐的,所以低12位对于寻址没什么帮助,另有用途
第0位是标志位,表示对应的页是否在内存中
*/
if (!(1 & *dir)) //如果对应的页不在内存中,那就不用释放
continue;
//页目录表象--->页表 获取页表的物理地址 也是4K对齐的,低12位对于寻址没帮助
pg_table = (unsigned long *) (0xfffff000 & *dir);
/*
释放页表中保存的物理页
一个页是4K,也就是4096字节,一个地址占据4字节。所以一个页的页表中能存储1024个物理页地址
*/
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table) //如果页在内存中存在
free_page(0xfffff000 & *pg_table); //*pg_table获取物理内存页地址(4K对齐)
*pg_table = 0;
pg_table++;
}
free_page(0xfffff000 & *dir); //释放页表本身
*dir = 0;
}
invalidate();
return 0;
}