冬天OS(三十三):内存管理 - fork

--------------------------------------------------------

实现 fork

--------------------------------------------------------

这一节信息量有点大,但是非常有意思 ...

 

·do_fork 函数
PUBLIC int do_fork()
{
	/* find a free slot in proc_table */
	struct proc *p = proc_table;
	int i;
	for (i = 0; i < NR_TASKS + NR_PROCS; i++, p++)
		if (p->p_flags == FREE_SLOT)
			break;

	int child_pid = i; /* 子进程的 PID */
	assert(p == &proc_table[child_pid]);
	assert(child_pid >= NR_TASKS + NR_NATIVE_PROCS);
	if (i == NR_TASKS + NR_PROCS) /* no free slot */
		return -1;
	assert(i < NR_TASKS + NR_PROCS);

	// 复制父进程的进程表项
	int pid = mm_msg.source; /* 父进程 ID */
	u16 child_ldt_sel = p->ldt_sel;
	*p = proc_table[pid];
	p->ldt_sel = child_ldt_sel;
	p->p_parent = pid;											/* 设置父进程 */
	sprintf(p->name, "%s_%d", proc_table[pid].name, child_pid); /* 子进程的名字不一样 */

	/* duplicate the process: T, D & S */
	struct descriptor *ppd;

	/* Text segment */
	ppd = &proc_table[pid].ldts[INDEX_LDT_C];
	/* base of T-seg, in bytes */
	int caller_T_base = reassembly(ppd->base_high, 24,
								   ppd->base_mid, 16,
								   ppd->base_low);
	/* limit of T-seg, in 1 or 4096 bytes,
	   depending on the G bit of descriptor */
	int caller_T_limit = reassembly(0, 0,
									(ppd->limit_high_attr2 & 0xF), 16,
									ppd->limit_low);
	/* size of T-seg, in bytes */
	int caller_T_size = ((caller_T_limit + 1) * /* 判断父进程的 LDT 项的 LIMIT 是以字节为单位还是以4096为单位 */
						 ((ppd->limit_high_attr2 & (DA_LIMIT_4K >> 8)) ? 4096 : 1));

	/* Data & Stack segments */
	ppd = &proc_table[pid].ldts[INDEX_LDT_RW];
	/* base of D&S-seg, in bytes */
	int caller_D_S_base = reassembly(ppd->base_high, 24,
									 ppd->base_mid, 16,
									 ppd->base_low);
	/* limit of D&S-seg, in 1 or 4096 bytes,
	   depending on the G bit of descriptor */
	int caller_D_S_limit = reassembly((ppd->limit_high_attr2 & 0xF), 16,
									  0, 0,
									  ppd->limit_low);
	/* size of D&S-seg, in bytes */
	int caller_D_S_size = ((caller_T_limit + 1) *
						   ((ppd->limit_high_attr2 & (DA_LIMIT_4K >> 8)) ? 4096 : 1));

	/* we don't separate T, D & S segments, so we have: */
	assert((caller_T_base == caller_D_S_base) &&
		   (caller_T_limit == caller_D_S_limit) &&
		   (caller_T_size == caller_D_S_size));

	/* base of child proc, T, D & S segments share the same space,
	   so we allocate memory just once */
	int child_base = alloc_mem(child_pid, caller_T_size);
	/* int child_limit = caller_T_limit; */
	printl("{MM} 0x%x <- 0x%x (0x%x bytes)\n",
		   child_base, caller_T_base, caller_T_size);
	
	// 拷贝父进程的内存给子进程空间
	phys_copy((void *)child_base, (void *)caller_T_base, caller_T_size);

	// 设置子进程的段基址和段限长(以 4K 计)
	init_desc(&p->ldts[INDEX_LDT_C],
			  child_base,
			  (PROC_IMAGE_SIZE_DEFAULT - 1) >> LIMIT_4K_SHIFT,
			  DA_LIMIT_4K | DA_32 | DA_C | PRIVILEGE_USER << 5);
	init_desc(&p->ldts[INDEX_LDT_RW],
			  child_base,
			  (PROC_IMAGE_SIZE_DEFAULT - 1) >> LIMIT_4K_SHIFT,
			  DA_LIMIT_4K | DA_32 | DA_DRW | PRIVILEGE_USER << 5);

	// 给文件系统发消息,让文件系统将 fd_cnt 、i_cnt 都 +1
	MESSAGE msg2fs;
	msg2fs.type = FORK;
	msg2fs.PID = child_pid;
	send_recv(BOTH, TASK_FS, &msg2fs);

	/* child PID will be returned to the parent proc */
	mm_msg.PID = child_pid;// mm_msg 是全局变量

	/* birth of the child */
	MESSAGE m;
	m.type = SYSCALL_RET;
	m.RETVAL = 0;
	m.PID = 0;
	send_recv(SEND, child_pid, &m);

	return 0;
}

 首先我们要复制父进程的进程表给子进程,子进程此时已经在进程表数组中找到了一项,但是在复制的时候要注意,这一点很重要也很关键,那就是 ldt_sel 牵引出来的问题:


u16 child_ldt_sel = p->ldt_sel;
*p = proc_table[pid];
p->ldt_sel = child_ldt_sel; 


这几句揭示了复制之后的子进程如何执行:下次调度的时候加载子进程的 LDT,在子进程上次阻塞的地方继续执行!大了说是这样,细了说就扯远了,我们扯远看看:
 

话说"当年" init 一个fork 调用就打算给 MM 发消息:

PUBLIC int fork()
{
	MESSAGE msg;
	msg.type = FORK;

	send_recv(BOTH, TASK_MM, &msg);

	return msg.PID;
}

