谢谢大家。
boot/head.s
_pg_dir: // 页目录将会存放在这里。因为这句话出现head.s第一个有效代码的位置,因为head.s移动到内存物理0的位置,所以页目录的位置存放在0x0000处
org 1000h // 从偏移0x1000 处开始是第1 个页表
pg0:
org 2000h // 从偏移0x2000 处开始是第2 个页表
pg1:
org 3000h // 从偏移0x3000 处开始是第3 个页表
pg2:
org 4000h // 从偏移0x4000 处开始是第4 个页表
pg3:
org 5000h //这句话主要是让下面的代码在第4个页表占用的内存外,因为第4个页表要占用0x4000-0x5000所以这些位置不能存放代码。
问题,页表可以在内存的任意位置吗?
错,页表可以在内存中通过分级,而不存储在一起,但是每一个页表的起始地址必须在4KB边界上,先看下图页目录的存储结构
原因一:我们看到页目录中用来表示页表地址只有10位置,但是现在要表示一个32位页表项的其实地址肯定是做不到,但是我们发现每一个页目录有1024个表,每一个4个字节,正好是占12位(2^10*2^2=2*12),这样的话,如果从4KB的边界开始存放页表,其实不用存储后面的地址,
原因二:从2KB边界开始存放,一个页目录对应的页表项可以用一个页面存储,不必分到两个页面,方便内存的分页管理。
下面的几句代码是对页目录和页表项所在的内存单元清零。主要是避免脏数据对以后代码进行页面转换的时候的影响,给所有的页表项赋首地址的。
setup_paging:
movl $1024*5,%ecx //一个页目录,4个页表项,每一个1024项,
//设置的循环的次数,即使填充次数,
xorl %eax,%eax //stosl使用的参数
xorl %edi,%edi // 页目录从0x000 地址开始。即使页表和页目录的首地址,
cld;rep;stosl //cld:清除EFALGS中方向的标志位置,rep 循环,循环次数有ecx决定, stosl:将eax的值保存到ES:EDI指向的地址中,由于没有设置标志 EDI 每次自增4
//7 = 111b,可知道设置了,第0、1、2位,分表示存在、用户可读写
//下面主要的功能使用来填充页目录项的
//填充页目录,所管理页表项的地址,即使页目录所管理第1个页表的值,在这//里即使 pg0的地址,+7表示页表项的属性,因为后12位不表示地址,因为每//一个页表项占4个自己,所以第2个页目录的地址是pg_dir+4,第2个是页目录//的地址是pg_dir+4,
,
movl $pg0+7,pg_dir
movl $pg1+7,pg_dir+4
movl $pg2+7,pg_dir+8
movl $pg3+7,pg_dir+12
//下面填写页表项的内容呀,首先从最后一个开始填写的,即是pg3页目录的最//后1个页表项的地址
movl $pg3+4092,%edi //计算pg3页目录的最后1个页表项的地址,最后
//1个页表位置在pg3+4KB-4B的位置,所以位置是//pg3+4096-4
movl $0xfff007,%eax //设置最后一个页表项的地址,16M-4K+7,其中
//其中16M-4KB表示地址,7表示存在,用户可读写
std //设置方向位,使用stosl是edi值递减4
1: stosl // stosl:将eax的值保存到ES:EDI指向的地址中, EDI 减4
subl $0x1000,%eax //每次讲eax的值减去4k,即使下一个要用的地址
jge 1b //循环设置,直到地址位0,设置完成,
xorl %eax,%eax // pg_dir在 0x0000
movl %eax,%cr3 //将页目录首地址放到cr3
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 //开启分页,
ret
设置完成后,初始化后的页目录和页表大概关系是这样的,
值得一提是,到现在我们做了这么多的操作,其实我也感到很不解的是,现在的线性地址经过转换得到的物理地址还是和线性地址的表示的值是一样的,也就是说线性地址实际已经是物理地址,我们来举一个列子:0x00000001:我们首先取出 页目录项 0000000000b,页表项:0000000000b 偏移地址是:1, 记过计算还是0x1;
为什么我还要在经过一次分页转换得到物理地址呀,牺牲这么大的性能做无用的的工作那,请看下内存为什么要分页。
下面就要进入mm/memory.c中的代码
memory.c中主要变量的介绍
//下面定义若需要改动,则需要与head.s 等文件中的相关信息一起改变
// linux 0.11 内核默认支持的最大内存容量是16M,可以修改这些定义以适//合更多的内存。
#define LOW_MEM 0x100000
// 内存低端(1MB)。
#define PAGING_MEMORY (15*1024*1024)
// 分页内存15MB。主内存区最多15M。
#define PAGING_PAGES (PAGING_MEMORY>>12)
// 分页后的物理内存页数。因为分页大小是4kB,等于PAGING_MEMORY/4K
//可以写成PAGING_MEMORY/(2^12)即使PAGING_MEMORY>>12
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
// 指定物理内存地址映射为页号。即使mem_map下标
#define USED 100
// 页面被占用标志。
static unsigned char mem_map [ PAGING_PAGES ] = {0,};
// 主内存映射字节图(1 字节代表1 页内存),每个页面对应的字节用于标
//志页面当前被引用(占用)次数。除了内核1M以外的空间
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem; //主内存的结束位置,在main.c设置的
//将所有的内存设置为
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_map数组,来查找系统中空闲的内存地址和共享控制的。
内存的分配,主要出现在进程的拷贝,程序申请内存
在看本函数以前,首先大家要明白意见事情,mem_map数组是用来表示内存是否被占用的,0,空闲,其他被占用的。只要找到mem_map数组中一个值为零的元素就可以了,本函数从mem_map的最后开始查找空闲内存空间
/*
* 获取首个(实际上是最后1 个:-)物理空闲页面,并标记为已使用。如果没有空闲页面, 就返回0。
取物理空闲页面。如果已经没有可用内存了,则返回0。
输入:%1(ax=0) - 0;%2(LOW_MEM);%3(cx=PAGING PAGES);%4(edi=mem_map+PAGING_PAGES-1)。
输出:返回%0(ax=页面起始地址)。
上面%4 寄存器实际指向mem_map[]内存字节图的最后一个字节。本函数从字节图末端开始向前扫描
所有页面标志(页面总数为PAGING_PAGES),若有页面空闲(其内存映像字节为0)则返回页面地址。
注意!本函数只是指出在主内存区的一页空闲页面,但并没有映射到某个进程的线性地址去。后面put_page()函数就是用来作映射的*/
unsigned long get_free_page(void)
{
register unsigned long __res ;
__asm__("std ; repne ; scasb\n\t" //置方向位,al与对应的每个页面(di)内容
//比较,di中存放的是mem_map数组的内容,
"jne 1f\n\t" //没有等于0的,表示没有空闲页面,跳转到结束,返回0
"movb $1,1(%%edi)\n\t" //找到对应的地址,将对应的页面的mem_map
//数组中元素的值,赋值1,表示占用。
"sall $12,%%ecx\n\t" //页面数*4kb=相对应页面起始地址。
"addl %2,%%ecx\n\t" //%2表示主内存,也是mem_map起始地址。所以加
//上 %2才是实际的物理地址
//下面的代码是清空本内存页(4KB)的内容。
"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)
:);
return __res;
}
内存的回收:当进程使结束的时候,或内存不在使用用的。一定要释放内存。这样可以提高内存的利用效率,要是不释放内存,多大的内存都不够用的。
注意:
1.高速缓存、mem_map管理之外的区域、内核使用的内用的内存不能释放。只允许释放非系统区域的内存。
2.空闲内存不允许释放。
/*
释放物理地址'addr'开始的一页内存。用于函数'free_page_tables()'。
释放物理地址addr 开始的一页面内存。
1MB 以下的内存空间用于内核程序和缓冲,不作为分配页面的内存空间。*/
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;
if (mem_map[addr]--) return;
mem_map[addr]=0;
panic("trying to free free page");
}
下面主要是释放连续的的页面。
/*
下面函数释放页表连续的内存块,'exit()'需要该函数。与copy_page_tables()
类似,该函数仅处理4Mb 的内存块。
根据指定的线性地址和限长(页表个数),释放对应内存页表所指定的内存块并置表项空闲。
页目录位于物理地址0 开始处,共1024 项,占4K 字节。每个目录项指定一个页表。
页表从物理地址0x1000 处开始(紧接着目录空间),每个页表有1024 项,也占4K 内存。
每个页表项对应一页物理内存(4K)。目录项和页表项的大小均为4 个字节。
参数:from - 起始基地址;size - 释放的长度。*/
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
//判断页面的起始位置是否在4MB的边界,
if (from & 0x3fffff)
panic("free_page_tables called with wrong alignment");
//from为0,表示要释放内核区域,出错
if (!from)
panic("Trying to free up swapper memory space");
// 计算所占页目录项数(4M 的进位整数倍),也即所占页表数。
//采用近一法计算,只要size对0xfffff取余数不为零,近一
size = (size + 0x3fffff) >> 22;
//得到页目录了,
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
// size 现在是需要被释放内存的目录项数。
for ( ; size-->0 ; dir++) {
//判断页目录是否被占用, 如果该目录项无效(P 位=0),则继续。
if (!(1 & *dir))
continue;
//找到页表地址, 取目录项中页表地址。
pg_table = (unsigned long *) (0xfffff000 & *dir);
//对一个页目录中的所有页表项进行释放, 每个页表有1024 个页项。
for (nr=0 ; nr<1024 ; nr++) {
//若该页表项有效(P 位=1),则释放对应内存页。
if (1 & *pg_table)
free_page(0xfffff000 & *pg_table);
*pg_table = 0; //该页表项内容清零。
pg_table++; //指向页表中下一项。
}
free_page(0xfffff000 & *dir); // 释放该页表所占内存页面。但由于页表在
// 物理地址1M 以内,所以这句什么都不做
*dir = 0;
}
invalidate(); // 对相应页表的目录项清零。
return 0;
}
/*
好了,下面是内存管理mm 中最为复杂的程序之一。它通过只复制内存页面
来拷贝一定范围内线性地址中的内容。希望代码中没有错误,因为我不想
再调试这块代码了 :-)
注意!我们并不是仅复制任何内存块- 内存块的地址需要是4Mb 的倍数(正好
一个页目录项对应的内存大小),因为这样处理可使函数很简单。不管怎样,
它仅被fork()使用(fork.c)
注意!!当from==0 时,是在为第一次fork()调用复制内核空间。此时我们
不想复制整个页目录项对应的内存,因为这样做会导致内存严重的浪费- 我们
只复制头160 个页面- 对应640kB。即使是复制这些页面也已经超出我们的需
求,但这不会占用更多的内存- 在低1Mb 内存范围内我们不执行写时复制操
作,所以 这些页面可以与内核共享。因此这是nr=xxxx 的特殊情况(nr 在程
序中指页面数)。
复制指定线性地址和长度(页表个数)内存对应的页目录项和页表,从而被复制的页目录和页表对应的原物理内存区被共享使用。复制指定地址和长度的内存对应的页目录项和页表项。需申请页面来存放新页表,原内存区被共享;此后两个进程将共享内存区,直到有一个进程执行写操作时,才分配新的内存页(写时复制机制)。*/
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;
// 源地址和目的地址都需要是在4Mb 的内存边界地址上。否则出错,死机。
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
// 取得源地址和目的地址的页目录项(from_dir 和to_dir)。
//本来from移动22位才会得到页目录的,为什么移动20位呀?
//因为(from>>22)*4得到页表地址,(from>>20)清空后两位。
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
///判读目的页目录是否被占用了,如果目的页目录项指定的页表已经存
//在(P=1),则出错,死机。
if (1 & *to_dir)
panic("copy_page_tables: already 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; /* Out of memory, see freeing */
// 设置目的目录项信息。7 是标志信息,表示(Usr, R/W, Present)。
*to_dir = ((unsigned long) to_page_table) | 7;
// 针对当前处理的页表,设置需复制的页面数。如果是在
//内核空间,则仅需复制头160 页,
// 否则需要复制1 个页表中的所有1024 页面。
//from=0,表示要拷贝的页目录位0进程,也就是进程0,由于进程0
//是内核进程,虽然所有进程都是0的子进程,但是由于0进程的特殊//性,只拷贝160(0xa0)个页表项即可了,
nr = (from==0)?0xA0:1024;
//将从from_page_table开始的页表nr个页表中的内容复制到
//to_page_table中,并将复制页面的使用次数加1(即使在mem_map
//对应的值),
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
//如果当前页面没有被使用将不复制本页面的。
if (!(1 & this_page))
continue;
//复位页表项中R/W 标志(置0)。(如果U/S 位是0,则R/W 就没
//有作用。如果U/S 是1,而R/W 是0,那么运行在用户层
//的代码就只能读页面。如果U/S 和R/W 都置位,则就有写的权限。)
this_page &= ~2; // 将该页表项复制到目的页表中。
// 如果该页表项所指页面的地址在1M 以上,则需要设置内存页面映射数组
//mem_map[],于是计算页面号,并以它为索引在页面映射数组相应项中增加
//引用次数。
*to_page_table = this_page;
if (this_page > LOW_MEM) {
//同时也更新原页面的状态的,主要是读写状态,因为现在两个、、
//进程使用同样的物理页面,两个进程对内容只允许读
*from_page_table = this_page;
//下面的三句用于计算页表在mem_map数组中对应的值
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}