实验总结 第六周 进程
分析fork函数对应的内核处理过程sys_clone,理解创建一个新进程如何创建和修改task_struct数据结构
Linux中创建进程一共有三个函数:
fork :创建子进程
vfork:和fork类似
clone:创建线程
进程创建过程:
SYSCALL_DEFINE0(fork)
{
    return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
#endif
 
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);
}
结论::fork、vfork和clone这三个函数最终都是通过do_fork函数实现的。
分析do_fork的代码
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
 | 
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;    // ...         // 复制进程描述符,返回创建的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);        // 取出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);        // ...        // 如果设置了 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,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
2. 初始化vfork的完成处理信息(如果是vfork调用)
3. 调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
4. 如果是vfork调用,需要阻塞父进程,知道子进程执行exec。
进程创建的关键copy_process:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 
88 
89 
90 
91 
92 
93 
94 
95 
96 
 | 
/*    创建进程描述符以及子进程所需要的其他所有数据结构    为子进程准备运行环境*/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 (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;    }    retval = -EAGAIN;    // 检查进程数量是否超过 max_threads,后者取决于内存的大小    if (nr_threads >= max_threads)        goto bad_fork_cleanup_count;    // 初始化自旋锁    // 初始化挂起信号    // 初始化定时器    // 完成对新进程调度程序数据结构的初始化,并把新进程的状态设置为TASK_RUNNING    retval = sched_fork(clone_flags, p);    // .....    // 复制所有的进程信息    // copy_xyz    // 初始化子进程的内核栈    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;    }    /* 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 (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {        // 如果是创建线程,那么同一线程组内的所有线程、进程共享parent        p->real_parent = current->real_parent;        p->parent_exec_id = current->parent_exec_id;    } else {        // 如果是创建进程,当前进程就是子进程的parent        p->real_parent = current;        p->parent_exec_id = current->self_exec_id;    }    // 将pid加入PIDTYPE_PID这个散列表    attach_pid(p, PIDTYPE_PID);    // 递增 nr_threads的值    nr_threads++;    // 返回被创建的task结构体指针    return p;} | 
分析copy_process的大体流程:
 检查各种标志位(已经省略)
 调用dup_task_struct复制一份task_struct结构体,作为子进程的进程描述符。
 检查进程的数量限制。
 初始化定时器、信号和自旋锁。
 初始化与调度有关的数据结构,调用了sched_fork,这里将子进程的state设置为TASK_RUNNING。
 复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等。
 调用copy_thread,这又是关键的一步,这里设置了子进程的堆栈信息。
 为子进程分配一个pid
 设置子进程与其他进程的关系,以及pid、tgid等。这里主要是对线程做一些区分。
在copy_process中,copy_thread函数为子进程准备了上下文堆栈信息
copy_thread的流程如下:
1. 获取子进程寄存器信息的存放位置
2. 对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。
3. 如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread,将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出
4. 将父进程的寄存器信息复制给子进程。
5. 将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0.
6. 子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。
 
创建的新进程是从哪里开始执行的?
——ret_from_fork
*childregs = *current_pt_regs(); //复制内核堆栈
childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!
     
p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
ip指向的是ret_from_fork,所以是从这里开始执行的。
复制内核堆栈的时候是复制的pt_regs,即只复制了SAVE_ALL相关的那一部分,即系统调用压栈的那一部分。
pt_regs里面内容有:
Entry(ret_from_fork):
最终会跳转到syscall_exit,这之前的内核堆栈状态和syscall_call的一致,然后返回用户态,变成子进程的用户态。
实验截图
![]()
 实验总结 :见第六周总结 
                    
                

                
            
        
浙公网安备 33010602011771号