Linux 0.11 内存管理 代码注释

/*
 *  linux/mm/memory.c
 *
 *  (C) 1991  Linus Torvalds
 */


#include <signal.h>
#include <asm/system.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <linux/kernel.h>


// 进程退出处理函数
volatile void do_exit(long code);					// 在 kernel/exit.c,102 行
													// 函数名前的关键字volatile用于告诉编译器gcc该函数不会返回。这样可以让gcc产生更
													// 好一些的代码,更重要的是使用这个关键字可以避免产生某些(未初始化变量的)假警告信息。


// 显示内存已用完出错信息,并退出。
static inline volatile void oom(void)				
{
	printk("out of memory\n\r");
	do_exit(SIGSEGV);								// do_exit应该使用退出代码,这里用了信号值SIGSEGV(11)相同值的出错码含义是
    												// “资源暂时不可用”,正好同义。
}

// 刷新页变换高速缓冲宏函数。
#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))				// 为了提高地址转换的效率,CPU将最近使用的页表数据存放在芯片中高速缓冲中。在修
													// 改过页表信息之后,就需要刷新该缓冲区。这里使用重新加载页目录基地址寄存器cr3
													// 的方法来进行刷新。下面eax=0,是页目录的基址。



// linux0.11内核默认支持的最大内存容量是16MB
// 可以修改这些定义适合更多的内存
// PAGING_MEMORY是除了1MB剩下的 [主内存区域]     
// MAP_NR 也得除去1MB 那就说明主内存区域开始是0号页
#define LOW_MEM 0x100000							// 内存低端(1MB) [内核所占空间]
#define PAGING_MEMORY (15*1024*1024) 				// 分页内存15 MB,主内存区最多15M. 转换成字节
#define PAGING_PAGES (PAGING_MEMORY>>12)			// 分页后的物理内存页面数(3840) 一页的大小4kb[4 * 1024 = 2的12次方]
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)			// 指定地址映射为页号 [物理地址]
#define USED 100									// 页面被占用标志.



// 该宏用于判断给定线性地址是否位于当前进程的代码段中
#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \ // 这个是要计算addr线性地址所在页末尾的线性地址
current->start_code + current->end_code)			// &~ 4095 的意思是将末尾三位取0 [因为一个页的开始地址末尾三位就是0]
													// ~ 取反操作 4095 = 0x0FFF [ 取反之后:0x1000]
													// 如果是addr末尾三位取0,那么是开始的线性地址,所以要加4095


// 全局变量,存放实际物理内存最高端地址
static long HIGH_MEMORY = 0;          				// 在下面mem_init中有设置  			


// 从from处复制1页内存到to处(4K字节)
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024))


// 物理内存映射字节图(1字节代表1页内存)
// 每个页面对应的字节用于标志页面当前引用(占用)次数
static unsigned char mem_map [ PAGING_PAGES ] = {0,}; // 它最大可以映射15MB的内存空间。在初始化函数mem_init()中,对于
													  // 不能用做主内存页面的位置均都预先被设置成USED(100)



// 获取首个空闲页面[物理地址],并标记为已使用(设置mem_map[])。如果没有可用内存,就返回0
// 输入:%1(ax = 0)
//       %2(LOW_MEM)                                  内存字节位图(mem_map[])管理的起始位置
//       %3(cx = PAGING_PAGES);                       分页后的物理内存页面数
//       %4(edi = mem_map + PAGING_PAGES - 1)	      指向mem_map[]内存字节位图的最后一个字节
// 输出:返回%0(ax = 物理内存页面起始地址)

// 本函数从位图末端开始向前扫描所有页面标志(页面总数PAGING_PAGE),
// 若有页面空闲(内存位图字节为0)则返回页面地址。注意!本函数只是指
// 出在主内存区的一页空闲物理内存页面,但并没有映射到某个进程的地址
// 空间中去。后面的put_page()函数即用于把指定页面映射到某个进程地址
// 空间中。当然对于内核使用本函数并不需要再使用put_page()进行映射,
// 因为内核代码和数据空间(16MB)已经对等地映射到物理地址空间。
unsigned long get_free_page(void){

	register unsigned long __res asm("ax");
 
	__asm__("std ; repne ; scasb\n\t"   			    // 置方向位,al(0)与对应每个页面的(di)内容比较
		"jne 1f\n\t"                    				// 如果没有等于0的字节,则跳转结束(返回0).
		"movb $1,1(%%edi)\n\t"          				// 1 => [1+edi],将对应页面内存映像bit位置1.
		"sall $12,%%ecx\n\t"            				// 页面数*4k = 相对页面起始地址
		"addl %2,%%ecx\n\t"             				// 再加上低端内存地址,得页面实际物理起始地址
		"movl %%ecx,%%edx\n\t"          				// 将页面实际起始地址->edx寄存器。
		"movl $1024,%%ecx\n\t"          				// 寄存器ecx置计数值1024
		"leal 4092(%%edx),%%edi\n\t"    				// 将4092+edx的位置->edi(该页面的末端地址)
		"rep ; stosl\n\t"               				// 将edi所指内存清零(反方向,即将该页面清零)
		"movl %%edx,%%eax\n"            				// 将页面起始地址->eax(返回值)
		"1:"
		:"=a" (__res)
		:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
		"D" (mem_map+PAGING_PAGES-1)
		);
	return __res;           							// 返回空闲物理页面地址(若无空闲页面则返回0).
}




