Linux内核分析:实验六--Linux进程的创建过程分析
Posted Lansing
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核分析:实验六--Linux进程的创建过程分析相关的知识,希望对你有一定的参考价值。
刘畅 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
概述
本次实验在MenuOS中加入fork系统调用,并通过GDB的调试跟踪,近距离的观察Linux中进程创建的过程。阅读Linux进程部分的源码,结合起来理解Linux内核创建新进程的过程。
Linux中对进程的描述
Linux中task_struct结构体用于描述系统中的进程,对应x86机器的此结构体定义放在了/include/linux/sched.h中。这个结构体相当庞大,为了能快速的理解进程的创建过程,这里简单的浏览一下task_struct结构体的定义。
struct task_struct {
/* 表示进程的状态 */
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
/* 进程的内核栈,通过alloc_thread_info来分配的 */
void *stack;
...
/* 进程的ID和所在进程组的ID */
pid_t pid;
pid_t tgid;
/* 表示进程间的关系 */
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
...
/* 表示进程的静态优先级 */
int prio, static_prio, normal_prio;
/* 进程的实时优先级 */
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
/* 表示调度策略 */
unsigned int policy;
int nr_cpus_allowed;
/* 表示进程在哪个CPU上执行 */
cpumask_t cpus_allowed;
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespaces */
struct nsproxy *nsproxy;
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;
}
task_struct结构体用于描述Linux中的进程,进程之间是使用双向链表链接起来的,这里简要的浏览了一下task_struct。进程的状态:TASK_RUNNING表示进程要么正在执行,要么正要准备执行。进程的状态转移图如下图所示,图片来自《Linux Kernel Development》:
Linux中进程的创建
在实验三分析Linux内核启动的过程中,我们了解到内核在rest_init函数中,使用kernel_thread创建了2个进程kernel_init和kthreadd,通过GDB跟踪我们知道它们都是用do_fork创建的。在这里,普通进程通过fork系统调用产生一个新进程也是通过do_fork实现的。我在机器上跟踪fork进程的过程,如下:
do_fork过程理解
通过GDB跟踪可以看出Linux中产生新的进程最终要通过do_fork来实现的,下面是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)
{
/* 进程的结构体 */
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
/* 克隆进程的一些标志部分,属于哪类的fork */
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;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
/* 复制进程的一些变量定义,返回一个新进程的结构体指针 */
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;
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);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
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;
}
下面是do_fork()函数执行的主要步骤:
- 通过查找pidmap_array位图,为子进程分配新的PID
- 检查父进程的ptrace字段:如果它的值不等于0,说明有另外一个进程正在跟踪父进程,因而,do_fork()函数检查debugger程序是否自己想跟踪子进程。在这种情况下,如果子进程不是内核线程(CLONE_UNTRACED标志被清0),则do_fork()函数设置CLONE_PTRACE标志。
- 调用copy_process()函数复制进程描述符。如果所有必须的资源都是可用的,则该函数返回刚创建的task_struct描述符的地址。
如果设置了CLONE_STOPPED标志,或者必须跟踪子进程,即在p->ptrace 中设置 PT_PTRACED标志,那么子进程的状态被设置成TASK_STOPPED状态,并且为子进程增加挂起的SIGSTOP信号。在另一个进程把子进程状态恢复成TASK_RUNNING之前,一直保持该状态。
如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task(p, clone_flags)函数以执行以下操作:
a.调整父进程和子进程的调度参数
b.如果子进程和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表(CLONE_VM标志被清0),那么,就把子进程插入到父进程的运行队列,插入时让子进程恰好在父进程前面,因此迫使子进程优于父进程先运行。如果子进程刷新其地址空间,并且在创建之后执行新程序,那么这种简单的处理会产生较好的性能。而如果我们让父进程先运行,那么写时复制机制将会执行一些不必要的页面复制。
c.否则,如果子进程与父进程运行在不同CPU上,或者父进程和子进程共享同一组页表(CLONE_VM标志被设置),就把子进程插入父进程所在运行队列的队尾。
- 如果设置了CLONE_STOPPED标志,则子进程的状态被设置成TASK_STOPPED状态。
- 如果父进程被跟踪,则把子进程的PID存入current的ptrace_message字段并调用ptrace_notify函数使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号。子进程的祖父进程是跟踪父进程的debugger进程。SIGCHLD信号通知debugger进程:当前进程current已经创建了一个子进程,可以通过current->ptrace_message字段获得该子进程的PID。
- 如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间(也就是说,直到子进程结束或执行新的程序)。
- 结束并返回子进程的PID。
copy_process的过程
从do_fork中可以看出,copy_process过程返回了新进程的结构体指针,这里面完成了新进程的初始化工作。下面对copy_process过程简要的浏览一下:
/*
创建进程描述符以及子进程所需要所有数据结构
*/
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结构体 */
p = dup_task_struct(current);
/* 进行一些权限条件判断 */
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
retval = -EAGAIN;
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
/* 把新进程加入到调度队列 */
retval = sched_fork(clone_flags, p);
/* 初始化新进程的内核栈 */
retval = copy_thread(clone_flags, stack_start, stack_size, p);
if (retval)
goto bad_fork_cleanup_io;
if (pid != &init_struct_pid) {
retval = -ENOMEM;
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (!pid)
goto bad_fork_cleanup_io;
}
p->pid = pid_nr(pid);
/* 这里设置新创建进程的关系 */
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -1;
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p;
p->tgid = p->pid;
}
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;
}
attach_pid(p, PIDTYPE_PID);
nr_threads++;
return p;
}
copy_process主要做了如下工作:
- 初始化新进程的结构体
- 设置进程的内核栈
- 将新进程加入到调度队列
- 设置新进程的关系图,比如它的父进程是谁,该进程所在进程组的leader信息等
- 返回新进程结构体的指针
Linux进程创建的过程总览
在Linux中,可以通过如下几种方式创建一个新进程:
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
可以看出 fork 、vfork 、clone 都是对 do_fork函数的一种封装,它们的参数个数以及标志位不同,也就是说 Linux中最终要通过do_fork来创建一个新的进程。在上面do_fork的源码分析过程中,我们了解了 do_fork 的工作流程,其中又分析了 copy_process的过程,大致了解了新进程的创建过程。
还剩下最后一个比较关键的问题,新创建的进程是从哪执行的呢?
在copy_process中有copy_thread调用,其中copy_thread中指明了新建立的进程从哪里执行。
int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;
p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
if (unlikely(p->flags & PF_KTHREAD)) {
/* kernel thread */
memset(childregs, 0, sizeof(struct pt_regs));
p->thread.ip = (unsigned long) ret_from_kernel_thread;
task_user_gs(p) = __KERNEL_STACK_CANARY;
childregs->ds = __USER_DS;
childregs->es = __USER_DS;
childregs->fs = __KERNEL_PERCPU;
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;
p->thread.io_bitmap_ptr = NULL;
return 0;
}
*childregs = *current_pt_regs();
childregs->ax = 0;
if (sp)
childregs->sp = sp;
p->thread.ip = (unsigned long) ret_from_fork;
task_user_gs(p) = get_user_gs(current_pt_regs());
p->thread.io_bitmap_ptr = NULL;
tsk = current;
err = -ENOMEM;
if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES, GFP_KERNEL);
if (!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max = 0;
return -ENOMEM;
}
set_tsk_thread_flag(p, TIF_IO_BITMAP);
}
err = 0;
/*
* Set a new TLS for the child thread?
*/
if (clone_flags & CLONE_SETTLS)
err = do_set_thread_area(p, -1,
(struct user_desc __user *)childregs->si, 0);
if (err && p->thread.io_bitmap_ptr) {
kfree(p->thread.io_bitmap_ptr);
p->thread.io_bitmap_max = 0;
}
return err;
}
在* p->thread.ip = (unsigned long) ret_from_fork*中指出了新进程的ip是指向 ret_from_fork 的位置,GDB走到这一步就跟踪不下去了。
总结
本次实验通过阅读源码和跟踪fork系统调用结合的方式,近距离的观察了进程创建的过程。大致了解到新进程的创建主要通过如下步骤:
- fork / vfork / clone 系统调用
- do_fork 生成新的进程,返回进程的id
- copy_process 返回新进程的结构体指针,还要设置进程之间的关系,将新进程加入到调度队列中。
- copy_thread 设置新进程的内核栈,已经开始执行的地方
ret_from_fork是新进程开始执行的地方,asmlinkage void ret_from_fork(void) asm(“ret_from_fork”); 它一条汇编指令实现的,其内部执行过程还没有触摸到。
以上是关于Linux内核分析:实验六--Linux进程的创建过程分析的主要内容,如果未能解决你的问题,请参考以下文章