Linux 内核解读之内存管理----memory.c

转载请注明原文出处http://blog.csdn.net/lizhiliang06/article/details/8655115

80x86体系结构中,Linux内核的内存管理程序使用分页管理方式。利用页目录和页表结构处理内核中其他部分代码对内存申请和释放操作。Memory.s是linux内存管理的核心,80x86内存管理最大寻址范围是4G的内存空间,在最大1024*1024*4KB的范围内寻址,内存管理分为3级寻址,第一级是目录,第二集是页表,第三级是页内偏移地址,它们在内存的分布关系如下图:

Linux 内核解读之内存管理----memory.c_第1张图片

线性地址转换如下图所示:

Linux 内核解读之内存管理----memory.c_第2张图片

1,这篇文章里面涉及到了页面出错异常处理。

2,写时复制机制。

3,需求加载机制。

具体代码详解如下:


#include 					//信号头文件,涉及到信号的结构和操作函数
#include 				//系统头文件,设置修改描述符、中断门等
#include 			//调度头文件,定义了任务结构,和调度函数
#include 				//head头文件, 定义了段描述符结构等
#include 			//内核头文件,定义了一些内核常用函数	

volatile void do_exit(long code);	//退出处理函数

static inline volatile void oom(void) //没有内存空间了
{
	printk("out of memory\n\r");
	do_exit(SIGSEGV);
}

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

/* these are not to be changed without changing head.s etc */
#define LOW_MEM 0x100000				//定义内存低端(1MB)
#define PAGING_MEMORY (15*1024*1024)	//定义 15MB作为主内存即分页内存
#define PAGING_PAGES (PAGING_MEMORY>>12) //分页后的主内存页数(15MB/4KB)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)	//定义内存地址映射为页号
#define USED 100

//判断地址是否在当前进程代码段中
#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \ 
current->start_code + current->end_code)

//存放物理内存最高端地址
static long HIGH_MEMORY = 0;

//复制1页内存
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si")

//内存映射字节图,可以知道每个页表被同时使用的次数
static unsigned char mem_map [ PAGING_PAGES ] = {0,};

/*
 * Get physical address of first (actually last :-) free page, and mark it
 * used. If no free pages left, return 0.
 */
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasb\n\t"
	"jne 1f\n\t"
	"movb $1,1(%%edi)\n\t"
	"sall $12,%%ecx\n\t"
	"addl %2,%%ecx\n\t"
	"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)
	:"di","cx","dx");
return __res;
}

/*
 * Free a page of memory at physical address 'addr'. Used by
 * 'free_page_tables()'
 */
void free_page(unsigned long addr)//释放一页内存,从物理地址addr开始的一页面内存开始释放,
{
	if (addr < LOW_MEM) return;	//如果地址小于最低内存低端,则返回
	if (addr >= HIGH_MEMORY)	//如果地址大于高端,着提示错误
		panic("trying to free nonexistent page");
	addr -= LOW_MEM;//减去低端的地址,右移12位即除以4*1024,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)    //0x4fffff是4MB,这里需要是4M的地址为界,否则报错
		panic("free_page_tables called with wrong alignment");

	if(!from)
		panic("Trying to free upswapper memory space");

	size = (size + 0x3fffff) >> 22;//即把size+(4M - 1)/ 4M得出所占页表数,记得每个页表有1024项,每项对于一页的内存4KB
	//注意:上面的size是得到的目录号,我们知道每一项目录号占4个字节,即每个地址是以4增量间隔递增,由于页目录在head.s里面
	//载入内存时从物理地址的0开始,所以实际的页目录号地址指针为:目录项号<<2,所以也是from>>20,与0xffc确保目录项指针范围有效
	//0xffc = 0b111111111100
	dir = (unsigned long *) ((from>>20) & 0xffc);
	for(; size-- > 0; dir++){ // size 为被释放目录数,dir为目录项指针
		if(!(1 & *dir))		  //*dir值与1,判断如果目录项无效即P位为0则继续,P位表示是否存在
			continue;
		pa_table = (unsigned long *) (0xfffff000 & *dir);//计算出目录的页表地址
		for(nr = 0; nr<1024; nr++){	//每个页表有1024个页		
			if(1&*pg_table)			//如果页表项P位为1,则释放内存
				free_page(0xfffff000 & *pg_table);

			*pa_table = 0;          //该页表项内容清零
			pg_table++;				//下一页
		}
		free_page(0xfffff000 & *dir);//这里是想是否页表所在的内存,这里的内存是1024*4B<1M;所以这里什么也没有做
		*dir = 0;	//清零页表目录项
	}
	invalidate();	//刷新页变换高速缓冲区
	return 0;
}