// 释放物理地址addr开始的1页面内存。
void free_page(unsigned long addr)
{
	if (addr < LOW_MEM) 								// 物理地址1MB以下的内容空间用于内核程序和缓冲
		return;
	if (addr >= HIGH_MEMORY)
		panic("trying to free nonexistent page");		// 大于最大内存显示出错信息

	addr -= LOW_MEM;									// 物理地址减去低端内存地址,再除以4KB,得页面号
	addr >>= 12;
	if (mem_map[addr]--) 								// 当前引用次数大于0,减1返回
		return;

	mem_map[addr]=0;									// 置为0 报错
	panic("trying to free free page");
}




// 释放页表连续得内存块,该函数仅处理4Mb的内存块
// 根据指定的线性地址和限长(页表个数),释放对应内存页表指定的内存块并置表项为
// 空闲。页目录位于物理地址0开始处,共1024项,每项4字节,共占4K字节。每个目录项
// 指定一个页表。内核页表从物理地址0x1000处开始(紧接着目录空间),共4个页表。每
// 个页表有1024项,每项4字节。因此占4K(1页)内存。各进程(除了在内核代码中的进
// 程0和1)的页表所占据的页面在进程被创建时由内核为其主内存区申请得到。每个页表
// 项对应1耶物理内存,因此一个页表最多可映射4MB的物理内存。
// 参数:from - 起始线性基地址;size - 释放的字节长度。
int free_page_tables(unsigned long from,unsigned long size)
{
	unsigned long *pg_table;
	unsigned long * dir, nr;

    // 首先检测参数from给出的线性基地址是否在4MB的边界处。因为该函数只能处理这
    // 种情况。若from=0,则出错。说明视图释放内核和缓冲所占空间。
	if (from & 0x3fffff)
		panic("free_page_tables called with wrong alignment");
	if (!from)
		panic("Trying to free up swapper memory space");

    // 计算所占页目录项数,也即所占页表数
	size = (size + 0x3fffff) >> 22;						    // 当为4k的时候,会进1
															// 0x3fffff 是22位
	// 计算起始目录项
	dir = (unsigned long *) ((from>>20) & 0xffc);           // from>>22得到的是目录项编号
															// 每一项都是4字节,即from>>22之后乘以4就得到该项的相对于页目录指针(CR3)的偏移了


	// 释放页目录表和页表之间的关系[*dir = 0]
	// 释放页表所占用的内存空间[free_page(0xfffff000 & *dir)] 
	// 释放页表和实际对应的物理基址之间的关系[*pg_table = 0]
	// 释放在这个物理基址下的页面[free_page(0xfffff000 & *pg_table)]
	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);                   	// 释放该页表所占内存页面。
		*dir = 0;                                       	// 对应页表的目录项清零
	}

	invalidate();                                       	// 在修改过页表信息之后,就需要刷新该缓冲区
															// 这里使用重新加载页目录基地址寄存器cr3的方法来进行刷新			
	return 0;
}




// 将从from开始的大小为size的页目录对应页表的值复制到以to开始的
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_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)												// 如果目的目录项指定的页表已经存在(P=1),则出错,死机
			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()))		// 为目的页表取一页空闲内存,如果返回0表示没有申请到空闲页面内存
			return -1;													


		*to_dir = ((unsigned long) to_page_table) | 7;					// 设置目的目录项信息[最后三位]  7[Usr,R/W,Present]
																		// 表示对应页表映射的内存页面是用户级的,并且可读写、存在
		// 针对当前处理的页表,设置需复制的页面数
		nr = (from == 0) ? 0xA0 : 1024;									// 如果是在内核空间,则仅需复制头160页(640KB)
																		// 否则需要复制一个页表的所有1024页面

		// 复制每个页表里面的页面
		// nr是复制的页面数 
		// 将数据[*to_page_table &= 2][只读]从 from_page_table 复制到 to_page_table
		// 如果 from_page_table 是在主存,则也要设置为[*to_page_table &= 2][只读]
		for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
			this_page = *from_page_table;
			if (!(1 & this_page))			 							// 如果当前源页面没有使用,则不用复制
				continue;
			
			// 如果U/S位是0,则R/W就没有作用
			// 如果U/S是1,而R/W是0,那么运行在用户层的代码就只能读页面
			// 如果U/S和R/W都置位,则就有写的权限
			this_page &= ~2;											// ~2: 1111 11101 只读
			*to_page_table = this_page;
            
			// 对于内核移动到任务0中并且调用fork()创建任务1时(用于运行init())
			// 由于此时复制的页面还仍然都在内核代码区域,因此以下判断中的语句不会执行
			// 任务0的页面仍然可以随时读写。只有当调用fork()的父进程代码处于主内存区
			// (页面位置大于1MB)时才会执行。这种情况需要在进程调用execve(),并装载执
			// 行了新程序代码时才会出现
			if (this_page > LOW_MEM) {
				*from_page_table = this_page;							// 令源页表项所指内存页也为只读
																		// 因为现在开始有两个进程公用内存区了。若其中1个进程需要进行写操作,
																		// 则可以通过页异常写保护处理为执行写操作的进程匹配1页新空闲页面,也
																		// 即进行写时复制(copy on write)操作
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;									// mem_map[]仅用于管理主内存区中的页面使用情况
			}
		}
	}
	invalidate();
	return 0;
}




