进程生命周期浅析
一、前言
本章节讲述内核对于进程的实现,包括进程的创建、状态变更和进程的销毁。对于进程的运行(即调度),已在其他章节中分析,这里不再进行赘述。
二、进程创建
内核中与进程创建相关的系统调用包括fork
、vfork
、clone
、unshare
、exec
等。
fork
:该系统调用没有参数,其创建个子进程,并采用写时复制技术,拷贝一份父进程的页表并标记为只读,当尝试对页表里的页进行写入时,将会触发拷贝动作(如果该页的使用数为1,则说明只有一个进程在使用,直接将其设置为可写)。vfork
:与fork
类似,不同的是该系统调用不复制页表,为了节省开销。该系统调用专门为那些fork
后立马调用exec
执行新程序的场景打造。需要注意,该系统调用将会使得父进程挂起,直到子进程调用exec
或者退出。在此之前,子进程将共享父进程的内存以及栈。clone
:这是一种更细粒度的创建进程(线程)的方法,根据参数的不同达到的目的也不同,比如pthread_create
创建线程就是用的这个,fork
和vfork
都是clone
的一种特殊实例。unshare
:设置当前进程的命名空间,使其独立出来(如网络命名空间等)。execve
:运行一个可执行程序(或者脚本),使其替换当前进程,包括代码段、数据段等。
2.1 __do_fork
上面的fork
、clone
和vfork
都是调用内核函数__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(¤t->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(¤t->signal->live);
atomic_inc(¤t->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(¤t->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(¤t_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()
内核函数。用于进程退出的系统调用有两个:exit
和exit_group
,后者不仅结束当前线程,更会结束当前线程组内的所有线程,也就是结束当前进程。