Linux内核学习笔记之内存分配(九)

    上节我们揭秘了Linux系统移形换影的秘密,这节我们将一起见证Linux系统另一“无中生有的幻术”------内存分配大法~

  • 周流六虚功------空闲内存分配

    内存对于进程就像空气对于我们,提供空闲内存是操作系统最基本的功能之一,如果学过操作系统就应该知道,计算机内存的分配是按内存页来的,每页的大小为4K。进程哪些地方需要申请内存页呢?进程的页目录、页表、栈、堆、代码和数据都是存放在内存页里的,所以进程启动和运行的时候都会申请内存页。下面让我们一起来看下Linux为进程准备的内存页分配函数------get_free_page(注意不管是进程切换还是内存分配,都是进程自己进入内核态来执行的,用户态和内核态就像家和国的概念一样,家的东西是自己的,国家的东西是大家一起的,所以自己动手丰衣足食~)

unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasw\n\t"
	"jne 1f\n\t"
	"movw $1,2(%%edi)\n\t"
	"sall $12,%%ecx\n\t"
	"movl %%ecx,%%edx\n\t"
	"addl %2,%%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;
}

    操作系统分配空闲内存页是非常简单的,首先要介绍下操作系统管理主内存的方式,用一个数组来表示主内存每页的状态,即内存映射位图

static unsigned short mem_map [ PAGING_PAGES ] = {0,};

    其中主内存页数的计算方式就是

#define PAGING_MEMORY (HIGH_MEMORY - LOW_MEM)
#define PAGING_PAGES (PAGING_MEMORY/4096)

    即主内存大小除以页数,未被分配的内存页对应的位图是0,所以我们只要遍历这个数组,找到一个值为0的数组元素,其对应的内存页的物理地址可以使用数组下标*4096+LOW_MEM。

    "0" (0)表示使用前面输出使用的寄存器eax,并将其值设置成0
    "D" (mem_map+PAGING_PAGES-1)表示将内存映射位图的最后一个元素的位置赋值给edi
    "c" (PAGING_PAGES)表示循环次数ecx为页数PAGING_PAGES
    std ; repne ; scasw这段就是将ax和mem_map[N]进行比较,不相等或者没有遍历完所有内存页对应位图就继续循环jne 1f用于判断上面的循环是什么原因结束的,如果相等说明是找到了为0的位图,如果不相等说明是所有位图都被分配了,直接跳到1标签处结束

	"movw $1,2(%%edi)\n\t"

    将mem_map[edi]设置成1表示此页将被分配

	"sall $12,%%ecx\n\t"
	"movl %%ecx,%%edx\n\t"
	"addl %2,%%edx\n\t"

    上面三步完成对应的内存页的物理地址的计算:edx =(数组下标*4096)---ecx + LOW_MEM---%2

	"movl $1024,%%ecx\n\t"
	"leal 4092(%%edx),%%edi\n\t"
	"rep ; stosl\n\t"

    这边完成对内存页的初始化工作,将整页清0

	"movl %%edx,%%eax\n"

    由于最后的输出结果是通过eax传递给寄存器参数__res的,所以要将edx中保存的分配的内存页的起始物理地址赋给eax。到此,就完成了获取内存空闲页的操作,注意这边是内核,逻辑地址==线性地址==物理地址

  • 武当绝技梯云纵------部分加载

    我们还知道在保护模式下,操作系统是支持部分加载技术的,即没必要一次性载入整个程序,只需要载入部分,当程序执行到未载入内存部分的代码的时候,会发生缺页中断,而对应的缺页中断程序会替我们完成剩余的加载动作,这个在操作系统课程中应该都有介绍,现在我们一起来看看这个到底是怎么实现的:

_page_fault:
	xchgl %eax,(%esp)
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%edx
	mov %dx,%ds
	mov %dx,%es
	mov %dx,%fs
	movl %cr2,%edx
	pushl %edx
	pushl %eax
	testl $1,%eax
	jne 1f
	call _do_no_page
	jmp 2f
1:	call _do_wp_page
2:	addl $8,%esp
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret

    这个就是页错误中断服务程序,对应的中断门在trap_init中有设置(忘记的同学可以看下笔记四),我们先看下缺页中断的实现,即call _do_no_page的实现

void do_no_page(unsigned long error_code,unsigned long address)
{
	unsigned long tmp;

	if (tmp=get_free_page())
		if (put_page(tmp,address))
			return;
	do_exit(SIGSEGV);
}

    其中获取空闲页的操作我们前面已经介绍过了,下面只要将进程的逻辑地址和我们刚分配的物理地址tmp绑定就OK了,还记得我在启动部分分页机制的介绍吗?(不记得?你丫的赶紧回到笔记三复习下)只要设置好逻辑地址对应的页目录项和页表项,就完成了页映射,现在我们来看看实际代码是怎么做的,虽然我们在启动部分已经看过了~