// 把一物理内存页面映射到线性地址空间指定处。
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 disagrees with %p at %p\n",page,address);

    // 如果该目录项有对应的页表,取出来
	// 否则 get_free_page,将获取到的页表地址赋给该目录项
	page_table = (unsigned long *) ((address>>20) & 0xffc);				// 计算指定地址在页目录表中对应的目录项指针
	if ((*page_table)&1)												// 如果该目录项[目录项中的值]有效(P=1)(也即指定的页表在内存中)
		page_table = (unsigned long *) (0xfffff000 & *page_table);      // 从中取出指定页表的地址
	else {
		if (!(tmp=get_free_page()))
			return 0;
		*page_table = tmp|7;											// [User, U/S, R/W]
		page_table = (unsigned long *) tmp;
	}
    
	// 将页表中的某个位置设为address
	page_table[(address>>12) & 0x3ff] = page | 7;						// 0x3ff[11 1111 1111] 10位,在页表中的索引值

	return page;
}



 取消写保护页面函数。用于页异常中断过程中写保护异常的处理(写时复制)。
// 在内核创建进程时,新进程与父进程被设置成共享代码和数据内存页面,并且所有这些
// 页面均被设置成只读页面。而当新进程或原进程需要向内存页面写数据时,CPU就会检测
// 到这个情况并产生页面写保护异常。于是在这个函数中内核就会首先判断要写的页面是
// 否被共享。若没有则把页面设置成可写然后退出。若页面是出于共享状态,则需要重新
// 申请一新页面并复制被写页面内容,以供写进程单独使用。共享被取消。本函数供下面
// do_wp_page()调用。
// 输入参数为页表项指针[就是已经确定了页表中的哪一项]。
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) {          //  == 1 表示页面仅被引用1次,页面没有被共享
		*table_entry |= 2;												// 在该页面的页表项中置R/W标志(可写)
		invalidate();													// 刷新页变换高速缓冲
		return;
	}

    // 否则就需要在主内存区申请一页空闲页面给执行写操作的进程单独使用
	// 取消页面共享
	// 原来父进程[XXX]和子进程使[*table_entry]用同一页面[old_page]
	// 使子进程使用get_free_page(这个函数中设置了mem_map) 
	// 父进程的mem_map - 1
	if (!(new_page=get_free_page()))
		oom();							 								 // 显示内存已用完出错信息,并退出
	if (old_page >= LOW_MEM)		
		mem_map[MAP_NR(old_page)]--;									 // 将原页面的页面映射字节数组递减1
	*table_entry = new_page | 7;										 // 置可读写等标志(U/S、R/W、P)
	invalidate();
	copy_page(old_page,new_page);
}	


 执行写保护页处理。
// 页异常中断处理过程中调用的C函数。在page.s程序中被调用。
// 参数error_code是进程在写写保护页面时由CPU自动产生,address是页面线性地址。
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
	if (CODE_SPACE(address))
		do_exit(SIGSEGV);
#endif
    // do_wp_page(输入参数为页表项指针[就是已经确定了页表中的哪一项])
	// ffc 就是10个1
	// *((unsigned long *) ((address>>20) &0xffc)) 页目录项中的值,就是页表的地址
	// 0xfffff000 & 意思是取页表的基址
	// (address>>10) & 0xffc) 页目录项中的偏移地址
	un_wp_page((unsigned long *)
		(((address>>10) & 0xffc) + (0xfffff000 &
		*((unsigned long *) ((address>>20) &0xffc)))));

}


 写页面验证
// 若页面不可写,则复制页面。
// 参数address是指定页面的线性地址。
void write_verify(unsigned long address)
{
	unsigned long page;

    // 判断指定地址所对应页目录项的页表是否存在(P), 若不存在(P=0)则返回
	if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
		return;

	page &= 0xfffff000;									// 取页表的地址
	page += ((address>>10) & 0xffc);					// 在页表中的页表项偏移值
    
	if ((3 & *(unsigned long *) page) == 1)  			// 如果该页面不可写(标志R/W没有置位)
		un_wp_page((unsigned long *) page);				// 写时复制
	return;
}


 取得一页空闲内存页并映射到指定线性地址处。
void get_empty_page(unsigned long address)
{
	unsigned long tmp;

    // 若不能取得一空闲页面,或者不能将页面放置到指定地址处,则显示内存不够的信息
	// 即使执行 get_free_page() 返回0也无所谓,因为 put_page()
	// 中还会对此情况再次申请空闲物理页面的
	if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
		free_page(tmp);		
		oom();
	}
}



 尝试对当前进程指定地址处的页面进行共享处理。
