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

进程的创建——fork函数的返回值为0上一章学习了进程的创建,在用户空间可以使用fork接口来创建一个用户进程,或者使用clone接口来创建一个用户线程,它们在内核空间都会调用do_

上一章学习了进程的创建,在用户空间可以使用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内核视频教程文档资料免费领取后台私信【内核】自行获取。

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

在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进程就成为子进程的父进程。

今天的文章进程的创建——fork函数的返回值为0分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/7814.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注