--------------------------------------------------------
实现 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,因为赋值的地址决定了!
运行:
欧克 ...