杨明辉+ 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、实验过程
1. 打开终端输入cd Linux进入Linux目录下,然后输入rm menu -rf命令删除menu,然后输入命令git clone https://github.com/mengning/menu.git克隆一个新的menu;然后输入命令mv test_fork.c test.c覆盖test.c的内容;最后输入sudo make rootfs进行编译,我们可以看到MENUOS中多了fork命令;实验结果如图1,图2所示。
图1
图22. 与前几次实验一样打开gdb,输入命令file linux-3.18.6/vmlinux加载符号表,然后输入target remote:1234来连接MENUOS,并在sys_clone、do_fork、dup_task_struct、copy_process、copy_thread、ret_from_fork处设置断点。实验结果如图3所示。
图33. 然后利用gdb中的c、n、s和finish等命令进行跟踪调试,实验结果如图4、图5、图6所示。
图4继续:
图5继续:
图6
二、创建新进程过程分析
1.进程的创建
在用户态调用fork()函数来创建一个子进程, fork系统调用在父进程和子进程都会返回一次,在父进程的返回值是子进程中的进程id号,在子进程的返回值是0,在shell命令行下执行函数调用后,函数中的if和else子句中的程序都得到了执行,但是if和else子句的执行分别是在不同的进程中执行的,一个在父进程、一个在子进程中执行;其中子进程复制了父进程所有的信息。子进程从fork()函数后开始执行,与父进程的程序逻辑相同。
fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建。创建新进程是通过复制当前进程的PCB(task_struct)来实现的,然后进行相应的初始化。
2. task_struct的关键数据结构
struct task_struct { volatile long state; /* -1 不可执行, 0 可执行, >0 停止 */ void *stack; atomic_t usage; unsigned int flags; /* 每一个进程的标识符,是唯一的 */ unsigned int ptrace; #ifdef CONFIG_SMP struct llist_node wake_entry; int on_cpu; struct task_struct *last_wakee; unsigned long wakee_flips; unsigned long wakee_flip_decay_ts; 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; #ifdef CONFIG_TREE_PREEMPT_RCU struct rcu_node *rcu_blocked_node; #endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */ #ifdef CONFIG_TASKS_RCU unsigned long rcu_tasks_nvcsw; #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT) struct sched_info sched_info; struct list_head tasks; struct plist_node pushable_tasks; struct rb_node pushable_dl_tasks; #endif struct mm_struct *mm, *active_mm; #ifdef CONFIG_COMPAT_BRK unsigned brk_randomized:1; #endif u32 vmacache_seqnum; struct vm_area_struct *vmacache[VMACACHE_SIZE]; #if defined(SPLIT_RSS_COUNTING) struct task_rss_stat rss_stat; };
3.进程执行的状态及状态转化过程如图7所示。
图7
4. 创建进程的大致流程。
1. fork函数通过ox80中断(系统调用)来陷入内核,然后进入系统提供的相应系统调用来完成进程的创建过程。
2. fork、vfork、和clone三个系统调用都可以创建一个新的进程,而且都是通过do_fork来实现进程的创建。
3. 在do_fork中首先调用copy_process为子进程复制一份进程信息。
4. 调用dup_task_struct复制当前的task_struct。
5. 检查进程数是否超过限制。
6. 初始化自旋锁、挂起信号、CPU定时器等。
7. 调用sched_fork初始化进程数据结构,并把进程状态设置为TASK_RUNNING.
8. 复制所以进程信息,包括文件系统、信号处理函数、信号、内存管理等。
9. 调用copy_thread初始化进程内核栈。
10. 为新进程分配并设置新的pid
5. do_fork关键代码及分析。
6.dup_task_struct的源码及分析
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; //…… //复制进程描述符,copy_process()的返回值是一个 task_struct 指针。 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; struct pid *pid; trace_sched_process_fork(current, p); //得到新创建的进程描述符中的pid pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); //如果调用的 vfork()方法,初始化 vfork 完成处理信息。 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } //将子进程加入到调度器中,为其分配 CPU,准备执行 wake_up_new_task(p); //fork 完成,子进程即将开始运行 if (unlikely(trace)) ptrace_event_pid(trace, pid); //如果是 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; }
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 节点,包含进程的内核栈,ti 为栈底 ti = alloc_thread_info_node(tsk, node); if (!ti) goto free_tsk; //将栈底的值赋给新节点的栈 tsk->stack = ti; //…… return tsk; }
7. 堆栈状态执行流程图如图8、图9所示。
图8
图9
三、总结
1. 新进程的执行源于以下原因:1. dup_task_struct中为其分配了新的堆栈。2. 调用了sched_fork,将其置为TASK_RUNNING。3. copy_thread中将父进程的寄存器上下文复制给子进程,保证了父子进程的堆栈信息是一致的。4. 将ret_from_fork的地址设置为eip寄存器的值。2. 最终子进程从ret_from_fork开始执行。