进程生命周期浅析

本文详细解读了内核如何实现进程的创建,通过__do_fork和copy_process函数,以及execve系统调用的运作。涵盖了进程状态转换、vfork特性和退出流程,深入剖析了Linux内核中关键进程管理技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程生命周期浅析

一、前言

本章节讲述内核对于进程的实现,包括进程的创建、状态变更和进程的销毁。对于进程的运行(即调度),已在其他章节中分析,这里不再进行赘述。

二、进程创建

内核中与进程创建相关的系统调用包括forkvforkcloneunshareexec等。

  • fork:该系统调用没有参数,其创建个子进程,并采用写时复制技术,拷贝一份父进程的页表并标记为只读,当尝试对页表里的页进行写入时,将会触发拷贝动作(如果该页的使用数为1,则说明只有一个进程在使用,直接将其设置为可写)。
  • vfork:与fork类似,不同的是该系统调用不复制页表,为了节省开销。该系统调用专门为那些fork后立马调用exec执行新程序的场景打造。需要注意,该系统调用将会使得父进程挂起,直到子进程调用exec或者退出。在此之前,子进程将共享父进程的内存以及栈。
  • clone:这是一种更细粒度的创建进程(线程)的方法,根据参数的不同达到的目的也不同,比如pthread_create创建线程就是用的这个,forkvfork都是clone的一种特殊实例。
  • unshare:设置当前进程的命名空间,使其独立出来(如网络命名空间等)。
  • execve:运行一个可执行程序(或者脚本),使其替换当前进程,包括代码段、数据段等。

2.1 __do_fork

上面的forkclonevfork都是调用内核函数__do_fork来实现的,因此该函数可以说是进程的起源,下面我们来重点分析该函数的实现。

long _do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr,
	      unsigned long tls)
{
	struct completion vfork;
	struct pid *pid;
	struct task_struct *p;
	int trace = 0;
	long nr;

	/*
	 * 检查要报告给ptrace的事件。这里可以看出,当设置了CLONE_UNTRACED时将不会被跟踪。
	 */
	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if ((clone_flags & CSIGNAL) != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;
		
        //检查当前进程是否被ptrace
		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}

    //核心功能,拷贝当前进程,产生一个新的进程(或者线程)。
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
	add_latent_entropy();

	if (IS_ERR(p))
		return PTR_ERR(p);

	/*
	 * tracepoint的跟踪点。
	 */
	trace_sched_process_fork(current, p);

	pid = get_task_pid(p, PIDTYPE_PID);
    //获取进程的局部进程号,即当前进程命名空间的进程号
	nr = pid_vnr(pid);

    //将新创建的进程的进程号传递给用户态
	if (clone_flags & CLONE_PARENT_SETTID)
		put_user(nr, parent_tidptr);

    /* 如果是vfork,那么初始化vfork变量并将其设置到vfork_done上。当vfork完成后,该
     * completion会被触发来通知父进程(即当前进程)。
     */
	if (clone_flags & CLONE_VFORK) {
		p->vfork_done = &vfork;
		init_completion(&vfork);
		get_task_struct(p);
	}

    //唤醒新创建的进程p
	wake_up_new_task(p);

	/* forking complete and child started to run, tell ptracer */
	if (unlikely(trace))
		ptrace_event_pid(trace, pid);

    //如果是vfork,那么进入睡眠等待p完成vfork过程
	if (clone_flags & CLONE_VFORK) {
		if (!wait_for_vfork_done(p, &vfork))
			ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
	}

	put_pid(pid);
	return nr;
}

2.2 copy_process