unsigned long put_page(unsigned long page,unsigned long address)
{
	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);
	page_table = (unsigned long *) ((address>>20) & 0xffc);
	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] = page | 7;
	return page;
}

    下面将对上面的代码进行庖丁解牛式的解说:

	page_table = (unsigned long *) ((address>>20) & 0xffc);

    ①这个操作就是取对应的页目录项,为什么咋没看到_pg_dir[N]的样子呢?这边linus大神已经给出提示了,我们所有进程都是用同一个页目录的,而这个页目录的起始地址_pg_dir=0x0,所以这个操作可以看出是_pg_dir+(unsigned long *) ((address>>20) & 0xffc),页目录项索引不是10位吗?这边怎么搞12位出来?注意这个是取实际物理地址不是用_pg_dir[N]这种数组索引的方式,0xffc==1111 1111 1100b,1111 1111 11b取出的是索引(前10位),后面两个00b作用其实是用索引乘以每个目录项的大小(4字节)的意思,即_pg_dir[N]==_pg_dir+N*4,如果还没明白那我真就无奈了~

	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;
	}

    ③如果不存在,我们则要新建一个页表,并在页目录中为这个页表设置页目录项,tmp的地址就是页表的基地址,所以这边不用和上面那样再弄一遍了(再次注意内核态三地址合一)

	page_table[(address>>12) & 0x3ff] = page | 7;

    ④现在的page_table已经从页目录切换到页表了,这边根据逻辑地址找到页表中对应的页表项,并将页表项的内容设置成我们获取的内存页的物理地址,注意这是用数组索引的方式,所以这边取的是逻辑地址中间的10位,这样逻辑地址和物理地址的关联就完成了(不过这边好像没有读入磁盘到内存页的操作,在研究进程创建后发现程序在一开始就将磁盘上的内容都读入内存了,感觉这个地方以后应该改进,根据逻辑地址找到对应磁盘上的内容然后读取,这样可以实现部分加载~)

  • 谐之道------写时拷贝技术

    这边要介绍页错误_page_fault中的另一个C处理函数,主要用于处理非法写内存页导致的硬件中断,这部分内容请先看下节进程创建才能更容易理解

void do_wp_page(unsigned long error_code,unsigned long address)
{
	un_wp_page((unsigned long *)
		(((address>>10) & 0xffc) + (0xfffff000 &
		*((unsigned long *) ((address>>20) &0xffc)))));

}
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) {
		*table_entry |= 2;
		return;
	}
	if (!(new_page=get_free_page()))
		do_exit(SIGSEGV);
	if (old_page >= LOW_MEM)
		mem_map[MAP_NR(old_page)]--;
	*table_entry = new_page | 7;
	copy_page(old_page,new_page);
}

    do_wp_page在将逻辑地址转换成物理地址后,调用核心函数un_wp_page完成内存页拷贝,下面我们主要来分析这个核心函数

	old_page = 0xfffff000 & *table_entry;

    我们取出发生页错误所在的内存页的起始物理地址(对页进行写操作时候出发中断的内存页)

	if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
		*table_entry |= 2;
		return;
	}

    先判断下这个内存页的地址是不是主内存区,并且引用次数是1,如果是的话,修改内存页的属性为可写,写时拷贝是发生在对共享页的写操作时候,如果没有共享,即引用计数为1,那么就不需要执行拷贝操作,直接修改对应内存页为可写,然后返回即可。例如fork后,子进程虽然共享父进程的代码页,但是数据页一般是独立的,所以其中一个需要拷贝一份数据页,另一个继续使用原先的数据页即可,即修改其属性为可写

	if (!(new_page=get_free_page()))
		do_exit(SIGSEGV);
	if (old_page >= LOW_MEM)
		mem_map[MAP_NR(old_page)]--;

    如果的确是对共享页写操作触发的页错误,我们要申请一块空闲内存页,如果共享页是主内存的还要减少其引用计数,因为我们后面要拷贝此页的副本到空闲页中,所以此页将不再是共享页,为什么要判断是主内存页?因为第一个子进程是拷贝任务0的,而任务0所有代码、数据和栈都是在内核区,而mem_map记录的只是主内存区的内存页~

	*table_entry = new_page | 7;
	copy_page(old_page,new_page);

    重新设置内存页对应的页表项为新申请的空闲内存页,权限是可读写,然后将原先的页内容拷贝到新页中

    到此为止,我们Linux的高乐积木模块已经讲解完了(内存的写时拷贝技术将在下节进程创建部分详解,即用即学的思想~),接下来就是激动人心的合体讲解了~童鞋别想歪了,是开始搭积木了,下节创建进程篇将为你解开所有疑惑  

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值