int copy_page_tables(unsigned long from, unsigned long to, long size)
{
			unsigned long *from_page_tables;
			unsigned long *to_page_tables;
			unsigned long this_page;
			unsigned long *from_dir, *to_dir;
			unsigned long nr;

			//源地址和目的地址都需要在4M内存边界上。否则出错
			if((from&0x3fffff) || (to&0x3fffff))
				panic("copy_page_tables called with wrong alignment");
			//计算出源地址和目标地址的目录指针,都是内存的物理地址
			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)
							panic("copy_page_tables: already exist");	
					from_page_table = (unsigned long *)(0xfffff000 & *from_dir);
					//拿到一块空闲页内存,get_free_page()返回0时表示失败,失败返回-1
					if(!(to_page_table = (unsigned long *) get_free_page()))
						return -1;
					//页目录表置7,二进制为0b111表示Usr,R/W,Present
					*to_dir = ((unsigned long) to_page_table) | 7;
					//from =0 则仅需要复制头160页
					nr = (from==0)?0xA0:1024;
					for( ; nr-- > 0; from_page_table++, to_page_table++){
							this_page = *from_page_table; //取源页表项内容
							if (!(1 & this_page)) //如果当前源页面没有被使用过,那么就不需要复制,继续
										continue;
							//复位页表项,R/W置0,表示用户层只能读不能写页面
							this_page &= ~2;
							*to_page_table = this_page;//将该页表项复制到目的页表里面
							if(this_page > LOW_MEM) {
										*from_page_table = this_page;
										this_page -= LOW_MEM; //减去低内存端
										this_page >>= 12;	//除以1024*4,得到内存所在页目录数
										mem_map[this_page]++;	//内存页映射值+1							
							}	
					}
			}

			invalidate();			//刷新页变换高速缓冲
			return 0;
}


unsigned long put_page(unsigned long page, unsigned long address)
{
			unsigned long tmp, *page_table;
			/*page地址在合法地址主存范围内*/	
			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);
			//计算出指定地址所在的页目录指针位置
			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;
				//标志对应的页目录项为(Use,U/S,R/W)
				*page_table = tmp|7;
				page_table = (unsigned long*) tmp;
			}
			//在页表中设置指定地址的页表
			page_table[(address>>12) & 0x3ff] = page | 7;
			
			return page;
}

//取消页面写保护
//table_entry 为页表项的指针
void un_wp_page(unsigned long *table_entry)
{
		unsigned long old_page, new_page;

		old_page =0xfffff000 & *table_entry; //取页的物理地址
		//如果地址大于1M而且这个页面还没有被共享,则设置该页面的R/W为可写,刷新变换高速缓冲 
		if(old_page >= LOW_MEM && mem_map[MAP_NR(old_page)] == 1){
				*table_entry |= 2;
				invalidate();
				return;
		}

		//申请空闲页失败,则提示out of memory 
		if(!(new_page = get_free_page()))
			oom();
		//如果原页面被共享了,则减一
		if(old_page >= LOW_MEM)
			mem_map[MAP_NR(old_page)]--;
		//并设置为(U/S, R/W, P)
		*table_entry = new_page | 7;
		invalidate();
		//复制页面到新的页面
		copy_page(old_page, new_page);
}

/*
 *当用户试图在一个已经共享的页面上写是,需要复制到一个新的地址,并递减原页面
 *error_code 是由CPU产生,address作为页面地址
 */
void do_wp_page(unsigned long error_code, unsigned long address)
{

//	((address>>10) & 0xffc)address>>10 = address/1024 获得以4K个字节为递增的页表项偏移地址
//  *((unsigned long*)((address>>20) & 0xffc )) 获得目录项,
//  (0xfffff000 & *((unsigned long*)((address>>20) & 0xffc )))并由页目录项得到页表中地址
//  2个相加得到页表项指针
	un_wp_page((unsigned long *) (((address>>10) & 0xffc) + (0xfffff000 & *((unsigned long*)((address>>20) & 0xffc )))));

}

