进程管理也是计算子系统(CPU&Memory)的核心功能,从本篇博文起,我们开始讨论进程管理。计算子系统相关内容目录点此进入。
什么是进程?什么是进程管理?为什么需要进程管理?
从物理视角说上,进程是CPU上的一段逻辑过程,它的控制(代码段)和数据(数据段)存放于内存。回顾一下计算子系统开篇中描绘的系统结构图(如下图),进程的执行要素包括CPU中的寄存器和内存段两个部分(虚拟内存段最终会映射到物理内存段):寄存器代表进程的瞬时运行状态;代码段存储指令,控制进程执行逻辑;数据段存储进程的全局数据;堆栈段存储局部数据和动态数据。从功能视角说,进程是各种“功能”的实现实体,计算机为人们提供的诸如聊天、上网、看视频等各种功能都是通过进程实现的,因此进程有时也被叫作“任务“。
进程管理是指与进程相关的一系列动作,如创建、替换、终止、调度、通信等等。进程管理使得一个CPU可以执行若干进程,各进程分时复用CPU的物理资源;内存管理使得多个进程可以共享物理内存;基于上述两个核心功能,计算机系统可以实现多任务并行,大大提升系统运行效率,方使客户使用(想象一下,如果你的计算机一个时刻只能运行一个任务,那将是一种多么糟糕体验)。
如何创建进程?
进程创建就是新建一个进程,这是进程管理最基本的功能,也是进程生命周期的起点。下面我们就来看看进程创建在Linux内核中是如何实现的。
fork、vfork和clone
从应用程序开发的层次上,我们应该知道创建进程(或线程,即轻量级进程)有fork、vfork和clone三种系统调用:fork是创建进程标准做法,父子进程共享代码段,但拥有独立数据、堆栈段;vfork是轻量级进程创建方法,父子进程共享代码、数据和堆栈段,子进程运行期间父进程是睡眠的,当子进程结束后父进程才继续运行;clone则提供了更灵活的进程创建方式,可以通过clone_flags来控制创建过程,libpthread库提供的相关API即是通过clone系统调用实现的。大家可以在网上找一些这三种方式的示例代码,动手实验一下以加深理解。到了内核态,这三个系统调用最终都通过do_fork函数来实现其核心功能:
linux/kernel/fork.c:
SYSCALL_DEFINE0(fork)
{
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
进程控制块:struct task_struct
在深入分析do_fork之前,我们首先要明白内核对进程需要有一个抽象的数据表达,基于这种数据表达才能实现各种管理功能。我们将内核中表达进程的数据结构叫做进程控制块,在linux中则是struct task_struct。这里我不打算对task_struct中的各个字段进行逐一描述,因为难以表述清楚,大家可以结合后续的代码流程来深入理解各字段的含义,下面是一幅整体结构图,供参考:
另外,在早期的linux版本中,进程控制块是包含进程的内核态栈的(通常是8KB大小)。什么是内核态栈?每个进程都有用户态空间和内核态空间两个执行空间,出于安全隔离的考虑,两个空间使用独立的栈,因此内核栈就被安排在了进程控制块中,栈底在高地址端,从高地址往低地址扩展,而进程控制块其它数据则被放置在8K的低地址起始位置处。随着内核的发展,各种功能不断被加入,进程控制块的数据结构也在不断变大,因此就存在挤占内核栈的风险。所以高版本内核将进程控制块和内核栈进行了分离:内核栈的低地址端只保留基本的进程信息,并通过指针对向真正的进程控制块结构,如下图所示:
深入do_fork
do_fork在传入参数clone_flags的控制下,基于当前进程复制了一个新进程,其大体流程是:先复制当前进程产生新的进程控制块,然后再调度新进程进入运行态。代码框架如下:
linux/kernel/fork.c:
/*
* 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,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
long nr;
...
/*基于当前进程的task_struct和clone_flags复制新进程*/
p = copy_process(clone_flags, stack_start, 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;
struct pid *pid;
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
/*如果clone_flags中置了CLONE_VFORK标置,则需要初始化等待结构体*/
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
/*将新创建的进程加入调度队列*/
wake_up_new_task(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);
} else {
nr = PTR_ERR(p);
}
return nr;
}
补充一下关于clone_flags标记的注释说明,建议大家在使用到的代码位置处仔细阅读,以加深理解:
linux/include/uapi/linux/sched.h:
/*
* cloning flags:
*/
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD 0x00010000 /* Same thread group? */
#define CLONE_NEWNS 0x00020000 /* New namespace group? */
#define CLONE_SYSVSEM 0x00040000 /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS 0x00080000 /* create a new TLS for the child */
#define CLONE_PARENT_SETTID 0x00100000 /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID 0x00200000 /* clear the TID in the child */
#define CLONE_DETACHED 0x00400000 /* Unused, ignored */
#define CLONE_UNTRACED 0x00800000 /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */
/* 0x02000000 was previously the unused CLONE_STOPPED (Start in stopped state)
and is now available for re-use. */
#define CLONE_NEWUTS 0x04000000 /* New utsname group? */
#define CLONE_NEWIPC 0x08000000 /* New ipcs */
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
#define CLONE_NEWNET 0x40000000 /* New network namespace */
#define CLONE_IO 0x80000000 /* Clone io context */
接下来深入看一下核心函数copy_process,它主要完成了页表和寄存器值的复制,这里我们略去cgroup和一些非重点代码:
linux/kernel/fork.c:
/*
* 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,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;
...
/*分配task_struct结构内存和thread_info页,并将当前进程相关信息复制到对应内存字段*/
retval = -ENOMEM;
p = dup_task_struct(current);
if (!p)
goto fork_out;
...
/* Perform scheduler related setup. Assign this task to a CPU. */
sched_fork(p);
...
/*新建并复制task_struct中files字段,它表示已打开文件;
如果CLONE_FILES置位,则共享当前进程的files*/
retval = copy_files(clone_flags, p);
...
/*新建并复制task_struct中的fs字段,它表示当前目录;
如果CLONE_FS置位,则共享当前进程的fs*/
retval = copy_fs(clone_flags, p);
...
/*复制信号及信号处理函数*/
retval = copy_sighand(clone_flags, p);
...
retval = copy_signal(clone_flags, p);
...
/*新建并复制mm_struct,并完成页表复制;
如果CLONE_VM置位,则共享当前进程的mm_struct*/
retval = copy_mm(clone_flags, p);
...
/*复制寄存器值*/
retval = copy_thread(clone_flags, stack_start, stack_size, p);
...
return p;
...
}
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
int retval;
...
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
if (!oldmm)
return 0;
/*如果CLONE_VM置位,则共享当前进程mm_struct*/
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
/*
* Allocate a new mm structure and copy contents from the
* mm structure of the passed in task structure.
*/
struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
int err;
if (!oldmm)
return NULL;
/*分配mm_struct内存*/
mm = allocate_mm();
if (!mm)
goto fail_nomem;
/*复制mm_struct内容,这里没有加锁保护,我理解是因为其中关键字段
会在后续流程中重新赋值*/
memcpy(mm, oldmm, sizeof(*mm));
mm_init_cpumask(mm);
...
/*重新初始化mm_struct中相关字段,这里会重新分配pgd表并将内核空间
地址映射复制到其中*/
if (!mm_init(mm, tsk))
goto fail_nomem;
if (init_new_context(tsk, mm))
goto fail_nocontext;
dup_mm_exe_file(oldmm, mm);
/*复制用户态vma段和页表*/
err = dup_mmap(mm, oldmm);
if (err)
goto free_pt;
...
return mm;
...
}
int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{
int err;
struct pt_regs *childregs;
struct task_struct *me = current;
/*task_stack_page(p),即p->stack,为thread_info起始地址;加上THREAD_SIZE
后为内核栈起始地址*/
p->thread.sp0 = (unsigned long)task_stack_page(p) + THREAD_SIZE;
/*childregs指向内核栈中保留所有寄存器后的偏移位置*/
childregs = task_pt_regs(p);
p->thread.sp = (unsigned long) childregs;
/*复制当前进程的用户态栈指针*/
p->thread.usersp = me->thread.usersp;
/*设置TIF_FORK标记,fork系统调用返回时用来判断是否
为新生成的进程*/
set_tsk_thread_flag(p, TIF_FORK);
...
/*对于内核进程,sp中保存的是入口函数指针*/
if (unlikely(p->flags & PF_KTHREAD)) {
/* kernel thread */
memset(childregs, 0, sizeof(struct pt_regs));
childregs->sp = (unsigned long)childregs;
childregs->ss = __KERNEL_DS;
childregs->bx = sp; /* function */
childregs->bp = arg;
childregs->orig_ax = -1;
childregs->cs = __KERNEL_CS | get_kernel_rpl();
childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
return 0;
}
/*复制当前进程在执行fork系统调用时保存的寄存器状态*/
*childregs = *current_pt_regs();
/*子进程的ax寄存器赋为零,该值即fork系统调用的返回值*/
childregs->ax = 0;
/*如果传入sp指针,则更新fork返回后栈指针值*/
if (sp)
childregs->sp = sp;
...
return err;
}
零号进程与一号进程
系统中有两个比较特殊的进程,即零号和一号进程。零号进程是内核初始化过程中最早产生的进程,最终成为bsp(SMP系统中的启动CPU)上的idle进程(swapper)。零号进程会创建一号进程,由一号进程完成部分初始化动作并拉起shell进程。最终一号进程成为所有孤儿进程的回收进程而长期存在于系统之中。
转载请注明:吴斌的博客 » 【计算子系统】进程管理之一:进程创建