目标
在《进程入门01》中主要是从概念和理论上来描述进程,本章从fork()
入手,从内核代码角度去观察一个进程的创建过程,更接地气的去感受真实世界里进程的具体形态。
本文代码是基于linux-2.6.34
版本。
系统调用
为了解释fork()
系统调用从用户态发起到内核态响应的过程,简单概述一下Linux中系统调用的概念和原理。
系统调用是内核向用户态程序提供的服务接口。用户态进程通过系统调用可以申请内核中一些资源和服务。内核提供系统调用的目的主要是为了隔离资源操作权限、保护系统稳定性。
系统调用是通过软中断(中断号为0x80
)来实现的,内核提前将所有系统调用的服务(函数)进行编号,如fork()
对应的系统调用号为57
。用户态程序使用系统调用时,根据接口约定指定好系统调用编号,设置好相关的参数,然后触发0x80
中断陷入内核,中断响应处理函数根据系统调用编号来调用相应的服务例程代码,等处理函数返回后,用户态程序按约定获取返回值就可以了。
注:通过
0x80
中断(int 0x80
)触发调用内核服务的方式已经过时了,现在使用syscall/sysret
指令(x86_64)和sysenter/sysexit
指令(x86_32)来调用内核的函数,速度更快。
x86_64的Linux内核,系统调用编号在<asm/unistd_64.h>
文件中定义:
...
#define __NR_clone 56
#define __NR_fork 57
#define __NR_vfork 58
#define __NR_execve 59
#define __NR_exit 60
#define __NR_wait4 61
#define __NR_kill 62
...
内核为每个系统调用都设置了相应的处理入口函数。sys_fork()
就是用来处理__NR_fork
系统调用的入口函数。
sys_fork()
sys_fork()
函数在<asm/syscalls.h>
文件中声明,在kernel/process.c
中实现。
int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}
do_fork()
完成了大部分创建的工作,在kernel/fork.c
文件中实现。do_fork()
函数主要调用copy_process()
完成进程复制,并唤醒新的子进程让其投入运行。
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
//......
//创建子进程数据结构,并复制父进程数据
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
trace_sched_process_fork(current, p);
nr = task_pid_vnr(p);
audit_finish_fork(p);
tracehook_report_clone(regs, clone_flags, nr, p);
/*
* We set PF_STARTING at creation in case tracing wants to
* use this to distinguish a fully live task from one that
* hasn't gotten to tracehook_report_clone() yet. Now we
* clear it and set the child going.
*/
p->flags &= ~PF_STARTING;
//唤醒新的子进程
wake_up_new_task(p, clone_flags);
tracehook_report_clone_complete(trace, regs,
clone_flags, nr, p);
} else {
nr = PTR_ERR(p);
}
return nr;
}
复制父进程数据
copy_process()
函数主要工作如下:
- 调用
dup_task_struct()
函数分配新的内核栈、thread_info结构和task_struct描述符,并复制父进程描述符和thread_info结构数据。 - 检查并确保新创建的子进程后,进程数量没有超过资源的限制。
- 初始化子进程描述符,与父进程区别分来。
- 调用
sched_fork()
函数,设置与调度相关的信息,将进程状态设置为TASK_WAKING
。 - 调用
copy_flags()
函数更新子进程task_struct的flags字段。 - 调用
alloc_pid()
函数为新的子进程分配PID - 根据
clone_flags
参数标志,复制或共享父进程的文件描述符、文件系统、信号处理函数、地址空间、命名空间等。 - 最后,返回子进程的task_struct指针。
/*
* This creates a new process as a copy of the old one,
* but does not actually start it yet.
*
* It copies the registers, and all the appropriate
* parts of the process environment (as per the clone
* flags). The actual kick-off is left to the caller.
*/
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
//...
//创建新的进程描述符和thread_info,复制父进程描述符信息
p = dup_task_struct(current);
//检查进程数量是否超过限制
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
//初始化子进程描述符,与父进程区分开
p->did_exec = 0;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
copy_flags(clone_flags, p);
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
rcu_copy_process(p);
p->vfork_done = NULL;
spin_lock_init(&p->alloc_lock);
init_sigpending(&p->pending);
//...
//初始化与调度相关的字段
sched_fork(p, clone_flags);
retval = perf_event_init_task(p);
if (retval)
goto bad_fork_cleanup_policy;
if ((retval = audit_alloc(p)))
goto bad_fork_cleanup_policy;
/* copy all the process information */
//如果CLONE_SYSVSEM置位,则使用父进程的System V信号量
if ((retval = copy_semundo(clone_flags, p)))
goto bad_fork_cleanup_audit;
//如果CLONE_FILES置位,则共享父进程的文件描述符,否则创建新的files数组,其包含的信息与父进程相同
if ((retval = copy_files(clone_flags, p)))
goto bad_fork_cleanup_semundo;
//如果CLONE_FS置位,则共享父进程的文件系统
if ((retval = copy_fs(clone_flags, p)))
goto bad_fork_cleanup_files;
//如果CLONE_SIGHAND置位,则共享父进程的信号处理程序
if ((retval = copy_sighand(clone_flags, p)))
goto bad_fork_cleanup_fs;
//如果CLONE_THREAD置位,则共享父进程信号处理相关数据信息
if ((retval = copy_signal(clone_flags, p)))
goto bad_fork_cleanup_sighand;
//如果CLONE_VM置位,则共享父进程的内存地址空间
if ((retval = copy_mm(clone_flags, p)))
goto bad_fork_cleanup_signal;
//命名空间处理刚好与前面的CLONE相反,如果设置了CLONE_NEWxyz相关的标志,则创建新的命名空间,否则共享父进程的命令空间
if ((retval = copy_namespaces(clone_flags, p)))
goto bad_fork_cleanup_mm;
//如果CLONE_IO置位,则共享父进程的IO上下文信息
if ((retval = copy_io(clone_flags, p)))
goto bad_fork_cleanup_namespaces;
//copy_thread()函数与特殊cpu体系结构相关,该函数主要处理进程使用的寄存器、进程切换相关的字段(主要是p->thread)
retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_io;
//在指定的PID命名空间中申请新PID
if (pid != &init_struct_pid) {
pid = alloc_pid(p->nsproxy->pid_ns);
}
p->pid = pid_nr(pid);
p->tgid = p->pid;
if (likely(p->pid)) {
tracehook_finish_clone(p, clone_flags, trace);
if (thread_group_leader(p)) {
if (clone_flags & CLONE_NEWPID)
p->nsproxy->pid_ns->child_reaper = p;
p->signal->leader_pid = pid;
tty_kref_put(p->signal->tty);
p->signal->tty = tty_kref_get(current->signal->tty);
attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
attach_pid(p, PIDTYPE_SID, task_session(current));
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
__get_cpu_var(process_counts)++;
}
attach_pid(p, PIDTYPE_PID, pid);
nr_threads++;
}
total_forks++;
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
proc_fork_connector(p);
cgroup_post_fork(p);
perf_event_fork(p);
return p;
//......
}
int __attribute__((weak)) arch_dup_task_struct(struct task_struct *dst,
struct task_struct *src)
{
*dst = *src;
return 0;
}
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
//...
//为子进程创建的进程描述符task_struct
tsk = alloc_task_struct();
//为子进程创建thread_info
ti = alloc_thread_info(tsk);
tsk->stack = ti;
//复制父进程的描述符结构(字段复制)
err = arch_dup_task_struct(tsk, orig);
//复制父进程的thread_info结构(字段复制)
setup_thread_stack(tsk, orig);
//...
return tsk;
}
唤醒子进程
do_fork()
函数调用copy_process()
函数填充子进程相关数据结构后,调用wake_up_new_task()
函数将子进程放入运行队列,唤醒子进程
void wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
{
unsigned long flags;
struct rq *rq;
int cpu __maybe_unused = get_cpu();
rq = cpu_rq(cpu); //获取cpu对应的运行队列(每个cpu都维护一个运行队列)
raw_spin_lock_irqsave(&rq->lock, flags);
BUG_ON(p->state != TASK_WAKING);
p->state = TASK_RUNNING; //设置进程状态为运行态
update_rq_clock(rq);
activate_task(rq, p, 0); //激活进程,将进程放入运行队列中
trace_sched_wakeup_new(rq, p, 1);
check_preempt_curr(rq, p, WF_FORK);
task_rq_unlock(rq, &flags);
put_cpu();
}
/*
* activate_task - move a task to the runqueue.
*/
static void activate_task(struct rq *rq, struct task_struct *p, int wakeup)
{
if (task_contributes_to_load(p))
rq->nr_uninterruptible--;
enqueue_task(rq, p, wakeup, false); //放入运行队列中
inc_nr_running(rq);
}
static void
enqueue_task(struct rq *rq, struct task_struct *p, int wakeup, bool head)
{
if (wakeup)
p->se.start_runtime = p->se.sum_exec_runtime;
sched_info_queued(p);
p->sched_class->enqueue_task(rq, p, wakeup, head);
p->se.on_rq = 1;
}