//写页面验证函数
/*
 *address 为传入的地址
 */
void write_verify(unsigned long address)
{
		unsigned long page;
		if(!((page = *((unsigned long *)((address>>20) & 0xffc)))&1))//获取地址所在的目录项,查看P位是不是存在
			return ;
		
		page &= 0xfffff000;//获取页表地址,
		page += ((address>>10) & 0xffc);//并且获取页表中的偏移量
		
		//如果页面不可写,则执行共享检验,复制页面操作
		if((3 & *(unsigned long *) page) == 1)
			un_wp_page((unsigned long *) page);

		return ;
}

/*申请空闲内存块*/
void get_empty_page(unsigned long address)
{
		unsigned long tmp;
		if(!(tmp=get_free_page()) || !put_page(tmp, address))
		{		
			free_page(tmp);
			oom();
		}

}
/*
 *try_to_share 在任务"p"中检查地址address的页面,看是否存在,是否干净
 *如果干净,这分享
 *注意:我们已经假设p != 当前任务,并且它们共同享有一个执行程序
 *
 */
static int try_to_shart(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;
		
		//求指定内存地址的页目录项
		from_page = to_page = ((address>>20) & 0xffc);
		//得出当前p代码开始的目录地址
		from_page += ((p->start_code>>20) & 0xffc); 
		//计算当前的代码起始地址的目录项
		to_page	  += ((current->start_code>>20) & 0xffc);

		//检查from地址处是否存在有效的页目录
		from = *(unsigned long*) from_page;
		if(!(from & 1))
			return 0;
		//获取页目录处的页表起始地址
		from &= 0xfffff000;
		//2项相加计算from_page的页表项地址
		from_page = from + ((address>>10) & 0xffc);
		//得出物理地址
		phys_addr = *(unsigned long *) from_page;
		//判断页表项的Dirty和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); //算出对应页表的地址
			if(1 & *(unsigned long *) to_page) //对比相应的页是否已经存在
				panic("try_to_share: to_page already exists");
					
			//对应的页,至上写保护标志
			*(unsigned long *) from_page &= ~2;
			*(unsigned long *) to_page = *(unsigned long *) from_page;
			
			//刷高速缓冲区
			invalidate();
			phys_addr -= LOW_MEM;
			phys_addr >>= 12;
			mem_map[phys_addr]++;//标志该页的内存被引用加一次
			return 1;
}

/*
 *分享页
 */
static int share_page(unsigned long address)
{
			struct task_struct **p;
			//如果是不可以执行的,则返回。excutable是执行进程节点	
			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;
					//如果2个executable不相等,继续
					if((*p)->executable != current->executable)
						continue;
					//尝试共享页
					if(try_to_share(address, *p))
						return 1;			
			}
			return 0;

}

//页异常中断处理调用函数,在页缺失的情况下处理
//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为空,或者超出代码与数据长度,则申请一页物理内存
			//start_code 是进程代码段地址, end_data是代码加数据长度
			if(!current->executable || tmp >= current->end_data){
					get_empty_page(address);
					return;		
			}

			//共享成功,退出	
			if(share_page(tmp))
				return;
			//申请空闲页
			if(!(page = get_free_page()))
				oom();
			//计算出缺页所在的数据块,一页内存有4块,4KB=4*block
			block = 1 + tmp/BLOCK_SIZE;
			//根据i节点信息,取数据块在设备上对应的逻辑块
			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;
			}
				
			if (put_page(page,address))
				return;

			free_page(page);
			oom();
}

void mem_init(long start_mem, long end_mem)
{
		int i;

		HIGH_MEMORY = end_mem;
		for(i = 0; i>= 12;
		
		while(end_mem-- > 0)
			mem_map[i++] = 0;
}


void calc_mem(void)
{
		int i, j, k, free = 0;
		long *pg_tbl;

		for(i=0; i转载请注明原文出处http://blog.csdn.net/lizhiliang06/article/details/8655115


你可能感兴趣的:(Linux内核之究竟)