// 当前进程与进程p是同一执行代码,也可以认为当前进程是由p进程执行fork操作产生的
// 进程,因此它们的代码内容一样。所以让当前进程和进程P指向同一页面
// address: 线性地址在进程空间中相对于进程基址的偏移长度值
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;

	// 有点晕 大概就是取p进程和当前进程 address 处对应的页目录项
	// 是否可以看成:from_page = ((address + p->start_code) >> 20) & 0xffc
	from_page = to_page = ((address>>20) & 0xffc);					// 求指定内存地址的页目录项
	from_page += ((p->start_code>>20) & 0xffc);						// 计算进程p的代码起始地址所对应的页目录项
	to_page   += ((current->start_code>>20) & 0xffc);			    // 计算当前进程中代码起始地址所对应的页目录项


	// 取P进程的页表项
	from = *(unsigned long *) from_page;							 // 取页目录项内容
	if (!(from & 1))											     // 若该目录项无效(P=0), 则返回
		return 0;
	from &= 0xfffff000;												 // 取该目录项对应页表地址
	from_page = from + ((address>>10) & 0xffc);						 // 在页表里偏移指针

	// 取P进程实际的物理地址
	phys_addr = *(unsigned long *) from_page;						 // 实际的物理地址

	// 检查P进程物理页面是否存在并且干净
	// 0x41对应页表项中的D(dirty) 和P(present)标志
	// 如果页面不干净或无效则返回
	if ((phys_addr & 0x41) != 0x01)									
		return 0;

	// 取页面地址 -> phys_addr
	// 如果该页面地址不存在或小于内存低端(1M)也返回退出
	phys_addr &= 0xfffff000;
	if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
		return 0;

    // 取当前进程的页目录项
	to = *(unsigned long *) to_page;								  // 取页目录项内容
	if (!(to & 1)) {												  // 如果该目录项无效(P=0),则取空闲页面,并更新 to_page 所指的目录项
		if ((to = get_free_page()))
			*(unsigned long *) to_page = to | 7;
		else
			oom();
	}
    
	// 取当前进程的页表项
	to &= 0xfffff000;												   // 取页表地址
	to_page = to + ((address>>10) & 0xffc);							   // 页表地址 + 偏移地址
	if (1 & *(unsigned long *) to_page)								   // 如果对应的页面已经存在,则出错,死机
		panic("try_to_share: to_page already exists");
    

	*(unsigned long *) from_page &= ~2;								   // 对p进程中页面置写保护标志(置R/W只读)
	*(unsigned long *) to_page = *(unsigned long *) from_page;		   // 当前进程中的对应页表项指向它
    

	invalidate();													   // 刷新页变换高速缓冲
	phys_addr -= LOW_MEM;
	phys_addr >>= 12;
	mem_map[phys_addr]++;											   // 对应页面映射字节数组项中的引用递增1
	return 1;														   // 最后返回1,表示共享处理成功
}	


 共享页面处理。
// 在发生缺页异常时,首先看看能否与运行同一个执行文件的其他进程进行页面共享处理
// address: 线性地址在进程空间中相对于进程基址的偏移长度值
// 返回:1 - 共享操作成功,0 - 失败。
static int share_page(unsigned long address)
{
	struct task_struct ** p;

    // 首先检查一下当前进程的executable字段是否指向某执行文件的i节点,以判断本
    // 进程是否有对应的执行文件。如果当前进程运行的执行文件的内存i节点引用
    // 计数等于1(executable->i_count=1),表示当前系统中只有1个进程(即当前进程)在
    // 运行该执行文件。因此无共享可言,直接退出函数。
	if (!current->executable)
		return 0;
	if (current->executable->i_count < 2)
		return 0;

    // 否则搜索任务数组中所有任务, 寻找与当前进程可共享页面的进程
	for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p)										// 如果该任务项空闲,则继续寻找
			continue;

		if (current == *p)								// 如果就是当前任务,也继续寻找
			continue;
	
		if ((*p)->executable != current->executable)	// 如果 executable 不等,也继续
			continue;

		if (try_to_share(address,*p))					// 尝试共享页面
			return 1;
	}

	return 0;
}



// 执行缺页处理
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 &= 0xfffff000]没有
	tmp = address - current->start_code;									// 线性地址在进程空间中相对于进程基址的偏移长度值
    

	// executable 是进程的 i 节点结构,该值为0,表明进程刚开始设置,需要内存
	// 如果指定的线性地址超出代码加数据长度,表明进程在申请新的内存空间,需要内存
	if (!current->executable || tmp >= current->end_data) {              	// current -> executable 可执行文件的i节点
																			// current -> end_data 代码段长度 + 数据段长度
		get_empty_page(address);											// 申请一页内存
		return;
	}

	// 如果尝试分享页面成功,则退出
	if (share_page(tmp))																					
		return;

	// 取空闲页面,如果内存不够了,则显示内存不够,终止进程
	if (!(page = get_free_page()))
		oom();


	
	block = 1 + tmp/BLOCK_SIZE;												// (程序) 头要使用1个数据块
	for (i=0 ; i<4 ; block++,i++)
		nr[i] = bmap(current->executable, block);							// 根据 i 节点信息,取数据块在设备上对应的逻辑块号
	bread_page(page, current->executable -> i_dev, nr);						// 读设备上一个页面的数据(4个逻辑块)
																			// 到指定物理地址 page 处
    
	// 在增加了一页内存后,该页内存的部分可能会超过进程的 end_data 位置
	// 将页面超出的部分清零
	i = tmp + 4096 - current->end_data;
	tmp = page + 4096;
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;
	}

    // 最后把引起缺页异常的一页物理页面映射到指定线性地址address处
	if (put_page(page,address))
		return;

	// 释放内存页,显示内存不够
	free_page(page);
	oom();
}

