基于X86架构的OS内核设计之杂记(四)

创建子进程fork与执行新程序execve时页表复制的问题

在os内核中,我通过fork系统调用创建新进程,在fork调用中,创建了新进程的tcb结构和内核态堆栈,通过mm_copy为新进程创建了独立的页目录和页表,这些页目录和页表映射到父进程相同的物理页上。

/**
 * the one just the return address for sys_call0_for()
 * int do_fork(int nr,unsigned long stack_start)
 */
int do_fork(unsigned long stack_start)
{
	struct task_struct *p;
 	struct regs *childreg;
	int pid;

	/**
	 * 分配大小为KERNEL_STACK_PAGES的空间作为内核栈,并在栈底建立TCB结构
	 * 新的TCP结构复制了父进程几乎所有的内容,除了EAX, 内核栈指针ESP
	 * 将父进程的堆栈空间设置为只读状态,以便触发COW机制.
	 * 为子进程创建独立的页目录和页表,并映射到和父进程相同的物理地址.
	 * 实现父子进程共享页表.
	 */
	
	p = (struct task_struct *)PA2VA(get_free_pages(KERNEL_STACK_PAGES));
	*p = *current;
	pid = get_last_pid();

	printk("task[%d] do_fork(): new struct_tcb[%d]=0x%08x, \n", current->pid, pid, (uint32)p);
	
	p->pid    = pid;
	p->state  = TASK_INTERRUPTABLE;
	p->delay  = 0;
	p->u_time = 0;
	p->k_time = 0;
	p->last_fd = 0;//current->last_fd;
	for ( int i = 0; i < FSAL_MAX_OPENED_FILE; i++){
		p->file[i] = current->file[i];
	}
	/* alloc memory block to new task kernel stack */
	p->thread.esp0 = (uint32)p + KERNEL_STACK_PAGES*PAGE_SIZE -4;
	p->thread.ss0 = SELECTOR_KERNEL_DATA;
	p->thread.eip = (unsigned long)ret_from_fork;
	p->thread.ss  = SELECTOR_USER_DATA;	
	p->thread.trace_bitmap =  0x80000000;
	
	/* copy current task kernel stack to new task kernel stack */
	childreg = (struct regs *)((p->thread.esp0) - (sizeof(struct regs)));
	reg_copy(childreg, (void*)(stack_start+4), sizeof(struct regs));

	// child process diffrent with parent
	p->thread.esp = (unsigned long)childreg;
	childreg->eax = 0;		// for child process !!!!!!
	p->parent = current;
	p->mm.stack_start = current->mm.stack_start; // 和父进程使用相同地址的栈空间.
 
	// 将父进程用户栈设置为只读,并将页面计数+1,当父子进程任何一个进行写入操作时,将page_default,从而重新分配页面.
	printk("task[%d] user_stack[0x%08x] set to wp[0x%02x]\n",current->pid, current->mm.stack_start->vaddr, P_P|P_US);	
 	for ( int i = 0; i < USER_STACK_PAGES; i++ )
	{
		unsigned long vaddr = current->mm.stack_start->vaddr+i*PAGE_SIZE;
		if (page_attrs_set(current->pg_dir,vaddr,P_P|P_US) == -1 )
		{
			kernel_die(" user'space [0x%08x] wp failed\n", vaddr);
		}else{
			printk(" user'space [0x%08x] to wp 0x%02x\n", vaddr, P_P|P_US);	
		}	
		#if 1
		if ( vaddr >= low_memory_start ){
			mem_map[MAP_NR(vaddr)]++;
		}
		#endif
 	} 

	/* 为子进程分配页目录,并复制父进程的页表项 */
	p->pg_dir = get_free_page();
	if ( !p->pg_dir ){
		kernel_die("fork() get_free_page failed\n");
	}
	mm_copy(current, p);

通过mm_copy来复制父进程的页表

void mm_copy(const struct task_struct *old, struct task_struct *new)
{
	uint32 *pgd_old, *pgd_new, page_copied = 0;

	pgd_old = (uint32 *)old->pg_dir;
	pgd_new = (uint32 *)new->pg_dir;

	printk("mm_copy() parent[%d] pgd %08x to child[%d] pgd %08x\n", old->pid, (uint32)pgd_old,new->pid,(uint32)pgd_new);
	
	for ( int i = 0; i < 1024; i++ )  // copy 4MB kernel space page table
	{
		pgd_new[i] = pgd_old[i];	
		if ( pgd_new[i] != 0x0 )  // 二级页表
		{
			uint32 new_page;
			new_page = get_free_page();
			if ( new_page == 0x0 ){
				kernel_die("mm_copy() no page\n");
			}
			printk("copy %d[0x%08x],0x%08x to 0x%08x\n", i, pgd_old[i], pgd_old[i]&0xFFFFF000, new_page);		
			memcpy( (char*)new_page, (char*)(pgd_old[i] & 0xFFFFF000), PAGE_SIZE );
			
			uint32 *pte = (uint32 *)new_page;
			for ( int k = 0; k < 1024; k++ ){
				if ( pte[k] & 0x1 ){
					page_copied++;		
				}
			}
		}
	}
	
	printk("mm_copy(): %d page(s) done\n", page_copied);	
}

通过mm_copy之后,父子进程拥有自己独立的页目录和页表,但是这些页表的页表项都是相同的,即它们映射的物理地址是相同的,并且这些页面目前来说都是只读状态,为后面的写时复制作了铺垫。最后将新进程加入调度列表:

	p->state  = TASK_RUNNING;
	task[pid] = p;
	
	return pid;

新进程创建出来后,先关闭父进程继承过来的文件描述符,并重新打开tty(目前这些操作只是一个空的系统调用,进入内核之后又原路返回,没有实际作用,仅仅是一个形式)然后执行execve调用,载入新进程sh的镜像,而父进程则通过wait等待子进程运行结束:

	for (;;)
	{
		if ((pid=fork()) < 0)
		{
			printf("init: fork() failed\n\r");
			continue;
		}
		if (!pid) {
			close(0);close(1);close(2);
			(void)open("/dev/tty0",O_RDWR,0);
			(void)dup(0);
			(void)dup(0);
		    _exit(execve("/bin/sh")); // exit if execve failed
		}
		while(1){
			if (pid==wait(&i)){
				break;
			}
		}
	}

execve实现如下:

int syscall_execve( uint32 *esp, uint32 *eip,const int8 *file_name)
 {
	// 进程在fork()时就已经具备独立的内核栈与用户栈(页表独立,实际的物理页还是共享父进程的)
	// 调用do_execve()时将两个堆栈指针恢复到栈顶.
	// 在调用execve时,压栈的地址为返回地址即EIP,当进入_syscall_execve后,EIP在
	// 内核栈的偏移位置为56字节.因此在_syscall_execve中,通过指令lea EIP(%esp),%eax 也即lea 56(%esp),%eax
	// 将返回地址存放在%eax寄存器中,然后将其压入syscall_execve()的栈帧内,得到的参数便是eip
	// 通过指针eip可以直接修改内核栈上对应EIP位置上的值.
	// 我们正是通过修改内核栈保存的EIP值达到切换系统调用返回后,直接执行新进程指令的目的.
	// 使用新程序的入口entry填入eip
	
	// 从RAM_DISK内存段中载入新进程的代码.先这样做吧,后续实现文件系统以后,改为从文件系统中读取可执行文件.
	// 此处新进程由loader加载在主内存临时区8M附近.

	// 改为COW 2019.07.11

	#if 1
	if ( !strncmp(file_name,"/bin/sh",16)){
		current->exe_inode.i_size = PAGE_SIZE * 2;
		current->exe_inode.i_start = 0x0;
	}else if (!strncmp(file_name,"/bin/ls",16)){
		current->exe_inode.i_size = PAGE_SIZE * 1;
		current->exe_inode.i_start = 0x4000;
	}else{
		kernel_die("task[%d] execve file not found %s\n", current->pid, file_name);
		return (-1);
	}
	#endif

	#if 1
	printk("task[%d] execve(): file_name=%s, inode_start: 0x%08x, size: 0x%08x\n",
			current->pid,
			file_name,current->exe_inode.i_start,
			current->exe_inode.i_size);
	#endif
	
	// 清空'用户空间'code、data、stack及其他mmap的页面映射关系 
	// 内核空间映射关系保留、一级也二级页表,它们在fork()时已经建立好.
	// 如此一来,该子进程的进程空间实际上的物理页面已全部释放,与父进程完全没有关系了.
	// 缺页中断触发时,只需申请物理页面,填到对应的表项即可重新完成页面映射关系.
	mm_free(current->pg_dir);
	
	*eip = 0x02000000;   // 新进程的入口点,所有由execve加载的二进制程序入口点统一
	*esp = 0x08000000/*+PAGE_SIZE*/-4; // 堆栈
	
	printk("task[%d] execve call ready\n", current->pid);
	
	return (0);
}

若子进程执行execve失败返回了,那么它将调用_exit(),在_exit()中将自己移出调度队列,向父进程发送一个信号,唤醒父进程,最后执行shedule()函数,让出CPU使用权(此时子进程就成了传说中的僵尸进程):

int do_exit(int exit_code)
{
	struct task_struct *p;
	p = current->parent;
	
	current->state = TASK_STOP;
	current->exit_code = exit_code;
	if (!p){
		p = task[0];
	}
	printk("send signal to parent [%d]\n", p->pid);
	p->signal |= (0x1<<0);	// signal to parent task.
	if ( p->state == TASK_WAIT ){
		p->state = TASK_RUNNING;
	}
if_return:
		schedule();
	goto if_return;
	
	return (0);
}

int syscall_exit ( int exit_code )
{
	return do_exit(exit_code);		
}

之后父进程被唤醒,在wait中找到子进程,并释放它的资源(收尸),页表,堆栈, 最后父进程继续进行下一轮fork()调用,周而复始。

在此过程中, mm_free()是关键,fork()中已经为子进程建立了独立的一级、二级页表结构,并且这些页表项与父进程相同,即共享父进程的物理页面。execve执行的是新的二进制文件,子进程实际上的.text,.data.bss、堆栈都需要不同于父进程的物理页面来存放,因此我先把子进程页表的用户空间映射关系全部解除,标记为页面不存在,达到子进程不再共享父进程的物理页面的目的。随后子进程sh执行时,由于缺页异常触发COW机制从磁盘(RAM_DISK)加载真正的物理页面。

void mm_free(uint32 pg_dir )
{
	uint32 *p_pg_dir, *pte;
	uint32 page_freed = 0;

	p_pg_dir = (uint32*)(pg_dir);
	
	uint32 cr3;
	__asm__ __volatile__("movl %%cr3, %%eax":"=g"(cr3)::"memory");
	printk("cr3=0x%08x\n", cr3);

	printk("mm_free(): free task user's space pg_dir=0x%08x\n", p_pg_dir);
	
	for ( int i = 0; i < 768; i++ )
	{	
		if ( !(p_pg_dir[i] & 0x1) ){	// 全局页目录项不为空,则存在页表
			continue;
		}
		printk("  p_pg_dir[%d] 0x%08x\n",i,p_pg_dir[i] & 0xFFFFF000);
		pte = (uint32*)(p_pg_dir[i] & 0xFFFFF000);
	    
		for ( int k = 0; k < 1024; k++ )
		{ 
			// 若页面存在,页表项设置为不存在标志,表示该物理页面不存在,触发缺页default.
			// 在fork()阶段,已经为进程分配了独立的页目录项和页表项,这些已分配的页目录和页表项
			// 指向了父进程的物理页,且堆栈标记为不可写.
			if ( pte[k] & 0x1){        // 页表项存在
				page_freed++;
			//	printk("pte[%d]=0x%08x\n", k,pte[k]);
			}
			pte[k] =  0;
		}
	}
	
	printk("mm_free(): %d page(s) done\n", page_freed);
}

但是这个流程出现了一点问题,就是mm_free()解除用户空间的页表映射关系时,导致bochs CPU重启,并报错:

physical address not available

而且页表是在释放的过程中,前768项页目录仅仅释放了2项时就报错,而且我释放的是用户空间的地址,此时执行的代码是处于内核态,使用的是内核空间,内核空间的256项页目录项并没有释放,不明白为什么会报错。为了验证释放的页目录地址是否正确,mm_free()还读取了cr3寄存器的值,也确定了是当前进程的tcb->pg_dir。 看来对页表的理解还是不够透彻。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值