龙空技术网

进程的创建——fork函数的返回值为0

嵌入式Linux内核 170

前言:

现在看官们对“linux进程的创建fork”可能比较珍视,同学们都需要分析一些“linux进程的创建fork”的相关资讯。那么小编在网摘上汇集了一些对于“linux进程的创建fork””的相关内容,希望同学们能喜欢,你们快快来了解一下吧!

上一章学习了进程的创建,在用户空间可以使用fork接口来创建一个用户进程,或者使用clone接口来创建一个用户线程,它们在内核空间都会调用do_fork函数来实现,但是我们有两个疑问未得到解答。

fork接口,它可以是父、子进程都会返回,那么它会返回两次,其中父进程的返回值是子进程的PID,而子进程返回0,这个过程是如何的呢?子进程第一次返回用户空间时,它的返回在哪里呢?进程如何完成终止一,fork的执行过程

当调用_do_fork()函数创建子进程后,子进程会加入到内核的调度器中,在调度器中参与调度。那么子进程在稍后的某一时刻得到调度和执行,因此fork函数也会有两次返回,一次是父进程的返回,另外一次是子进程被调度后执行的返回。

我们以copy_process为例,下面是在里面有一个copy_thread的线程:

static __latent_entropy struct task_struct *copy_process(					unsigned long clone_flags,					unsigned long stack_start,					unsigned long stack_size,					int __user *child_tidptr,					struct pid *pid,					int trace,					unsigned long tls,					int node){    ...    retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);    ...}

更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取。

在Linux4.2后增加了CONFIG_HAVE_COPY_THREAD_TLS宏和copy_thread_tls函数,这个函数使一个特定于体系结构的函数,用于复制进程中特定的线程的数据,重要的是填充task_struct->thread的各个成员,其对于ARM64的定义如下:

int copy_thread(unsigned long clone_flags, unsigned long stack_start,		unsigned long stk_sz, struct task_struct *p){	struct pt_regs *childregs = task_pt_regs(p);	memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));	fpsimd_flush_task_state(p);	//创建子进程时用户进程的情况	if (likely(!(p->flags & PF_KTHREAD))) {        //将当前寄存器信息复制给子进程		*childregs = *current_pt_regs();		childregs->regs[0] = 0;//子进程 X0寄存器 0,因此fork 在子进程返回0  		*task_user_tls(p) = read_sysreg(tpidr_el0);		if (stack_start) {			if (is_compat_thread(task_thread_info(p)))				childregs->compat_sp = stack_start;			else				childregs->sp = stack_start;		//创建线程时设置用户栈起始地址		}		if (clone_flags & CLONE_SETTLS)			p->thread.tp_value = childregs->regs[3];	} else {//处理子进程是内核线程的情况		memset(childregs, 0, sizeof(struct pt_regs));		childregs->pstate = PSR_MODE_EL1h;//设置子进程的处理器状态为   PSR_MODE_EL1h ,异常等级为el1使用sp_el1             		if (IS_ENABLED(CONFIG_ARM64_UAO) &&		    cpus_have_cap(ARM64_HAS_UAO))			childregs->pstate |= PSR_UAO_BIT;		p->thread.cpu_context.x19 = stack_start;//设置内核线程执行函数地址		p->thread.cpu_context.x20 = stk_sz;//设置传递给函数的参数  	}    //设置子进程的进程硬件上下文中的PC和SP成员的值	p->thread.cpu_context.pc = (unsigned long)ret_from_fork;	p->thread.cpu_context.sp = (unsigned long)childregs;	ptrace_hw_copy_thread(p);	return 0;}

childregs->regs[0] = 0子进程被调度返回用户空间的时候,fork的返回值为0,这就是为何fork返回值为0表示是子进程的原因

如果创建的是子进程,那么就直接和父进程写时复制方式共享用户栈,而栈不需要在进行设置,直接使用父进程的

进程切换时,子进程的pc和sp,当子进程第一次被调度的时候,从ret_from_fork开始执行指令,栈指针指向childregs,即为设置后pt_regs