// 物理内存管理初始化
// 该函数对1MB以上的内存区域以页面为单位进行管理前的初始化设置工作。一个页面长度
// 为4KB bytes.该函数把1MB以上所有物理内存划分成一个个页面,并使用一个页面映射字节
// 数组mem_map[]来管理所有这些页面。对于具有16MB内存容量的机器,该数组共有3840
// 项((16MB-1MB)/4KB),即可管理3840个物理页面。每当一个物理内存页面被占用时就把
// mem_map[]中对应的字节值增1;若释放一个物理页面,就把对应字节值减1。若字节值为0,
// 则表示对应页面空闲;若字节值大于或等于1,则表示对应页面被占用或被不同程序共享占用。
// 在该版本的Linux内核中,最多能管理16MB的物理内存,大于16MB的内存将弃之不用。
// 对于具有16MB内存的PC机系统,在没有设置虚拟盘RAMDISK的情况下start_mem通常是4MB,
// end_mem是16MB。因此此时主内存区范围是4MB-16MB,共有3072个物理页面可供分配。而
// 范围0-1MB内存空间用于内核系统(其实内核只使用0-640Kb,剩下的部分被部分高速缓冲和
// 设备内存占用)。
// 参数start_mem是可用做页面分配的主内存区起始地址(已去除RANDISK所占内存空间)。
// end_mem是实际物理内存最大地址。而地址范围start_mem到end_mem是主内存区。
void mem_init(long start_mem, long end_mem) {

	int i;

    // 首先将1MB到16MB范围内所有内存页面对应
	// 的内存映射字节数组项置为已占用状态
	HIGH_MEMORY = end_mem;                  		// 初始化 HIGH_MEMORY: 最大物理内存
	for (i=0 ; i<PAGING_PAGES ; i++)
		mem_map[i] = USED;							// 100

    
	// 对于具有16MB物理内存的系统
	// mem_map[]中对应4MB-16MB主内存区的项被清零。
	i = MAP_NR(start_mem);      					// 主内存区起始位置处页面号[]  i是在mem_map中的位置
													// MAP_NR(addr) (((addr)-LOW_MEM)>>12)
	end_mem -= start_mem;
	end_mem >>= 12;             					// 主内存区中的总页面数
	while (end_mem-->0)
		mem_map[i++]=0;         					// 主内存区页面对应字节值清零
}


