进程 0 把需要弄的弄好之后,立马就 fork 出了进程 1,这篇博客主要学习 fork 这个系统调用的详细过程及进程 1 生成后的主要工作。
进入中断
main.c
里调用的 fork()
实际是一个定义在 include/unistd.h
里面的宏。
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
展开后发现是根据name
的编号放到 eax 作为输入,进到int $0x80
中断。中断保存现场将 SS, ESP, EFLAGS, CS, EIP
依次压栈,接着特权级变为 0,进到内核态。根据 IDT 表找到中断处理函数,也就是 kernel/system_call.s
里面的 _system_call:
这里执行。等到执行结束后,会将 eax 的值作为 fork()
的返回值。
sys_fork()
_system_call
会将接下来执行 copy_process
需要的参数压栈,之后 call _sys_call_table
, 这条指令本身也会压栈, 接下来会找到 sys_fork()
这个函数去执行
获取进程号
sys_fork()
首先会执行 _find_empty_process
,作用是到 task[64]
这个数组里面找到一个空闲的编号,并对 last_pid
这个全局变量进行自加
copy_process
重点在于接下来调用的 copy_process
函数,这是整个操作的重点。sys_fork()
会再次向栈里面压一些寄存器的值
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
申请内核栈
copy_process
会首先调用 get_free_page()
,为进程 1 申请一个内存页面,用于进程 1 的内核栈,以及存放一个进程最重要的数据结构 task_struct
.
申请新页面成功后,会将进程0 的 task_struct 原封不动的拷贝给进程 1,然后会进行一些修改,比如将进程状态设为休眠,因为进程 1 还没弄好,不能先让它就绪。接下来对进程 1 的 TSS
进行赋值,用到了栈里面已经压了一堆的参数。
设置内存管理
接下来是设置进程 1 的分页管理。调用 copy_mem()
这个函数,设置子进程的段基址和段限长,接下来调用mm/memory.c
里面的 copy_page_tables()
设置子进程的页表。
首先申请一个新的页面作为页目录表,然后将父进程的页表项复制进来。此时父子进程指向相同的页表。最后刷新高速缓存。
设置文件相关项和GDT
接下来回到copy_process
里面,会将父进程打开的文件、当前工作目录等值复制给子进程的 task_struct
里面的相关的值。
然后
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
这两句将 GDT 中与该子进程相关的项设置好。
进程 0 返回
接下来子进程就设置完了,可以设置为就绪态了。copy_process
会将 last_pid
作为返回值返回。按照之前压栈的顺序,这些调用的函数依次出栈,最终一句 iret
特权级回到了 3 用户态,进程 0 得到了 last_pid
作为返回值。
if (!fork()) { /* we count on this going ok */
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;) pause();
之后进程 0 进到了死循环里面,并进到了 pause 系统调用中,开始了进程的调度。
进程 1 开始工作
调度到进程 1
之前进程 0 触发了 sys_pause 系统调用,其中会调用 kernel/sched.h
中的 schedule()
函数进行系统进程调度。
该函数会遍历所有进程,找到处于就绪态并且进程的 counter 最大,切换到这个进程。通过一个 switch_to
的宏,系统用一个 ljmp
指令恢复了进程 1 保存的 TSS 现场,进到进程 1 开始执行。
由于进程 1 的 TSS 保存的 EIP 寄存器对应的代码在 if (__res >= 0)
这一句,代码里面写死了 eax 是 0,同时进程 1 与进程 0 用的是同一个代码段,因此返回到了 main 函数中继续执行 fork()
之后的代码, 返回值为 0.
此时, fork()
的调用在父子进程中都结束了,开始执行用户代码中子进程的逻辑部分, init()
init()
TO BE CONTINUED