在copy_thread函数中会复制父进程struct pt_regs栈的全部内容到子进程,包括描述内核栈上保持的寄存器的全部信息,如X0-X30寄存器,栈指针寄存器,PC寄存器以及PSTATE寄存器信息等。同时还会修改子进程X0的值,该值在返回用户空间时子进程的返回值就是该值。

由此可见,copy_thread这个函数对于进程调度很重要,决定了进程第一次被调度的时候执行哪个代码,决定了fork函数的返回值。pt_regs描述的发生异常的时候保存的现场信息,主要是一些通用寄存器,我们这里称为异常现场:

struct pt_regs {	union {		struct user_pt_regs user_regs;		struct {			u64 regs[31];			u64 sp;			u64 pc;			u64 pstate;		};	};	u64 orig_x0;	u64 syscallno;	u64 orig_addr_limit;	u64 unused;	// maintain 16 byte alignment};

当异常发生,异常的现场,通用寄存器的内容,如X0-X30,sp,pc,pstate会被压入内核栈,通过pt_reg结构来描述。

当异常处理结束时候,需要恢复异常前的现场,会将这些保持的值恢复到通用寄存器中

pu_context描述的是进程调度的时候需要保存的进程上下文,我们这里成为调度现场:

struct cpu_context {	unsigned long x19;	unsigned long x20;	unsigned long x21;	unsigned long x22;	unsigned long x23;	unsigned long x24;	unsigned long x25;	unsigned long x26;	unsigned long x27;	unsigned long x28;	unsigned long fp;	unsigned long sp;	unsigned long pc;};

当进程切换的时候,会将处理器当前需要保持的寄存器保存到前一个进程的tsk的thread.cpu_context中,并将后一个即将要调度的进程的上下文信息从该tsk的thread.cpu_context中恢复到相应的寄存器中,就完成了处理器状态的切换。

所以对该过程pt_regs表明发生异常时,处理器现场,而cpu_context发生调度时,当前进程的处理器现场。

二,子进程开始执行

子进程时如何开始执行呢?由上面代码,copy_thread函数会使子进程的入口地址PC指向ret_from_fork,该过程主要是通过子进程硬件上下文中PC成员来实现,那么子进程执行就会跳转到该汇编函数中

/* * This is how we return from a fork. */ENTRY(ret_from_fork)	bl	schedule_tail	cbz	x19, 1f				// not a kernel thread	mov	x0, x20				//赋值内核线程的参数	blr	x19					//执行内核线程函数1:	get_thread_info tsk	b	ret_to_user			//返回用户空间ENDPROC(ret_from_fork)

在第2行中,判断X19寄存器的值是否为空,如果为空,说明这是一个用户进程,则跳转到第5行代码中,调用ret_to_user汇编函数,直接返回用户空间。如果X19寄存器的值不为空,说明这是一个内核线程,直接执行X19寄存器中保存的内核线程回调函数。这个章节在后面进程上下文中单独学习。

三,进程的终止

系统有源源不断的进程的诞生,同时,也会有进程不断的终止。进程的终止有两种方式

资源的终止,包括调用exit系统调用或者从某个程序的主函数返回被动地收到终止信号或者异常终止

进程主动终止主要有以下两种途径:

从main函数返回,链接程序会自动添加exit()系统调用主动调用exit()系统调用

进程被动终止主要有以下途径:

进程收到一个自己不能处理的信号进程在内核态执行时发生了一个异常进程收到SIGKILL等终止信号

当一个进程终止时,Linux内核会释放所占用的所有资源,并把这个消息告诉给父进程,而一个进程终止时可能又有以下情况

它先于父进程终止,那么子进程会变成僵尸进程,直到父进程调用wait()才能最终消亡它也在父进程之后终止,那么Init进程将成为子进程的新父进程四,僵尸进程

一个进程通过exit()系统调用终止之后,就会处于僵尸状态。在僵尸状态中,除了进程描述符依然保留外,进程的其他资源已经归还给内核。

Linux内核这么做是为了让系统可以得到子进程的终止原因,父进程可以通过wait()系统调用来获取已终结的子进程的信息之后,内核才会释放子进程的task_strcut数据结构。

但是如果父进程先于子进程消亡,那么子进程就变成孤儿进程。Linux内核会把它托孤给init进程(1号进程),init进程就成为子进程的父进程。

标签: #linux进程的创建fork