linux0.11源码分析-共享页帧

593 阅读3分钟

address表示的是线性地址,linux0.11采用的时二级页表,二级页表的知识在这里

当引发缺页中断时,share_page可能会被do_no_page被调用。

mm/memory.c

static int share_page(unsigned long address)
{
	struct task_struct ** p;
	if (!current->executable)
		return 0;
	if (current->executable->i_count < 2)
		return 0;
        
    // 否则搜索任务数组中所有任务。寻找与当前进程可共享页面的进程,即运行相同的
    // 执行文件的另一个进程,并尝试对指定地址的页面进行共享。如果找到某个进程p,
    // 其executable字段值与当前进程的相同,则调用try_to_share()尝试页面共享。若
    // 共享操作成功,则函数返回1。否则返回0,表示共享页面操作失败.
	for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p)
			continue;
		if (current == *p)
			continue;
        // 如果executable不等,表示运行的不是与当前进程相同的执行文件,因此也继续
        // 寻找。
		if ((*p)->executable != current->executable)
			continue;
		if (try_to_share(address,*p))
			return 1;
	}
	return 0;
}

try_to_share先检查P进程中对应的页帧是否存在,不存在就返回0。然后查看这个页帧是否干净,不干净也返回0,然后查看这个地址有没有在主内存中,到这里已经满足一半的条件。接着检查当前进程对应的页帧是否存在,因为这里是缺页异常,页帧一定不存在,否咋就会出问题,然后申请一个页帧,修改进程P对应页表项的标志位RW为0,表示只读,然后当前进程复制进程p对应页表项数据,刷新页变换高速缓冲,因为该页被共享,引用次数被增加了,所以还要在mem_map对象的页使用次数加1。

参数address表示的是页地址,不含有偏移地址。当前进程共享进程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;

  
	from_page = to_page = ((address>>20) & 0xffc); //获取address在页目录中页目录项PDE的地址
	from_page += ((p->start_code>>20) & 0xffc); // 获取p进程首地址的PDE地址
	to_page += ((current->start_code>>20) & 0xffc); //获取当前进程首地址的PDE地址
    
	from = *(unsigned long *) from_page;  //页表项内容
	if (!(from & 1)) //查看P进程from_page的页是不是存在,不存在,何来共享?直接退出
		return 0;
        
	from &= 0xfffff000;  // 页表项地址
	from_page = from + ((address>>10) & 0xffc); //页表项地址 + 偏移地址 = 物理地址
	phys_addr = *(unsigned long *) from_page; 
    
  
	if ((phys_addr & 0x41) != 0x01)  //phys_addr & 0100 0001 ,0x41对应页表项中的D(dirty)和P(present)标志
		return 0;
	phys_addr &= 0xfffff000;
    
    //它不应该超过机器最大物理地址值,也不应该小于内存低端(1 MB).
	if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
		return 0;
        
	to = *(unsigned long *) to_page; //获取页目录项内容
	if (!(to & 1)) {  // p == 0 ,页不存在
		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; //把R/W标志位置位0,表示该页只读
	*(unsigned long *) to_page = *(unsigned long *) from_page; //复制页表项
    
	invalidate();  		 //页变换高速缓冲
	phys_addr -= LOW_MEM;    // 减去低端内存地址
	phys_addr >>= 12; 	 //转换为页号
	mem_map[phys_addr]++;    //页帧使用次数加1
	return 1;
}
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).
}

get_free_page就是在mem_map数组中找到一个值为0的位置,因为数组的每一项代表主存中的每一页,所以可以根据数组中的位置乘以4KB就可以得到一个相对内存位置,再加上低端内存地址(LOW_MEM)就可以得出物理地址,然后把刚才申请的一页的内存全部初始化为0。把申请的地址返回给调用者。如果申请不到就返回0。

要想看详细的代码分析,就看下面的一行一行分析,你可以选择看或者不看😵

一行一行代码分析:

汇编代码执行前寄存器的状态:

  • register unsigned long __res;定义一个寄存器变量,如果想指定哪个寄存器就可以register unsigned long __res asm("ax");
  • "=a" (__res)表示结果输出到__res,即eax寄存器中。
  • "0" (0) 第一个0表示用上一次的约束,即用eax寄存器,第二个0是把值0给eax"c" (PAGING_PAGES)表示把PAGING_PAGES给ecx "D" (mem_map+PAGING_PAGES-1)表示em_map+PAGING_PAGES-1给edi

开始执行寄存器中的代码:

  • std指令将DF标志置1,DF=0表示正向操作,DF=1表示反向操作。cld指令将DF标志清零,正向操作是指传送操作的方向是从内存区域的低地址端到高地址端,反向操作则正好相反。

  • SCASBSCASWSCASD 指令分别将 AL/AX/EAX 中的值与 EDI 寻址的一个字节 / 字 / 双字进行比较。这些指令可用于在字符串或数组中寻找一个数值。结合 REPE(或 REPZ)前缀,当 ECX > 0 且 AL/AX/EAX 的值等于内存中每个连续的值时,不断扫描字符串或数组。REPNE 前缀也能实现扫描,直到 AL/AX/EAX 与某个内存数值相等或者 ECX = 0。

repne ; scasb:重复执行scasb指令,scasb指令的作用就是把al中的值(现在是0)与es:edi中的值比较,edi中初始化的值是mem_map的最后一项的地址,因为使用了std,每次scasb后,edi都会自动减去1(因为这里用的是scasb,b表示byte),并且修改标志寄存器中的一些标志位。

repne:要想停下来要么是ecx中的循环次数等于0,要么是ZF位等于1。

  • jne 1f向前跳转到标号1处,标号1没有执行代码,直接返回。
  • movb $1,1(%%edi)表示edi + 1的位置给1,
  • sall $12,%%ecx:ecx之前存的是主存总页数(linux0.11最大支持16MB,内存规划是内核、缓冲区、虚拟盘、主存、高端内存),现在是页面数 * 4KB表示主内存的字节数。
  • movl %%ecx,%%edx:主内存的字节数 -> edx
  • movl $1024,%%ecx:1024 -> ecx
  • leal 4092(%%edx),%%edi:由于依照4字节对齐,所以每项占用4字节,取当前物理页最后一项4096 = 4096-4 = 4092,
  • rep ; stosl:stosl指令相当于将EAX中的值保存到ES:EDI指向的地址中,若设置了EFLAGS中的方向位置位(即在stosl指令前使用STD指令)则EDI自减4,否则(使用CLD指令)EDI自增4,从ecx+4092处开始,反方向,步进4,反复1024次,将该物理页1024项所有填入eax寄存器的值(0)。
  • movl %%edx,%%eax:将该物理页面起始地址放入eax寄存器中,用于返回

参考:

英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A

x86 Instruction Set Reference SCAS/SCASB/SCASW/SCASD

x86 Instruction Set Reference REP/REPE/REPZ/REPNE/REPNZ