问题:
1.在运行应用程序的时候,是把所有的代码和数据全部加载到内存中么?如果不是,那么在用到一些代码而这些代码不在内存中该肿么办?
2.在fork一个进程的时候,我们完全的为子进程拷贝了父进程的内存空间,那么这个进程的拷贝是真的创建了两个完全一个的内存块在物理内存中么?如果不是,那操作系统怎么做的(写时拷贝)memory.c
内存管理主要实现了两个重要方式:
1.分页机制:缺页重读
2.内存读写权限:用时拷贝
在页目录和页表表项结构中会空余12位的长度供其它权限使用
其中:
p:存在位 当p=1时该项可用,当目录表项或二级页表表项的p=0时,该项无效
1.不是全部进行加载
缺页中断异常处理
dll
2.写时复制机制copy on write
在A进程fork B进程之后,此时只是把A的虚拟内存 拷贝给B
但此时A与B共用一段物理内存,并且把当前的共享内存设置为只读内存
一旦有A或B对这块内存进行写操作的时候,就会引发页面出错异常:page_fault int 14
在该异常的中断服务函数就会首先取消共享内存的操作,并且给写进程复制一个新的物理内存,此时A B就各自有一块要写的内存,然后设置该内存为可读写状态,然后返回重新进行刚才异常的写操作。
写操作---->页面异常中断---->处理写保护异常---->重新分配物理内存页--->重新执行写操作
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);// 设置协处理器的陷阱门。
outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯片的IRQ2 中断请求。
outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯片的IRQ13 中断请求。
set_trap_gate(39,¶llel_interrupt);// 设置并行口的陷阱门。
}
看这段包括异常(陷阱)中断程序初始化子程序,设置它们的中断调用门。而看第14中断就是咱们的页异常中断处理程序
_page_fault:
xchg ss:[esp],eax ;// 取出错码到eax。
push ecx
push edx
push ds
push es
push fs
mov edx,10h ;// 置内核数据段选择符。
mov ds,dx
mov es,dx
mov fs,dx
mov edx,cr2 ;// 取引起页面异常的线性地址
push edx ;// 将该线性地址和出错码压入堆栈,作为调用函数的参数。
push eax
test eax,1 ;// 测试标志P,如果不是缺页引起的异常则跳转。
jne l1
call _do_no_page ;// 调用缺页处理函数(mm/memory.c,365 行)。
jmp l2
l1: call _do_wp_page ;// 调用写保护处理函数(mm/memory.c,247 行)。
l2: add esp,8 ;// 丢弃压入栈的两个参数。
pop fs
pop es
pop ds
pop edx
pop ecx
pop eax
iretd
end
可以看到就是之前中断讲了的那几部,看看调用了啥中断函数,可以看到这里是有一个判断,判断这个中断异常是不是缺页引起的,如果是则call do_no_page函数,如果是权限问题则调用 do_wp_page。
TLB,linux如果老是访问访问几个页表项,这样老是有一个虚拟地址到物理地址的映射,就会很慢,那么linux里面就有一个硬件缓冲区,用来做一个缓存,所以下次再取页表时,就会先从TLB里查,如果没查到就再进行虚拟地址到物理地址映射再来取,然后再刷新到TLB里。当页表项所映射的物理内存有所改变的时候,TLB里面对应的就不一样了,所以就要进行刷新。下面很多函数都会用到这个刷新。而invalidate就是这样一个指令。
#define invalidate() \
_asm{_asm xor eax,eax _asm mov cr3,eax}
#define LOW_MEM 0x100000 // 内存低端(1MB)。
#define PAGING_MEMORY (15*1024*1024)// 分页内存15MB。主内存区最多15M。
#define PAGING_PAGES (PAGING_MEMORY>>12)// 分页后的物理内存页数。
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)// 指定物理内存地址映射为页号。
#define USED 100// 页面被占用标志。
内存映射字节图(1 字节代表1 页内存),每个页面对应的字节用于标志页面当前被引用(占用)次数。物理内存页的使用情况,用了就置1,释放就清0
static unsigned char mem_map [ PAGING_PAGES ] = {0,};
取物理空闲页面。如果已经没有可用内存了,则返回0
unsigned long get_free_page(void)
释放物理地址addr 开始的一页面内存。
void free_page(unsigned long addr)
{
if (addr < LOW_MEM) return;// 如果物理地址addr 小于内存低端(1MB),则返回。
if (addr >= HIGH_MEMORY)// 如果物理地址addr>=内存最高端,则显示出错信息。
panic("trying to free nonexistent page");
addr -= LOW_MEM;// 物理地址减去低端内存位置,再除以4KB,得页面号。
addr >>= 12;
if (mem_map[addr]--) return;// 如果对应内存页面映射字节不等于0,则减1 返回。
mem_map[addr]=0;// 否则置对应页面映射字节为0,并显示出错信息,死机。
panic("trying to free free page");
}
根据指定的线性地址和限长(页表个数),释放对应内存页表所指定的内存块并置表项空闲。
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
//取整
if (from & 0x3fffff)// 要释放内存块的地址需以4M 为边界。
panic("free_page_tables called with wrong alignment");
if (!from)// 出错,试图释放内核和缓冲所占空间。
panic("Trying to free up swapper memory space");
// 计算所占页目录项数(4M 的进位整数倍),也即所占页表数。
//取对应页目录项的编号
size = (size + 0x3fffff) >> 22;
// 下面一句计算起始目录项。对应的目录项号=from>>22,因每项占4 字节,并且由于页目录是从
// 物理地址0 开始,因此实际的目录项指针=目录项号<<2,也即(from>>20)。与上0xffc 确保
// 目录项指针范围有效。
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
for ( ; size-->0 ; dir++) {// size 现在是需要被释放内存的目录项数。
if (!(1 & *dir))// 如果该目录项无效(P 位=0),则继续。
continue;// 目录项的位0(P 位)表示对应页表是否存在。
pg_table = (unsigned long *) (0xfffff000 & *dir);// 取目录项中页表地址。
for (nr=0 ; nr<1024 ; nr++) {// 每个页表有1024 个页项。
if (1 & *pg_table)// 若该页表项有效(P 位=1),则释放对应内存页。
free_page(0xfffff000 & *pg_table);
*pg_table = 0;// 该页表项内容清零。
pg_table++;// 指向页表中下一项。
}
free_page(0xfffff000 & *dir);// 释放该页表所占内存页面。但由于页表在
// 物理地址1M 以内,所以这句什么都不做。
*dir = 0;// 对相应页表的目录项清零。
}
invalidate();// 刷新页变换高速缓冲。
return 0;
}
注释很明白,通过from参数,得到起始页目录项,然后遍历要释放的页目录项,然后再遍历该页目录项的每个页目录,看映射的物理内存页是否有内容,如果有就释放。因为改变了目录项映射到物理内存,所以要invalidate()刷新TLB。
1.把线性地址映射为目录项
2.把目录项映射为目录项地址
3.从目录项地址取页表地址
4.进行页表项地址的操作
5.针对物理内存做操作
//拷贝线性地址空间,从一个线性地址 拷贝到另一个线性地址
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)。参见对115 句的注释。
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++) {
if (1 & *to_dir)// 如果目的目录项指定的页表已经存在(P=1),则出错,死机。
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))// 如果此源目录项未被使用,则不用复制对应页表,跳过。
continue;
// 取当前源目录项中页表的地址 -> from_page_table。
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
// 为目的页表取一页空闲内存,如果返回是0 则说明没有申请到空闲内存页面。返回值=-1,退出。
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 页面。
nr = (from==0)?0xA0:1024;
// 对于当前页表,开始复制指定数目nr 个内存页面。
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;
*to_page_table = this_page;// 将该页表项复制到目的页表中。
// 如果该页表项所指页面的地址在1M 以上,则需要设置内存页面映射数组mem_map[],于是计算
// 页面号,并以它为索引在页面映射数组相应项中增加引用次数。
if (this_page > LOW_MEM) {
// 下面这句的含义是令源页表项所指内存页也为只读。因为现在开始有两个进程共用内存区了。
// 若其中一个内存需要进行写操作,则可以通过页异常的写保护处理,为执行写操作的进程分配
// 一页新的空闲页面,也即进行写时复制的操作。
*from_page_table = this_page;// 令源页表项也只读。
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();// 刷新页变换高速缓冲。
return 0;
}
可以看到这个函数,copy_page_tables在fork.c文件里的copy_mem函数被调用了,而copy_mem被copy_process函数调用了,copy_process不晓得还有没有印象,就在前几篇文章里写过的,就是fork的一个中断系统调用,会先调用find_empty_process找到一个不重复的进程号,然后再调用copy_process,将父进程复制到子进程就是调用这么一个函数,而复制,就是copy_process->copy_mem->copy_page_tables。
在copy_mem函数中可以看到,父进程是将自己的代码段和数据段调用copy_page_tables复制到子进程里,也就是复制页表目录,而这个页表目录怎么复制呢,就得看这个代码了。
*to_dir = ((unsigned long) to_page_table) | 7;是把申请好的物理内存地址映射到其页表地址 并置权限为111
fork的时候复制的内存项
1.复制了内存的目录项
2.复制了进程的页表和页表项
3.设置共享内存
所以这时候,当子进程或者父进程,再写入内存的时候,肯定会报一个异常,因为这两个进程是共享一个物理内存,而且权限设置只读,所以就会到上文的14号中断,会调用do_wp_page函数,就是这么一个流程。
/*
* 当用户试图往一个共享页面上写时,该函数处理已存在的内存页面,(写时复制)
* 它是通过将页面复制到一个新地址上并递减原页面的共享页面计数值实现的。
*
* 如果它在代码空间,我们就以段错误信息退出。
*/
页异常中断处理调用的C 函数。写共享页面处理函数。在page.s 程序中被调用。
// 参数error_code 是由CPU 自动产生,address 是页面线性地址。
// 写共享页面时,需复制页面(写时复制)。
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* 我们现在还不能这样做:因为estdio 库会在代码空间执行写操作 */
/* 真是太愚蠢了。我真想从GNU 得到libc.a 库。 */
if (CODE_SPACE(address)) // 如果地址位于代码空间,则终止执行程序。
do_exit(SIGSEGV);
#endif
// 处理取消页面保护。参数指定页面在页表中的页表项指针,其计算方法是:
// ((address>>10) & 0xffc):计算指定地址的页面在页表中的偏移地址;
// (0xfffff000 &((address>>20) &0xffc)):取目录项中页表的地址值,
// 其中((address>>20) &0xffc)计算页面所在页表的目录项指针;
// 两者相加即得指定地址对应页面的页表项指针。这里对共享的页面进行复制。
un_wp_page(
(unsigned long *)(((address>>10) & 0xffc) +
(0xfffff000 & *((unsigned long *) ((address>>20) &0xffc))))
);
}
取消写保护页面函数。用于页异常中断过程中写保护异常的处理(写时复制)。
// 输入参数为页表项指针。
// [ un_wp_page 意思是取消页面的写保护:Un-Write Protected。]
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;
old_page = 0xfffff000 & *table_entry;// 取原页面对应的目录项号。
// 如果原页面地址大于内存低端LOW_MEM(1Mb),并且其在页面映射字节图数组中值为1(表示仅
// 被引用1 次,页面没有被共享),则在该页面的页表项中置R/W 标志(可写),并刷新页变换
// 高速缓冲,然后返回。
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2;
invalidate();
return;
}
// 否则,在主内存区内申请一页空闲页面。
if (!(new_page=get_free_page()))
oom();// Out of Memory。内存不够处理。
// 如果原页面大于内存低端(则意味着mem_map[]>1,页面是共享的),则将原页面的页面映射
// 数组值递减1。然后将指定页表项内容更新为新页面的地址,并置可读写等标志(U/S, R/W, P)。
// 刷新页变换高速缓冲。最后将原页面内容复制到新页面。
if (old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--;
*table_entry = new_page | 7;
invalidate();
copy_page(old_page,new_page);
}
void un_wp_page(unsigned long * table_entry)
1.检查老的物理内存是否是共享内存
2.为table_entry申请新的内存页,并且设置内存也可写
3.拷贝老的内存页给新的内存页
上面是copy on write,写时复制
缺页异常
页异常中断处理调用的函数。处理缺页异常情况。在page.s 程序中被调用。
// 参数error_code 是由CPU 自动产生,address 是页面线性地址。
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
address &= 0xfffff000;// 页面地址。
// 首先算出指定线性地址在进程空间中相对于进程基址的偏移长度值。
tmp = address - current->start_code;
// 若当前进程的executable 空,或者指定地址超出代码+数据长度,则申请一页物理内存,并映射
// 影射到指定的线性地址处。executable 是进程的i 节点结构。该值为0,表明进程刚开始设置,
// 需要内存;而指定的线性地址超出代码加数据长度,表明进程在申请新的内存空间,也需要给予。
// 因此就直接调用get_empty_page()函数,申请一页物理内存并映射到指定线性地址处即可。
// start_code 是进程代码段地址,end_data 是代码加数据长度。对于linux 内核,它的代码段和
// 数据段是起始基址是相同的。
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address);
return;
}
// 如果尝试共享页面成功,则退出。
if (share_page(tmp))
return;
// 取空闲页面,如果内存不够了,则显示内存不够,终止进程。
if (!(page = get_free_page()))
oom();
/* 记住,(程序)头要使用1 个数据块 */
// 首先计算缺页所在的数据块项。BLOCK_SIZE = 1024 字节,因此一页内存需要4 个数据块。
block = 1 + tmp/BLOCK_SIZE;
// 根据i 节点信息,取数据块在设备上的对应的逻辑块号。
for (i=0 ; i<4 ; block++,i++)
nr[i] = bmap(current->executable,block);
// 读设备上一个页面的数据(4 个逻辑块)到指定物理地址page 处。
bread_page(page,current->executable->i_dev,nr);
// 在增加了一页内存后,该页内存的部分可能会超过进程的end_data 位置。下面的循环即是对物理
// 页面超出的部分进行清零处理。
i = tmp + 4096 - current->end_data;
tmp = page + 4096;
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
// 如果把物理页面映射到指定线性地址的操作成功,就返回。否则就释放内存页,显示内存不够。
if (put_page(page,address))
return;
free_page(page);
oom();
}