乍一看,do_fork好像也没有做啥,那是因为所有的工作都被交给了copy_process()来进行。该函数通过用户设置的flags会执行不同的进程拷贝操作。这个函数可以说是内核里最为复杂的函数之一,其长度高达500行,这里就简要分析。

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)
{
	/**
	 * stack_start和stack_size分别是用户态传递进来的用作新的进程的栈的地址和
	 * 大小。通过这种方式,允许新进程在创建好后运行特定的代码,比如用户态可以
	 * 对栈里面的内容做修改,指定新进程的入口函数,这也是c库里面的标准用法:
	 * 
	 *        int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
         *        * pid_t *parent_tid, void *tls, pid_t *child_tid* );
	 * 
	 * 实际上,stack_size只有创建内核进程的时候才会有用,用户进程没有使用到。
	 * 
	 * 可以看出来,C库对clone系统调用进行了封装,其中fn为新进程要运行的函数,
	 * arg为要传递给该函数的参数,其他地方与原始clone相同。这也是clone与fork
	 * 最大的区别:fork创建的新进程只能继续运行当前代码(调用fork后面的代码)
	 * 
	 * 在不指定stack的情况下,内核会默认做内存拷贝。可以看出,如果不指定stack,
	 * 那么是不能指定CLONE_VM的,不然会出现两个进程共用一个栈的情况,引起混乱。
	 **/
	int retval;
	struct task_struct *p;
	struct multiprocess_signals delayed;

	...

	/* 初始化调度相关的参数,将任务状态改为TASK_NEW,并将其添加到当前CPU */
	retval = sched_fork(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_policy;

	/* perf初始化 */
	retval = perf_event_init_task(p);
	if (retval)
		goto bad_fork_cleanup_policy;
	retval = audit_alloc(p);
	if (retval)
		goto bad_fork_cleanup_perf;
	/* 共享内存,初始化变量 */
	shm_init_task(p);
	retval = security_task_alloc(p, clone_flags);
	if (retval)
		goto bad_fork_cleanup_audit;
	/* 如果设置了CLONE_SYSVSEM标志,那么子进程与当前进程共享信号量列表。 */
	retval = copy_semundo(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_security;
	/* 根据是否设置了CLONE_FILES来判断是否拷贝当前进程的打开文件列表(默认拷贝)。
	 * 如果设置了,那么将共享打开文件列表。 */
	retval = copy_files(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_semundo;
	/* 如果设置了CLONE_FS,那么父子进程将共享文件系统(包括根、当前工作目录);
	 * 否则,会做一份当前fs_struct的拷贝
	 */
	retval = copy_fs(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_files;

	/* 注意:如果设置了CLONE_THREAD,那么CLONE_SIGHAND和CLONE_VM同样会被设置。
	 * 这是合理的,因为就我们所知,线程组内的所有线程共享内存和信号。
	 */

	/* 如果设置了CLONE_SIGHAND,那么共享信号处理程序;否则,做一份拷贝。 */
	retval = copy_sighand(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_fs;
	/* 如果设置了CLONE_THREAD(线程),那么父子共享信号;否则做一份拷贝。 */
	retval = copy_signal(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_sighand;
	/* 如果设置了CLONE_VM,那么父子共享内存;否则,做一份页表的拷贝。 */
	retval = copy_mm(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_signal;
	/* 检查是否存在以下标志:
	 * CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
			      CLONE_NEWPID | CLONE_NEWNET |
			      CLONE_NEWCGROUP
	 * 存在的话,则为其创建对应的命名空间;否则,直接使用当前进程的命名空间。
	 */
	retval = copy_namespaces(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_mm;
	/* 通过是否设置CLONE_IO来决定是否共享块设备IO上下文。 */
	retval = copy_io(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_namespaces;
	retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
	if (retval)
		goto bad_fork_cleanup_io;

	if (pid != &init_struct_pid) {
		/* 为新进程分配pid */
		pid = alloc_pid(p->nsproxy->pid_ns_for_children);
		if (IS_ERR(pid)) {
			retval = PTR_ERR(pid);
			goto bad_fork_cleanup_thread;
		}
	}

#ifdef CONFIG_BLOCK
	p->plug = NULL;
#endif
#ifdef CONFIG_FUTEX
	p->robust_list = NULL;
#ifdef CONFIG_COMPAT
	p->compat_robust_list = NULL;
#endif
	INIT_LIST_HEAD(&p->pi_state_list);
	p->pi_state_cache = NULL;
#endif
	/*
	 * 如果设置了CLONE_VM且没有设置CLONE_VFORK,那么初始化sas_ss。
	 */
	if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
		sas_ss_reset(p);

	/*
	 * 禁用新进程的trace以及单步调试等功能。
	 */
	user_disable_single_step(p);
	clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
	clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
	clear_all_latency_tracing(p);

	/* 设置pid */
	p->pid = pid_nr(pid);
	if (clone_flags & CLONE_THREAD) {
		p->exit_signal = -1;
		p->group_leader = current->group_leader;
		/* 设置线程组pid,即当前进程的pid */
		p->tgid = current->tgid;
	} else {
		/* 如果设置了CLONE_PARENT,那么新进程的父进程将会设置为当前进程的父进程;
		 * 否则(默认情况下)父进程会设置为当前进程。
		 */

		/* 这里可以看出,线程的退出信号为-1,进程(或者是线程组的leader)的退出
		 * 信号为clone_flags,可以通过退出信号是否小于0来判断其是否是进程。
		 */
		if (clone_flags & CLONE_PARENT)
			p->exit_signal = current->group_leader->exit_signal;
		else
			p->exit_signal = (clone_flags & CSIGNAL);
		/* 将线程组的leader设置成自己 */
		p->group_leader = p;
		p->tgid = p->pid;
	}

	p->nr_dirtied = 0;
	p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
	p->dirty_paused_when = 0;

	p->pdeath_signal = 0;
	INIT_LIST_HEAD(&p->thread_group);
	p->task_works = NULL;

	cgroup_threadgroup_change_begin(current);
	/*
	 * 分别调用每个cgroup子系统的can_fork回调函数来简单当前是否可以进行fork
	 */
	retval = cgroup_can_fork(p);
	if (retval)
		goto bad_fork_free_pid;

	/*
	 * 下面要获取tasklist_lock锁了,在此之前该进程对于用户是不可见的。
	 * 禁止在此之前用户与该进程的通信。
	 */

	p->start_time = ktime_get_ns();
	p->real_start_time = ktime_get_boot_ns();

	/*
	 * 获取tasklist_lock锁,来将其变得对于其他子系统可见。
	 */
	write_lock_irq(&tasklist_lock);

	/* 正确设置进程的父进程。 */
	if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
		p->real_parent = current->real_parent;
		p->parent_exec_id = current->parent_exec_id;
	} else {
		p->real_parent = current;
		p->parent_exec_id = current->self_exec_id;
	}

	klp_copy_process(p);

	spin_lock(&current->sighand->siglock);

	/*
	 * 拷贝seccomp,一个安全相关的机制,能够限制进程使用哪些系统调用。
	 */
	copy_seccomp(p);

	rseq_fork(p, clone_flags);

	/* 如果进程的命名空间即将销毁,那么不容许再创建进程 */
	if (unlikely(!(ns_of_pid(pid)->pid_allocated & PIDNS_ADDING))) {
		retval = -ENOMEM;
		goto bad_fork_cancel_cgroup;
	}

	/* 再次检查当前进程是否收到KILL信号,允许在fork中途被打断。 */
	if (fatal_signal_pending(current)) {
		retval = -EINTR;
		goto bad_fork_cancel_cgroup;
	}

	/* 初始化pid_links。进程会通过该数组与其他进程产生各种关联。 */
	init_task_pid_links(p);
	if (likely(p->pid)) {
		/* 初始化该进程的ptrace功能。 */
		ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

		/* 下面的代码会设置进程与其他进程的各种关联。 */

		init_task_pid(p, PIDTYPE_PID, pid);
		if (thread_group_leader(p)) {
			/* 如果是线程组的leader(即p是进程),运行下面的初始化。 */

			/* 将线程组pid设置成自己 */
			init_task_pid(p, PIDTYPE_TGID, pid);
			/* 设置p的进程组和会话为当前进程的进程组和会话 */
			init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
			init_task_pid(p, PIDTYPE_SID, task_session(current));

			/* 如果p是其进程命名空间的第一个进程,那么就将其设置为
			 * child_reaper(孩子收割者?)并且不可被kill掉
			 */
			if (is_child_reaper(pid)) {
				ns_of_pid(pid)->child_reaper = p;
				p->signal->flags |= SIGNAL_UNKILLABLE;
			}
			/* 设置延迟的信号 */
			p->signal->shared_pending.signal = delayed.signal;
			/* 设置p的伪终端为当前进程的伪终端 */
			p->signal->tty = tty_kref_get(current->signal->tty);
			/*
			 * Inherit has_child_subreaper flag under the same
			 * tasklist_lock with adding child to the process tree
			 * for propagate_has_child_subreaper optimization.
			 */
			p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||
							 p->real_parent->signal->is_child_subreaper;
			/* 将p添加到父进程的链表以及init进程的链表。
			 * 整个过程都是在tasklist_lock受保护期间进行的。
			 */
			list_add_tail(&p->sibling, &p->real_parent->children);
			list_add_tail_rcu(&p->tasks, &init_task.tasks);
			attach_pid(p, PIDTYPE_TGID);
			attach_pid(p, PIDTYPE_PGID);
			attach_pid(p, PIDTYPE_SID);
			__this_cpu_inc(process_counts);
		} else {
			/* 如果是线程的话,处理比较简单,因为基本上都是与当前进程共享的。 */
			current->signal->nr_threads++;
			atomic_inc(&current->signal->live);
			atomic_inc(&current->signal->sigcnt);
			task_join_group_stop(p);
			list_add_tail_rcu(&p->thread_group,
					  &p->group_leader->thread_group);
			list_add_tail_rcu(&p->thread_node,
					  &p->signal->thread_head);
		}
		attach_pid(p, PIDTYPE_PID);
		nr_threads++;
	}
	total_forks++;
	hlist_del_init(&delayed.node);
	spin_unlock(&current->sighand->siglock);
	syscall_tracepoint_update(p);
	write_unlock_irq(&tasklist_lock);

	/* 到这里,tasklist_lock解开了,进程已经用户可见了。 */

	/* 初始化进程的proc接口 */
	proc_fork_connector(p);
	/* 将p添加到父进程的cgroup中 */
	cgroup_post_fork(p);
	/* 通知当前进程cgroup的变化 */
	cgroup_threadgroup_change_end(current);
	perf_event_fork(p);

	trace_task_newtask(p, clone_flags);
	/* 初始化uprobe相关的内容 */
	uprobe_copy_process(p, clone_flags);

	return p;
    ...
}

2.3 execve

上面的do_fork只是创建了个进程(线程),默认情况下新创建的进程会直接运行父进程的代码(当前代码或者父进程指定的地址),直到使用了execve系统调用运行了新的可执行程序来接管当前进程,这也是所有的可执行程序运行的过程具体实现代码如下:

static int __do_execve_file(int fd, struct filename *filename,
			    struct user_arg_ptr argv,
			    struct user_arg_ptr envp,
			    int flags, struct file *file)
{
	/**
	 * fd为目录,这个在filename为相对路径的时候才会有作用,默认情况下
	 * 是当前进程的工作目录。
	 * 
	 * argv为传递给可执行程序的启动参数
	 * 
	 * envp为环境变量。
	 */
	char *pathbuf = NULL;
	struct linux_binprm *bprm;
	struct files_struct *displaced;
	int retval;

	if (IS_ERR(filename))
		return PTR_ERR(filename);

	/*
	 * 检查当前用户的进程数量是否超标
	 */
	if ((current->flags & PF_NPROC_EXCEEDED) &&
	    atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {
		retval = -EAGAIN;
		goto out_ret;
	}

	current->flags &= ~PF_NPROC_EXCEEDED;

	/* 这个方法会对共享的files_struct进行解耦:如果当前进程的files_struct与
	 * 别的进程共享,那么做一份拷贝,否则不做处理。
	 * 
	 * 拷贝的时候,会把当前打开文件列表也拷贝进去,displaced存放的是原来
	 * 老的files_struct
	 * 
	 * 可以看出,只有创建进程时指定了CLONE_FILES标志才会需要解耦。
	 */
	retval = unshare_files(&displaced);
	if (retval)
		goto out_ret;

	retval = -ENOMEM;
	/* 分配一个binprm,该结构体用于存储运行可执行程序期间的参数。 */
	bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
	if (!bprm)
		goto out_files;

	/* 为可执行程序分配一个credentials */
	retval = prepare_bprm_creds(bprm);
	if (retval)
		goto out_free;


	check_unsafe_exec(bprm);
	current->in_execve = 1;

	/* 打开可执行程序 */
	if (!file)
		file = do_open_execat(fd, filename, flags);
	retval = PTR_ERR(file);
	if (IS_ERR(file))
		goto out_unmark;

	sched_exec();

	bprm->file = file;
	/* 获取可执行程序的文件名(绝对路径) */
	if (!filename) {
		bprm->filename = "none";
	} else if (fd == AT_FDCWD || filename->name[0] == '/') {
		bprm->filename = filename->name;
	} else {
		if (filename->name[0] == '\0')
			pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);
		else
			pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s",
					    fd, filename->name);
		if (!pathbuf) {
			retval = -ENOMEM;
			goto out_unmark;
		}
		/*
		 * 如果fd设置了O_CLOEXEC,那么执行可执行程序后将不可见该目录,
		 * 将其记录下来(还没搞懂)。
		 */
		if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
			bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
		bprm->filename = pathbuf;
	}
	bprm->interp = bprm->filename;

	/* 为可执行程序分配一个mm,同时分配一个用于表示栈的vma */
	retval = bprm_mm_init(bprm);
	if (retval)
		goto out_unmark;

	/* 检查用户参数数量上限有没有超标 */
	bprm->argc = count(argv, MAX_ARG_STRINGS);
	if ((retval = bprm->argc) < 0)
		goto out;

	bprm->envc = count(envp, MAX_ARG_STRINGS);
	if ((retval = bprm->envc) < 0)
		goto out;

	/* 初始化以及文件的权限、 读取文件的前128个字节到bprm的buf*/
	retval = prepare_binprm(bprm);
	if (retval < 0)
		goto out;

	/* 将文件名拷贝进来 */
	retval = copy_strings_kernel(1, &bprm->filename, bprm);
	if (retval < 0)
		goto out;

	bprm->exec = bprm->p;
	/* 将环境变量拷贝进来 */
	retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out;

	/* 将用户参数拷贝进来 */
	retval = copy_strings(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out;

	would_dump(bprm, bprm->file);

	/**
	 * 开始执行文件,该函数会查找该文件格式所对应的处理钩子来执行。
	 * 基本上后面的工作都是在这里完成,比如mm的切换(用户态栈的切换)
	 * 等。由于mm的变动不影响内核栈,所以当前上限文代码的执行不会
	 * 受影响。
	 * 
	 * 这个里面会将当前进程的mm替换成上面新创建的mm,并将内核栈底部的
	 * 存放用户态返回地址的寄存器值改成新的mm里的用户态栈,从而进程
	 * 在返回用户态后就可以直接运行新的程序了。
	 */
	retval = exec_binprm(bprm);
	if (retval < 0)
		goto out;

	/* 成功了,下面做一些清理工作。 */
	current->fs->in_exec = 0;
	current->in_execve = 0;
	membarrier_execve(current);
	rseq_execve(current);
	acct_update_integrals(current);
	task_numa_free(current);
	free_bprm(bprm);
	kfree(pathbuf);
	if (filename)
		putname(filename);
	if (displaced)
		put_files_struct(displaced);
	return retval;
    
    ......
}

三、进程退出

进程在退出的时候,无论是正常的结束任务(通过exit()系统调用),还是收到异常信号,最终都会调用do_exit()内核函数。用于进程退出的系统调用有两个:exitexit_group,后者不仅结束当前线程,更会结束当前线程组内的所有线程,也就是结束当前进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值