linux 0.11 学习(3)——进程1

进程 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