2023-06-11  阅读(377)
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_10946851.html

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

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

1. 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);
        ...
    }

在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发生调度时,当前进程的处理器现场。

2. 子进程开始执行

子进程时如何开始执行呢?由上面代码,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寄存器中保存的内核线程回调函数。这个章节在后面进程上下文中单独学习。

3. 进程的终止

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

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

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

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

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

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

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

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

4. 僵尸进程

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

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

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

阅读全文
  • 点赞