Linux-0.11 memory.c详解
Linux-0.11 memory.c详解
模块简介
memory.c负责内存分页机制的管理。其中un_wp_page,copy_page_tables, do_no_page等函数较为重要。
在Linux-0.11中,内存区域划分如下图所示:
在Linux-0.11内核中,所有进程都使用一个页目录表,而每个进程都有自己的页表。 这与最新的操作系统是不一样的,最新的操作系统通常是每个进程有一套独立的页表。
在Linux-0.11中, 页表为两级结构, 第一级是页目录表, 第二级是页表。页目录表和页表中的每一项为4个字节(32位),页目录表和页表的格式如下所示:
图中页表项的第0位P代表存在位,该位表示这个页表项是否可以用于地址转换,P=1代表该项可用, 当目录表项或者第二级表项的P=0时,代表该项是无效的,不能用于地址转换。
当访问的内存页P=0时,处理器会发出缺页异常的信号, 缺页中断的异常处理程序就可以将所请求的页面加入到物理内存中。
页表项中的第1位R/W表示内存的读写权限,当它被设置为0时,说明该页面只能读而不能写,如果设置为1,说明可读可写。copy-on-write(写时复制将利用到这一点)。
函数详解
get_free_page
unsigned long get_free_page(void)
该函数的作用是获取一个空闲页面,从内存的高地址向低地址开始搜索。 该函数仅是在mem_map寻找为0的位置,还没有建立线性地址和物理地址的映射关系。映射关系是在get_empty_page中调用put_page建立的,在下面的函数中会提到。
该函数使用的是c语言内嵌汇编的方式实现,输入参数为 $1: ax = 0,$2: LOW_MEM, $3: cx = PAGING_PAGES, $4 edi = mem_map+PAGING_PAGES-1
这里将edi的值指向了mem_map数组的尾,如下图所示:
查找的过程就是从mem_map的尾部向前搜寻为0的值,0值代表物理内存是空闲的。
free_page
void free_page(unsigned long addr);
该函数的作用就是释放某个物理地址对应的内存页。 这里的入参addr指的是物理地址。
该函数首先对地址addr进行校验, 如果addr小于LOW_MEM,就会直接返回。如果addr大于HIGH_MEMORY,将会抛出一个内核错误。
if (addr < LOW_MEM) return;
if (addr >= HIGH_MEMORY)
panic("trying to free nonexistent page");
接下来就根据addr计算出在mem_map中的下标,并将该地址的使用次数减去1。如果该地址的使用次数为0,却尝试去释放,那么也会抛出内核错误。
addr -= LOW_MEM;
addr >>= 12;
if (mem_map[addr]--) return;
mem_map[addr]=0;
panic("trying to free free page");
free_page_tables
int free_page_tables(unsigned long from,unsigned long size)
该函数用于释放起始位置为from,长度为size的线性地址所对应的物理地址。
入参中的from代表的是线性地址。
首先检查参数合法性,检查from参数,其值是否是 4MB、8MB、12MB、16MB,即4M的倍数。一个页表项可以管理4M的内存,因此这里检查from是否是4M的倍数。
同时还检查其是否是0,如果是0则出错,说明试图释放内核和缓冲所在空间。
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的内存空间占据多少个页目录项。
一个页目录项可以管理4M的内存,因此移位>>22
可以计算size占用多少个4M,而其中加上0x3fffff是采用了进1法计算占用空间。
例如size = 4M + 1byte(0x400001)
,
那么(size + 0x3fffff) >> 22 = (0x400001 + 0x3fffff) >> 22 = 2
。
size = (size + 0x3fffff) >> 22;
接下来的代码用于计算from所在的页目录项的地址。
其中,(from>>20) & 0xffc
等同于 (from >> 22) << 2
。
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
得到页目录项的起始地址dir和占据的页目录项个数size后,就开始遍历,依次进行释放。
for ( ; size-->0 ; dir++) {//遍历dir
if (!(1 & *dir))//判断存在位是否为0
continue;
pg_table = (unsigned long *) (0xfffff000 & *dir);//取出页表的地址
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table)//判断存在位是否为1
free_page(0xfffff000 & *pg_table);//释放该页表对应的内存
*pg_table = 0;//将该页表项清零
pg_table++;//指向下一个页表项
}
free_page(0xfffff000 & *dir);//释放该内存
*dir = 0;//页目录表清零
}
整个过程如下图所示:
copy_page_tables
int copy_page_tables(unsigned long from,unsigned long to,long size)
该函数的作用是进行页表的复制。
这里首先定义了一些参数,并校验了from和to是否是4M的倍数。
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_tables called with wrong alignment");
下面的代码是不是很熟悉? 在上面的free_page_tables中就有提到过,其作用是取出from和to所在的页目录项的起始地址。
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
这里是计算大小为size的内存空间占据多少个页目录项。
size = ((unsigned) (size+0x3fffff)) >> 22;
接下来就是遍历from所在的页目录项, 依次拷贝其页表项内容。
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)//如果目的页目录表存在位为0, 则代表目的页目录表已经存在,这将是致命的错误
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))//如果源页目录表存在位为0, 则代表该页目录表可以不用复制
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);//取出源地址的页表起始的位置
if (!(to_page_table = (unsigned long *) get_free_page()))//申请一个4k内存页面,作为目的地址的页表
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;//将存在位设置为1,将R/W设置为1,代表可写,将U/S为设置为1,代表为用户态内存
nr = (from==0)?0xA0:1024;
下面这里依次拷贝页表中的每一项。 拷贝页表项的过程中,会将页表项的存在位(R/W位)设置位0,当后面修改到该段内存时,将会触发写时复制。
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;//存在位设置位0,后面触发写时复制。
*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]++;
}
}
整个拷贝过程如下图所示:
拷贝结束后的结果如下图所示,实现了from和to对应的线性地址指向了相同的物理地址
put_page
unsigned long put_page(unsigned long page,unsigned long address)
该函数的作用是将物理内存页映射到线性地址中。
程序的开始是对一些边界值做一些校验。如果低于LOW_MEM或者高于HIGH_MEMORY, 则打印告警信息。
unsigned long tmp, *page_table;
/* NOTE !!! This uses the fact that _pg_dir=0 */
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);
首先取出addr所在的页目录表,这里再看到复杂的移位和与操作就不再陌生了,在上面的函数中已经多次见到。
page_table = (unsigned long *) ((address>>20) & 0xffc);
如果页目录项的存在位为1, 也页表已经存在, 那么取出页表的地址。 如果存在为0, 即页表不存在,那么就新建页表,让页目录表指向该页表。
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中,下面要做的就是从页表中建立线性地址和物理页的映射。
(address>>12) & 0x3ff
的作用是取出中间的是个bit位,其代表在页表中的序号。
page_table[(address>>12) & 0x3ff] = page | 7;//设置页表项的值,同时设置存在位,R/W位和U/S位。
un_wp_page
void un_wp_page(unsigned long * table_entry)
该函数的作用是取消写保护异常,用于页异常中断过程中写保护异常的处理(写时复制)
入参table_entry指的是页表项的地址。 *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;
}
接下来申请一个新的内存页面用于复制。
if (!(new_page=get_free_page()))
oom();
接下来将页表项指向新页面,并进行内容的拷贝。
*table_entry = new_page | 7;
invalidate();
copy_page(old_page,new_page);
写时复制的过程如下图所示:
do_wp_page
void do_wp_page(unsigned long error_code,unsigned long address)
该函数的内部调用un_wp_page进行写保护异常的处理。
((address>>10) & 0xffc)
是页表中的偏移量。
(0xfffff000 & *((unsigned long *) ((address>>20) &0xffc)))
是页表的起始位置。
两者相加就可以找到线性地址address的页表项。
write_verify
void write_verify(unsigned long address)
该函数的作用就是在程序进行内存写操作的时候,判断是否可写,不可写则复制页面。
如果页目录项不存在,则直接返回。
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);
get_empty_page
void get_empty_page(unsigned long address)
该函数容易与get_free_page混淆。
get_empty_page内部调用get_free_page申请内存页,并调用put_page建立页表项到物理地址的映射。
代码相对简单, 就是先调用get_free_page申请物理内存,再调用put_page建立页表映射关系。
unsigned long tmp;
if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
free_page(tmp); /* 0 is ok - ignored */
oom();
}
try_to_share
static int try_to_share(unsigned long address, struct task_struct * p)
这里的入参address指的是逻辑地址(偏移量)。
由于address是偏移量,因此在计算address对应的页目录地址时,需要加上进程的起始地址start_code。
from_page = to_page = ((address>>20) & 0xffc);
from_page += ((p->start_code>>20) & 0xffc);
to_page += ((current->start_code>>20) & 0xffc);
如果from对应的页目录表的存在位为0,说明无法共享,直接返回。
from = *(unsigned long *) from_page;
if (!(from & 1))
return 0;
下面这里根据页目录表from_page计算得到了物理地址phys_addr。
from &= 0xfffff000;
from_page = from + ((address>>10) & 0xffc);
phys_addr = *(unsigned long *) from_page;
接下来物理地址帧进行校验,校验其存在和脏位。
if ((phys_addr & 0x41) != 0x01)
return 0;
phys_addr &= 0xfffff000;
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
return 0;
让to_page的页表项指向相同的物理地址。
*(unsigned long *) from_page &= ~2;
*(unsigned long *) to_page = *(unsigned long *) from_page;
将对应的物理页的使用次数增加1。
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++;
share_page
static int share_page(unsigned long address)
当发生缺页异常时,该函数将尝试从其他进程中共享内存页面。因为系统中可能会有多个进程执行相同的可执行文件。
首先进行一些边界判断,如果进程的executable inode为0,则返回。 如果executable inode的icount<2, 说明没有执行相同可执行文件的其他进程,直接返回。
struct task_struct ** p;
if (!current->executable)
return 0;
if (current->executable->i_count < 2)
return 0;
接下来就是对系统中任务数组进行遍历, 查找与当前进程的executable inode相同的项目, 并调用try_to_share进行共享。
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;
}
do_no_page
void do_no_page(unsigned long error_code,unsigned long address)
该函数的作用是执行缺页处理。
入参中的address是线性地址。
将线性地址减去进程在进程空间的首地址,即可得到逻辑地址(偏移量)。
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
address &= 0xfffff000;//取出address的页起始地址
tmp = address - current->start_code;//得到address的逻辑地址(偏移量)
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address);
return;
}
否则尝试共享页面。所谓共享页面就是去查看当前系统的其他进程里,是否加载了相同的页面到内存,如果有,则可以进行共享。
if (share_page(tmp))
return;
如果共享失败,那么则尝试申请一个物理内存页。如果申请失败,抛出out-of-memory,程序退出。
if (!(page = get_free_page()))
oom();
接下来的工作就是将磁盘中的内容读取到内存中。可执行文件的header会占用一个block,使用tmp/1024 + 1就可以得到tmp地址是该文件的第几个block块。 接下来使用bmap函数得到可执行文件的第block块数据位于磁盘上的位置。 接下来使用bread_page读取4k的内容到内存中。
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;
}
最后调用put_page在页表中建立映射关系。
if (put_page(page,address))
return;
mem_init
void mem_init(long start_mem, long end_mem)
HIGH_MEMORY = end_mem;//16M
for (i=0 ; i<PAGING_PAGES ; i++)//15M内存包含多少个4K,即3840
mem_map[i] = USED; //将这些页面都标记为USED状态(100)
i = MAP_NR(start_mem);//计算start_mem在数组中的起始位置
end_mem -= start_mem;
end_mem >>= 12;//计算总共多少个页面
while (end_mem-->0)
mem_map[i++]=0;//将这些页面标记为0,未使用。
calc_mem
void calc_mem(void)
该函数统计内存的空闲情况。
首先遍历物理内存使用数组mem_map,获取其中为0的项目的总数,即统计出有多少4k物理页面还没有使用。
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 (1&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);
}
}