Linux内核分析:实验六--Linux进程的创建过程分析
来源:互联网 发布:95式自动步枪弹道数据 编辑:程序博客网 时间:2024/05/17 04:30
刘畅 原创作品转载请注明出处 《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。
此处参考CSDN-do_fork()函数详解
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内核创建一个新进程的过程
- Linux内核分析:实验六--Linux进程的创建过程分析
- 实验六:分析Linux内核创建一个新进程的过程
- [网易云课堂]Linux内核分析(六)—— 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程(Linux)
- Linux内核分析实验六
- Linux内核分析:实验六
- Linux内核分析:实验六
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- Linux内核创建一个进程的过程分析
- Linux内核创建一个进程的过程分析
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 分析Linux内核创建一个新进程的过程
- 新手笔记:java集合汇总
- Linux下/etc/fstab文件详解
- 理解MySQL——索引与优化
- CodeForces 25E Test
- 系统重装后恢复MySQL数据库文件
- Linux内核分析:实验六--Linux进程的创建过程分析
- 调用STAF的TCL简单脚本
- 国内jQuery CDN
- hdoj 2029 Palindromes _easy version (字符串)
- 2016郑州大学ACM/ICPC训练赛 C 最长匹配子串
- 阅读《Android 从入门到精通》(35)——后台服务
- 笔试题12. 微信红包
- Java之JDBC对接MySql
- POJ 3734Blocks