然后呢,它带着自己的 process pointer 就进入了 "黑洞洞" 的 INT_VECTOR_SYS_CALL 中断:
 

sendrec:
    push	ebx                 ; .
    push	ecx                 ; > 12 bytes
    push	edx                 ; /

    mov	eax, _NR_sendrec
    mov	ebx, [esp + 12 +  4] ; function
    mov	ecx, [esp + 12 +  8] ; src_dest
    mov	edx, [esp + 12 + 12] ; msg
    int	INT_VECTOR_SYS_CALL

    pop	edx
    pop	ecx
    pop	ebx

int 指令调用之前会将用户进程的 cs、ip、eflags、ss、sp 压栈以准备从中断返回——也就是 int    INT_VECTOR_SYS_CALL 的下一条指令 pop    edx 的地址!


然后进入 sys_call 中断处理函数:
 

sys_call:
    call    save

    sti
    push	esi

    push	dword [p_proc_ready]
    push	edx
    push	ecx
    push	ebx
    call    	[sys_call_table + eax * 4]
    add		esp, 4 * 4

    pop		esi
    mov     	[esi + EAXREG - P_STACKBASE], eax
    cli

call save 指令将用户进程的现场信息保存进用户的 process table ,这很重要,这也是支撑 IPC 消息机制非常重要的手段!


随后就会进入 sys_sendrec 中,执行 msg_send ,如果顺利,那头已经有人等了,那么拿到消息之后唤醒对方就返回,如果没有人在等,就会调用 block 阻塞自己,block 重要的就是将 p_proc_ready 换为了别的进程,于是 msg_send 返回,来到 sys_call 执行中断返回,想要切换到下一个用户进程并阻塞当前的用户进程,就必须使用到 p_proc_ready ,而 sys_call 所在的中断处理框架,恰恰使用到了:
 

restart:
    mov	esp, [p_proc_ready]
    lldt	[esp + P_LDT_SEL]
    lea	eax, [esp + P_STACKTOP]
    mov	dword [tss + TSS3_S_SP0], eax
restart_reenter:
    dec	dword [k_reenter]
    pop	gs
    pop	fs
    pop	es
    pop	ds
    popad
    add	esp, 4
    iretd

因为 sys_call 只会由用户发起,因此 INT_VECTOR_SYS_CALL 号中断绝不会是中断重入的,所以一定会从 restart 执行,至于刚进入 INT_VECTOR_SYS_CALL 中断前的那条 pop     edx ,则是不会被执行了,并且因为设置了 flags 的 send 标志,下次时钟切换也不会考虑此进程了,当下次此进程被唤醒时,时钟中断就会从此进程的进程表中获取 cs、ip、eflags、ss、sp 以及现场信息 ,而 ip 保存的就是那条被抛弃了的 pop edx 指令的地址,所以,阻塞、唤醒并能继续执行的完美实现还是依赖于这个中断处理框架!
 

话题绕远了,我们还是来看看 ldt_sel 的事情:


调度程序如果想切换到一个进程,那么需要将此进程的 LDT 加载进 LDT 寄存器,LDT 中的就是进程的数据段和指令段,然后根据 IP 将可以执行这个进程(SP还是次要的,但是必要的)!因此我们初始化的时候(fork 以前),就设置好了所有进程的 LDT在GDT、LDT 在 GDT 中的选择子,在 fork 的时候,只要对进程的LDT赋上正确的值就可以了!


这是 ldt_sel 涉及的东西,再后面的为新进程分配运行空间的事情都是水到渠成的!
接下来很重要的是这么几句:
 

// 给文件系统发消息,让文件系统将 fd_cnt 、i_cnt 都 +1
MESSAGE msg2fs;
msg2fs.type = FORK;
msg2fs.PID = child_pid;
send_recv(BOTH, TASK_FS, &msg2fs);

/* child PID will be returned to the parent proc */
mm_msg.PID = child_pid;// mm_msg 是全局变量

/* birth of the child */
MESSAGE m;
m.type = SYSCALL_RET;
m.RETVAL = 0;
m.PID = 0;
send_recv(SEND, child_pid, &m);

因为子进程是复制的,所以子进程具备所有父进程的状态,父进程向 MM send 了之后就 recv 阻塞在等待 MM 的 send,也就是上面我们分析的 pop edx 那里,同样,子进程的进程表和父进程相同,因此子进程也阻塞在 pop edx 那里(子进程将执行 pop edx 指令,但是因为继承了父进程的 recv 阻塞状态,所以没法得到执行),虽然 pop edx 指令被移动了(移动到了子进程的地址空间),但我们修改了子进程的 LDT 为子进程的地址空间,所以子进程阻塞在自己进程空间的 pop edx 是成立的!


所以形成了这么一种情况:子进程和父进程都在等待 MM 的 send 而唤醒自己!


MM 确实是 send 了,如上面的代码,MM 唤醒父进程是在 MM 的正常消息反馈情况下进行的,而用额外的 send 唤醒子进程!并且在这过程中,构造了父进程和子进程 fork 函数不同的返回值,如果父进程和子进程不对 fork 的返回值进行区分,那么它们除了运行的位置不同之外,其余都相同,但是,子进程和父进程通过判断它们自己的(是的,此时 fork 是私有财产) fork 函数的返回值,可以知道自己是创造者还是被创造者!


额外需要注意的一点是,此时 IDT、GDT 等数据结构使用的还是真正内核的 IDT、GDT,因为赋值的地址决定了!

 

运行:

欧克 ...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sssnial-jz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值