分析Linux内核创建一个新进程的过程
来源:互联网 发布:java tochararray用法 编辑:程序博客网 时间:2024/05/16 09:47
刘文 + 原创作品转载请注明出处 +《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
这一周的课程我们主要分析了Linux内核中关于进程的部分,重点是研究Linux内核创建一个新进程的过程及其源代码。我们先看一下使用fork函数创建一个进程的简单代码:
#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main(int argc, char * argv[]){ int pid; /* fork another process */ pid = fork(); if (pid < 0) { /* error occurred */ fprintf(stderr,"Fork Failed!"); exit(-1); } else if (pid == 0) { /* child process */ printf("This is Child Process!\n"); } else { /* parent process */ printf("This is Parent Process!\n"); /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!\n"); }}
fork()实际上封装了sys_clone()内核函数,而sys_clone又调用了内核程序do_fork(),为了研究Linux内核创建新进程的过程,我们看一下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; //总的pid数量 /* * 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. */ 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; } // 复制进程描述符,返回创建的task_struct的指针 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); // 取出task结构体内的pid pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } // 将子进程添加到调度器的队列,使得子进程有机会获得CPU wake_up_new_task(p); /* forking complete and child started to run, tell ptracer */ if (unlikely(trace)) ptrace_event_pid(trace, pid); // 如果设置了 CLONE_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;}
我们再分析一下do_fork中比较重要的部分,这个函数中主要调用了两个函数:
1、通过copy_process来复制进程描述符,返回新创建的子进程的task_struct的指针(即PCB指针);
2、通过调用wake_up_new_task将新的进程放入运行队列并唤醒该进程。
那么我们先看一下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与当前进程的task,仅仅是stack地址不同 p = dup_task_struct(current); if (!p) goto fork_out; ··· retval = -EAGAIN; // 检查该用户的进程数是否超过限制 if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) { // 检查该用户是否具有相关权限,不一定是root if (p->real_cred->user != INIT_USER && !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN)) goto bad_fork_free; } current->flags &= ~PF_NPROC_EXCEEDED; retval = copy_creds(p, clone_flags); if (retval < 0) goto bad_fork_free; /* * If multiple threads are within copy_process(), then this check * triggers too late. This doesn't hurt, the check is only there * to stop root fork bombs. */ retval = -EAGAIN; // 检查进程数量是否超过 max_threads,后者取决于内存的大小 if (nr_threads >= max_threads) goto bad_fork_cleanup_count; if (!try_module_get(task_thread_info(p)->exec_domain->module)) goto bad_fork_cleanup_count; delayacct_tsk_init(p); /* Must remain after dup_task_struct() */ p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER); // 表明子进程还没有调用exec系统调用 p->flags |= PF_FORKNOEXEC; 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); // 初始化定时器 p->utime = p->stime = p->gtime = 0; p->utimescaled = p->stimescaled = 0;#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE p->prev_cputime.utime = p->prev_cputime.stime = 0;#endif#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN seqlock_init(&p->vtime_seqlock); p->vtime_snap = 0; p->vtime_snap_whence = VTIME_SLEEPING;#endif ...#ifdef CONFIG_DEBUG_MUTEXES p->blocked_on = NULL; /* not blocked yet */#endif#ifdef CONFIG_BCACHE p->sequential_io = 0; p->sequential_io_avg = 0;#endif /* Perform scheduler related setup. Assign this task to a CPU. */ // 完成对新进程调度程序数据结构的初始化,并把新进程的状态设置为TASK_RUNNING // 同时将thread_info中得preempt_count置为1,禁止内核抢占 retval = sched_fork(clone_flags, p); if (retval) goto bad_fork_cleanup_policy; retval = perf_event_init_task(p); if (retval) goto bad_fork_cleanup_policy; retval = audit_alloc(p); if (retval) goto bad_fork_cleanup_perf; /* copy all the process information */ // 复制所有的进程信息 shm_init_task(p); retval = copy_semundo(clone_flags, p); if (retval) goto bad_fork_cleanup_audit; retval = copy_files(clone_flags, p); if (retval) goto bad_fork_cleanup_semundo; ... // 初始化子进程的内核栈 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号 pid = alloc_pid(p->nsproxy->pid_ns_for_children); if (!pid) goto bad_fork_cleanup_io; } ... // 清除子进程thread_info结构的 TIF_SYSCALL_TRACE,防止 ret_from_fork将系统调用消息通知给调试进程 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); /* ok, now we should be set up.. */ // 设置子进程的pid p->pid = pid_nr(pid); // 如果是创建线程 if (clone_flags & CLONE_THREAD) { p->exit_signal = -1; // 线程组的leader设置为当前线程的leader p->group_leader = current->group_leader; // tgid是当前线程组的id,也就是main进程的pid 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; // tgid和pid相同 p->tgid = p->pid; } ... if (likely(p->pid)) { ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace); init_task_pid(p, PIDTYPE_PID, pid); if (thread_group_leader(p)) { ... // 将pid加入散列表 attach_pid(p, PIDTYPE_PGID); attach_pid(p, PIDTYPE_SID); __this_cpu_inc(process_counts); } else { ... } // 将pid加入PIDTYPE_PID这个散列表 attach_pid(p, PIDTYPE_PID); // 递增 nr_threads的值 nr_threads++; } total_forks++; spin_unlock(¤t->sighand->siglock); syscall_tracepoint_update(p); write_unlock_irq(&tasklist_lock); ... // 返回被创建的task结构体指针 return p; ... }
这个函数中比较重要的语句:
p = dup_task_struct(current) 为新进程分配一个新的内核堆栈,复制了thread_info;
copy_* 这一系列的拷贝函数为pcb复制了一些指针数据,tsk->files,tsk->fs,等等;copy_thread () 该函数拷贝和体系结构相关的进程执行状态,寄存器,指令指针ip等等。
下面我们对其一一分析,首先是dup_task_struct这个函数,会分配一个新的task_struct给子进程,但是这个task_struct是未初始化的,下面我们看看这个dup_task_struct:
static struct task_struct *dup_task_struct(struct task_struct *orig){ struct task_struct *tsk; struct thread_info *ti; int node = tsk_fork_get_node(orig); int err; // 分配一个task_struct结点 tsk = alloc_task_struct_node(node); if (!tsk) return NULL; // 分配一个thread_info结点,其实内部分配了一个union,包含进程的内核栈 // 此时ti的值为栈底,在x86下为union的高地址处。 ti = alloc_thread_info_node(tsk, node); if (!ti) goto free_tsk; err = arch_dup_task_struct(tsk, orig); if (err) goto free_ti; // 将栈底的值赋给新结点的stack tsk->stack = ti; ... /* * One for us, one for whoever does the "release_task()" (usually * parent) */ // 将进程描述符的使用计数器置为2 atomic_set(&tsk->usage, 2);#ifdef CONFIG_BLK_DEV_IO_TRACE tsk->btrace_seq = 0;#endif tsk->splice_pipe = NULL; tsk->task_frag.page = NULL; account_kernel_stack(ti, 1); // 返回新申请的结点 return tsk;free_ti: free_thread_info(ti);free_tsk: free_task_struct(tsk); return NULL;}
下面我们看一下copy_thread()函数,这里的过程基本就是给新的进程的各种运行时状态进行初始化,比如寄存器信息(通过父进程的寄存器信息来初始化,但是eip会是个例外,eip将会取决于最后子进程将会从哪里开始执行),栈会被置空未初始化状态:
// 初始化子进程的内核栈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(); // 子进程的eax置为0,所以fork的子进程返回值为0 childregs->ax = 0; if (sp) childregs->sp = sp; // 子进程从ret_from_fork开始执行 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; // 如果父进程使用IO权限位图,那么子进程获得该位图的一个拷贝 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); } ... return err;}
在copy_thread函数中,子进程的内核堆栈被初始化,进程的sp,ip信息也是在这时被写入,我们在第一次实验my_kernel中得知,0号进程的ip被我们设置成即将执行的函数的入口地址,而在这里,ip被我们设置为汇编函数ret_from_fork,这是子进程的执行的起点;也是在这个函数中,寄存器eax被改写为0,这就是fork出的子进程id=0的原因。
最后总结一下Linux创建一个新进程的过程,函数调用和执行的顺序如下:
libc fork() -> system_call -> sys_clone() -> do_fork() -> copy_process() {dup_task_struct; copy_thread } -> wake_up_new_task() -> ret_from_fork
- 分析Linux内核创建一个新进程的过程(Linux)
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 6.分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 6、分析Linux内核创建一个新进程的过程
- Swift数据类型(一)
- 反射之前奏Oracle简单版
- Android的事件分发机制
- (二)WI-FI辅助定位
- 递归递推练习 M
- 分析Linux内核创建一个新进程的过程
- hdu 2048 错排的递推式
- Android5.x Notification应用解析
- 【day-5】希尔排序
- 关于textarea的一道题
- Caffe入门:对于抽象概念的图解分析
- Lecture 3 Divide and Conquer
- 中山大学数据库作业三
- 递推递归练习 O