// 计算内存空闲页面数并显示
// [内核中没有其他地方调用该函数,Linus调试过程中用的]
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++) {               			  // i应该等于4  页目录项0-3被内核使用
		if (1 & pg_dir[i]) {							  // 第一位 P 位,用于确定一个表项是否可以用于地址转换过程
			pg_tbl=(long *)(0xfffff000 & pg_dir[i]);	  // 去除 pg_dir[i] 中的属性位,页表的位置
			for(j = k = 0; j < 1024; j++)
				if (pg_tbl[j] & 1)
					k++;								  // k 是 在这个页表中,有多少项是使用的
			printk("Pg-dir[%d] uses %d pages\n",i,k);
		}
	}
}
目录树 下面再给个样例 ├─Makefile │ ├─boot │ bootsect.s │ head.s │ setup.s │ ├─fs │ bitmap.c │ block_dev.c │ buffer.c │ char_dev.c │ exec.c │ fcntl.c │ file_dev.c │ file_table.c │ inode.c │ ioctl.c │ Makefile │ namei.c │ open.c │ pipe.c │ read_write.c │ stat.c │ super.c │ truncate.c │ ├─include │ │ a.out.h │ │ const.h │ │ ctype.h │ │ errno.h │ │ fcntl.h │ │ signal.h │ │ stdarg.h │ │ stddef.h │ │ string.h │ │ termios.h │ │ time.h │ │ unistd.h │ │ utime.h │ │ │ ├─asm │ │ io.h │ │ memory.h │ │ segment.h │ │ system.h │ │ │ ├─linux │ │ config.h │ │ fs.h │ │ hdreg.h │ │ head.h │ │ kernel.h │ │ mm.h │ │ sched.h │ │ sys.h │ │ tty.h │ │ │ └─sys │ stat.h │ times.h │ types.h │ utsname.h │ wait.h │ ├─init │ main.c │ ├─kernel │ │ asm.s │ │ exit.c │ │ fork.c │ │ mktime.c │ │ panic.c │ │ printk.c │ │ sched.c │ │ signal.c │ │ sys.c │ │ system_call.s │ │ vsprintf.c │ │ │ ├─blk_drv │ │ blk.h │ │ floppy.c │ │ hd.c │ │ ll_rw_blk.c │ │ Makefile │ │ ramdisk.c │ │ │ ├─chr_drv │ │ console.c │ │ keyboard.S │ │ Makefile │ │ rs_io.s │ │ serial.c │ │ tty_io.c │ │ tty_ioctl.c │ │ │ └─math │ Makefile │ math_emulate. │ ├─lib │ close.c │ ctype.c │ dup.c │ errno.c │ execve.c │ Makefile │ malloc.c │ open.c │ setsid.c │ string.c │ wait.c │ write.c │ _exit.c │ ├─mm │ Makefile │ memory.c │ page.s │ └─tools build.c 样例 main。c 用sourceinsight软件阅读 很方便 /* * linux/init/main.c * * (C) 1991 Linus Torvalds */ #define __LIBRARY__ // 定义该变量是为了包括定义在unistd.h 中的内嵌汇编代码等信息。 #include // *.h 头文件所在的默认目录是include/,则在代码中就不用明确指明位置。 // 如果不是UNIX 的标准头文件,则需要指明所在的目录,并用双引号括住。 // 标准符号常数与类型文件。定义了各种符号常数和类型,并申明了各种函数。 // 如果定义了__LIBRARY__,则还包括系统调用号和内嵌汇编代码_syscall0()等。 #include // 时间类型头文件。其中最主要定义了tm 结构和一些有关时间的函数原形。 /* * we need this inline - forking from kernel space will result * in NO COPY ON WRITE (!!!), until an execve is executed. This * is no problem, but for the stack. This is handled by not letting * main() use the stack at all after fork(). Thus, no function * calls - which means inline code for fork too, as otherwise we * would use the stack upon exit from 'fork()'. * * Actually only pause and fork are needed inline, so that there * won't be any messing with the stack from main(), but we define * some others too. */ /* * 我们需要下面这些内嵌语句 - 从内核空间创建进程(forking)将导致没有写时复制(COPY ON WRITE)!!! * 直到一个执行execve 调用。这对堆栈可能带来问题。处理的方法是在fork()调用之后不让main()使用 * 任何堆栈。因此就不能有函数调用 - 这意味着fork 也要使用内嵌的代码,否则我们在从fork()退出 * 时就要使用堆栈了。 * 实际上只有pause 和fork 需要使用内嵌方式,以保证从main()中不会弄乱堆栈,但是我们同时还 * 定义了其它一些函数。 */ static inline _syscall0 (int, fork) // 是unistd.h 中的内嵌宏代码。以嵌入汇编的形式调用 // Linux 的系统调用中断0x80。该中断是所有系统调用的 // 入口。该条语句实际上是int fork()创建进程系统调用。 // syscall0 名称中最后的0 表示无参数,1 表示1 个参数。 static inline _syscall0 (int, pause) // int pause()系统调用:暂停进程的执行,直到 // 收到一个信号。 static inline _syscall1 (int, setup, void *, BIOS) // int setup(void * BIOS)系统调用,仅用于 // linux 初始化(仅在这个程序中被调用)。 static inline _syscall0 (int, sync) // int sync()系统调用:更新文件系统。 #include // tty 头文件,定义了有关tty_io,串行通信方面的参数、常数。 #include // 调度程序头文件,定义了任务结构task_struct、第1 个初始任务 // 的数据。还有一些以宏的形式定义的有关描述符参数设置和获取的 // 嵌入式汇编函数程序。 #include // head 头文件,定义了段描述符的简单结构,和几个选择符常量。 #include // 系统头文件。以宏的形式定义了许多有关设置或修改 // 描述符/中断门等的嵌入式汇编子程序。 #include // io 头文件。以宏的嵌入汇编程序形式定义对io 端口操作的函数。 #include // 标准定义头文件。定义了NULL, offsetof(TYPE, MEMBER)。 #include // 标准参数头文件。以宏的形式定义变量参数列表。主要说明了-个 // 类型(va_list)和三个宏(va_start, va_arg 和va_end),vsprintf、 // vprintf、vfprintf。 #include #include // 文件控制头文件。用于文件及其描述符的操作控制常数符号的定义。 #include // 类型头文件。定义了基本的系统数据类型。 #include // 文件系统头文件。定义文件表结构(file,buffer_head,m_inode 等)。 static char printbuf[1024]; // 静态字符串数组。 extern int vsprintf (); // 送格式化输出到一字符串中(在kernel/vsprintf.c,92 行)。 extern void init (void); // 函数原形,初始化(在168 行)。 extern void blk_dev_init (void); // 块设备初始化子程序(kernel/blk_drv/ll_rw_blk.c,157 行) extern void chr_dev_init (void); // 字符设备初始化(kernel/chr_drv/tty_io.c, 347 行) extern void hd_init (void); // 硬盘初始化程序(kernel/blk_drv/hd.c, 343 行) extern void floppy_init (void); // 软驱初始化程序(kernel/blk_drv/floppy.c, 457 行) extern void mem_init (long start, long end); // 内存管理初始化(mm/memory.c, 399 行) extern long rd_init (long mem_start, int length); //虚拟盘初始化(kernel/blk_drv/ramdisk.c,52) extern long kernel_mktime (struct tm *tm); // 建立内核时间(秒)。 extern long startup_time; // 内核启动时间(开机时间)(秒)。 /* * This is set up by the setup-routine at boot-time */ /* * 以下这些数据是由setup.s 程序在引导时间设置的(参见第2 章2.3.1 节中的表2.1)。 */ #define EXT_MEM_K (*(unsigned short *)0x90002) // 1M 以后的扩展内存大小(KB)。 #define DRIVE_INFO (*(struct drive_info *)0x90080) // 硬盘参数表基址。 #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) // 根文件系统所在设备号。 /* * Yeah, yeah, it's ugly, but I cannot find how to do this correctly * and this seems to work. I anybody has more info on the real-time * clock I'd be interested. Most of this was trial and error, and some * bios-listing reading. Urghh. */ /* * 是啊,是啊,下面这段程序很差劲,但我不知道如何正确地实现,而且好象它还能运行。如果有 * 关于实时时钟更多的资料,那我很感兴趣。这些都是试探出来的,以及看了一些bios 程序,呵! */ #define CMOS_READ(addr) ({ \ // 这段宏读取CMOS 实时时钟信息。 outb_p (0x80 | addr, 0x70); \ // 0x70 是写端口号,0x80|addr 是要读取的CMOS 内存地址。 inb_p (0x71); \ // 0x71 是读端口号。 } ) #define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10) // 将BCD 码转换成数字。 static void time_init (void) // 该子程序取CMOS 时钟,并设置开机时间??startup_time(秒)。 { struct tm time; do { time.tm_sec = CMOS_READ (0); // 参见后面CMOS 内存列表。 time.tm_min = CMOS_READ (2); time.tm_hour = CMOS_READ (4); time.tm_mday = CMOS_READ (7); time.tm_mon = CMOS_READ (8); time.tm_year = CMOS_READ (9); } while (time.tm_sec != CMOS_READ (0)); BCD_TO_BIN (time.tm_sec); BCD_TO_BIN (time.tm_min); BCD_TO_BIN (time.tm_hour); BCD_TO_BIN (time.tm_mday); BCD_TO_BIN (time.tm_mon); BCD_TO_BIN (time.tm_year); time.tm_mon--; startup_time = kernel_mktime (&time); } static long memory_end = 0; // 机器具有的内存(字节数)。 static long buffer_memory_end = 0; // 高速缓冲区末端地址。 static long main_memory_start = 0; // 主内存(将用于分页)开始的位置。 struct drive_info { char dummy[32]; } drive_info; // 用于存放硬盘参数表信息。 void main (void) /* This really IS void, no error here. */ { /* The startup routine assumes (well, ...) this */ /* 这里确实是void,并没错。在startup 程序(head.s)中就是这样假设的。 */ // 参见head.s 程序第136 行开始的几行代码。 /* * Interrupts are still disabled. Do necessary setups, then * enable them */ /* * 此时中断仍被禁止着,做完必要的设置后就将其开启。 */ // 下面这段代码用于保存: // 根设备号 ??ROOT_DEV; 高速缓存末端地址??buffer_memory_end; // 机器内存数??memory_end;主内存开始地址 ??main_memory_start; ROOT_DEV = ORIG_ROOT_DEV; drive_info = DRIVE_INFO; memory_end = (1 << 20) + (EXT_MEM_K < 16 * 1024 * 1024) // 如果内存超过16Mb,则按16Mb 计。 memory_end = 16 * 1024 * 1024; if (memory_end > 12 * 1024 * 1024) // 如果内存>12Mb,则设置缓冲区末端=4Mb buffer_memory_end = 4 * 1024 * 1024; else if (memory_end > 6 * 1024 * 1024) // 否则如果内存>6Mb,则设置缓冲区末端=2Mb buffer_memory_end = 2 * 1024 * 1024; else buffer_memory_end = 1 * 1024 * 1024; // 否则则设置缓冲区末端=1Mb main_memory_start = buffer_memory_end; // 主内存起始位置=缓冲区末端; #ifdef RAMDISK // 如果定义了虚拟盘,则主内存将减少。 main_memory_start += rd_init (main_memory_start, RAMDISK * 1024); #endif // 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,实在看 // 不下去了,就先放一放,看下一个初始化调用 -- 这是经验之谈?。 mem_init (main_memory_start, memory_end); trap_init (); // 陷阱门(硬件中断向量)初始化。(kernel/traps.c,181 行) blk_dev_init (); // 块设备初始化。 (kernel/blk_dev/ll_rw_blk.c,157 行) chr_dev_init (); // 字符设备初始化。 (kernel/chr_dev/tty_io.c,347 行) tty_init (); // tty 初始化。 (kernel/chr_dev/tty_io.c,105 行) time_init (); // 设置开机启动时间??startup_time(见76 行)。 sched_init (); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c,385) buffer_init (buffer_memory_end); // 缓冲管理初始化,建内存链表等。(fs/buffer.c,348) hd_init (); // 硬盘初始化。 (kernel/blk_dev/hd.c,343 行) floppy_init (); // 软驱初始化。 (kernel/blk_dev/floppy.c,457 行) sti (); // 所有初始化工作都做完了,开启中断。 // 下面过程通过在堆栈中设置的参数,利用中断返回指令切换到任务0。 move_to_user_mode (); // 移到用户模式。 (include/asm/system.h,第1 行) if (!fork ()) { /* we count on this going ok */ init (); } /* * NOTE!! For any other task 'pause()' would mean we have to get a * signal to awaken, but task0 is the sole exception (see 'schedule()') * as task 0 gets activated at every idle moment (when no other tasks * can run). For task0 'pause()' just means we go check if some other * task can run, and if not we return here. */ /* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返 * 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任务0 在 * 任何空闲时间里都会被激活(当没有其它任务在运行时),因此对于任务0'pause()'仅意味着 * 我们返回来查看是否有其它任务可以运行,如果没有的话我们就回到这里,一直循环执行'pause()'。 */ for (;;) pause (); } static int printf (const char *fmt, ...) // 产生格式化信息并输出到标准输出设备stdout(1),这里是指屏幕上显示。参数'*fmt'指定输出将 // 采用的格式,参见各种标准C 语言书籍。该子程序正好是vsprintf 如何使用的一个例子。 // 该程序使用vsprintf()将格式化的字符串放入printbuf 缓冲区,然后用write()将缓冲区的内容 // 输出到标准设备(1--stdout)。 { va_list args; int i; va_start (args, fmt); write (1, printbuf, i = vsprintf (printbuf, fmt, args)); va_end (args); return i; } static char *argv_rc[] = { "/bin/sh", NULL}; // 调用执行程序时参数的字符串数组。 static char *envp_rc[] = { "HOME=/", NULL}; // 调用执行程序时的环境字符串数组。 static char *argv[] = { "-/bin/sh", NULL}; // 同上。 static char *envp[] = { "HOME=/usr/root", NULL}; void init (void) { int pid, i; // 读取硬盘参数包括分区表信息并建立虚拟盘和安装根文件系统设备。 // 该函数是在25 行上的宏定义的,对应函数是sys_setup(),在kernel/blk_drv/hd.c,71 行。 setup ((void *) &drive_info); (void) open ("/dev/tty0", O_RDWR, 0); // 用读写访问方式打开设备“/dev/tty0”, // 这里对应终端控制台。 // 返回的句柄号0 -- stdin 标准输入设备。 (void) dup (0); // 复制句柄,产生句柄1 号 -- stdout 标准输出设备。 (void) dup (0); // 复制句柄,产生句柄2 号 -- stderr 标准出错输出设备。 printf ("%d buffers = %d bytes buffer space\n\r", NR_BUFFERS, NR_BUFFERS * BLOCK_SIZE); // 打印缓冲区块数和总字节数,每块1024 字节。 printf ("Free mem: %d bytes\n\r", memory_end - main_memory_start); //空闲内存字节数。 // 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程,fork()将返回0 值, // 对于原(父进程)将返回子进程的进程号。所以180-184 句是子进程执行的内容。该子进程 // 关闭了句柄0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序,所带参数和 // 环境变量分别由argv_rc 和envp_rc 数组给出。参见后面的描述。 if (!(pid = fork ())) { close (0); if (open ("/etc/rc", O_RDONLY, 0)) _exit (1); // 如果打开文件失败,则退出(/lib/_exit.c,10)。 execve ("/bin/sh", argv_rc, envp_rc); // 装入/bin/sh 程序并执行。 _exit (2); // 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。 } // 下面是父进程执行的语句。wait()是等待子进程停止或终止,其返回值应是子进程的进程号(pid)。 // 这三句的作用是父进程等待子进程的结束。&i 是存放返回状态信息的位置。如果wait()返回值不 // 等于子进程号,则继续等待。 if (pid > 0) while (pid != wait (&i)) /* nothing */ ; // 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建一个子进程, // 如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对于所创建的子进程关闭所有 // 以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话并设置进程组号,然后重新打开 // /dev/tty0 作为stdin,并复制成stdout 和stderr。再次执行系统解释程序/bin/sh。但这次执行所 // 选用的参数和环境数组另选了一套(见上面165-167 行)。然后父进程再次运行wait()等待。如果 // 子进程又停止了执行,则在标准输出上显示出错信息“子进程pid 停止了运行,返回码是i”,然后 // 继续重试下去…,形成“大”死循环。 while (1) { if ((pid = fork ()) < 0) { printf ("Fork failed in init\r\n"); continue; } if (!pid) { close (0); close (1); close (2); setsid (); (void) open ("/dev/tty0", O_RDWR, 0); (void) dup (0); (void) dup (0); _exit (execve ("/bin/sh", argv, envp)); } while (1) if (pid == wait (&i)) break; printf ("\n\rchild %d died with code %04x\n\r", pid, i); sync (); } _exit (0); /* NOTE! _exit, not exit() */ }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值