leevis.com icon indicating copy to clipboard operation
leevis.com copied to clipboard

Linux-0.11 fork系统调用分析

Open vislee opened this issue 3 years ago • 0 comments

概述

fork系统调用产生一个子进程,返回2次。父进程返回子进程的进程ID,子进程则返回0. 出错则返回-1. 例如:nginx调用fork创建worker进程。

    switch (pid) {

    case -1:
        return NGX_INVALID_PID;

    case 0:
        proc(cycle, data);  // worker进程执行函数
        break;

    default:     // 父进程
        break;
    }
    ...

fork内核代码分析

int find_empty_process(void)
{
	int i;

	repeat:
		if ((++last_pid)<0) last_pid=1;  // 32位整形 最大2**31,超过最大值从1重新开始。
		for(i=0 ; i<NR_TASKS ; i++)
			if (task[i] && task[i]->pid == last_pid) goto repeat;  // 找运行进程最大的一个值作为新进程的pid。
	for(i=1 ; i<NR_TASKS ; i++)  // 进程数组中,找一个空位,返回空位的下标。
		if (!task[i])
			return i;
	return -EAGAIN;   // 没有找到空槽,则返回一个负数。
}
_sys_fork:
	call _find_empty_process  # 任务数组中找一个空位,eax是空位下标,如果没找到返回负数。
	testl %eax,%eax                # 查看 eax内容是 0, <0, >0
	js 1f                                    # <0 则跳到1处。  也就是没有找到空槽则返回
	push %gs
	pushl %esi
	pushl %edi
	pushl %ebp
	pushl %eax                       # eax内容是调用 find_empty_process 的返回值,也就是找到的空槽下标
	call _copy_process           # 复制进程结构
	addl $20,%esp                 # 恢复堆栈
1:	ret                                     # 此时 eax已经是调用 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)
{
	struct task_struct *p;
	int i;
	struct file *f;

	p = (struct task_struct *) get_free_page();  // 找一页空闲物理内存
	if (!p)
		return -EAGAIN;  // 如果没有空闲内存则返回负数。最终被赋给了errno。
	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;  // 申请了一页(4k)物理内存,低地址保存了task_struct,剩余的都是内核态栈
	p->tss.ss0 = 0x10;  // 内核数据段选择子 0x10=0b0001 0000。 index=0b00010 TI=0b0 RPL=0b00  GDT表第3项。 对于linux0.11内核,内核数据段,段限长足够长可以寻址整个物理内存,包括主内存。
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;               // eax做为函数调用的返回值,当新进程被调度执行时,
                                                // 该值会被cpu加载到eax寄存器,因此子进程返回0.
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) { // 复制页表
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)
		if (f=p->filp[I])     // 继承父进程的打开文件数加1
			f->f_count++;
	if (current->pwd)        // 进程工作目录,i节点引用数加1
		current->pwd->i_count++;
	if (current->root)        // 进程根目录,i节点引用数加1
		current->root->i_count++;
	if (current->executable)   // 执行文件i节点引用数加1
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case */
	return last_pid;       // 返回子进程的id
}

总结

用户进程调用fork后,通过int80中断调用到内核的sys_fork函数。 该函数首先获取一个任务空槽,然后申请一页物理内存,保存子进程任务结构体,复制父进程结构体,把子进程状态改为不可中断的等待状态,防止被提前调度。然后对子进程结构进行修改。修改完后将新的子进程的状态改为就绪,等待调度。

vislee avatar Nov 20 '21 16:11 vislee