实验总结 第六周 进程

分析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的一致,然后返回用户态,变成子进程的用户态。

实验截图

 实验总结 :见第六周总结 

时间: 2024-10-19 02:22:49

实验总结 第六周 进程的相关文章

20135201李辰希《Linux内核分析》第六周 进程的描述与创建

李辰希 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.进程的描述 操作系统的三大管理功能: 进程管理(最重要的) 内存管理 文件系统 为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息. 进程控制块PCB task_struct: 进程状态 进程打开的文件 进程优先级信息 task_struct总体数据结构的抽象: tty:控制台 fs:文件系统

20135327郭皓--Linux内核分析第六周 进程的描述和进程的创建

进程的描述和进程的创建 一.进程的描述 操作系统三大功能: 进程管理 内存管理 文件系统 进程描述符task_struct数据结构 task _ struct:为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息. 进程的状态:Linux进程的状态(就绪态.运行态.阻塞态) 进程的标示pid:用来标示进程 进程描述符task_struct数据结构(重要部分): 1 struct task_struct { 2 volatile long state; /* 进程

Linux内核学习第六周 进程描述与进程创建

1.task_struct的数据结构 1235struct task_struct { 1236 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 1237 void *stack; 1238 atomic_t usage; 1239 unsigned int flags; /* per process flags, defined below */ 1240 unsigned int ptrace; 1241 12

第六节 进程的描述和进程的创建

第六周 进程的描述和进程的创建 By 20135217 孙小博 本周的主要内容: 如何描述一个进程:进程描述符的数据结构: 如何创建一个进程:内核是如何执行的,以及新创建的进程从哪里开始执行: 使用gdb跟踪新进程的创建过程. 进程的描述 操作系统三大功能: 进程管理(最核心最基础) 内存管理 文件系统 进程描述符task_struct数据结构 task _ struct:为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息.struct task_struct

LINUX内核分析第六周学习总结——进程的描述和进程的创建

LINUX内核分析第六周学习总结——进程的描述和进程的创建 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.知识概要 进程的描述 进程描述符task_struct数据结构(一) 进程描述符task_struct数据结构(二) 进程的创建 进程的创建概览及fork一个进程的用户态代码 理解进程创建过程复杂代码的方法 浏览进程创建过程相关的关键代码 创建的新进程是从哪里开始执行的

第八周课程总结 &amp; 实验报告(六)

第八周课程总结 一.包装类 介绍 装箱与拆箱 应用 二.异常 基本概念 基本格式 异常类的继承结构 throws关键字 throw关键字 Exception类和RuntimeException类 自定义异常类 三.多线程 基本概念 进程 多线程 Java中线程的实现 继承Thread类 实现Runnable接口 Thread和Runnable的区别 一.包装类        1.介绍                                8种基本类型变包装类 (1)除了Character

第六周总结 &amp; 实验报告(四)

第六周小结 一.instanceof关键字         在Java中使用instanceof关键字判断一个对象到底是哪个类的实例,返回boolean类型 1.instanceof关键字的作用 例class A{ public void fun1(){ System.out.println("A-->public void fun1(){}"); } public void fun2(){ this.fun1(); } } classB extends A{ public vo

第六周学习总结与实验报告

第六周&java实验报告四 一.实验目的 (1)掌握类的继承 (2)变量的继承和覆盖,方法的继承,重载和覆盖的实现: 二.实验的内容 (1)根据下面的要求实现圆类Circle. 1.圆类Circle的成员变量:radius表示圆的半径. 2.圆类Circle的方法成员: Circle():构造方法,将半径置0 Circle(double r):构造方法,创建Circle对象时将半径初始化为r double getRadius():获得圆的半径值 double getPerimeter():获得圆

第六周总结&amp;第四次实验报告

实验四 类的继承 一. 实验目的 (1) 掌握类的继承方法: (2) 变量的继承和覆盖,方法的继承.重载和覆盖实现: 二. 实验内容 三.实验过程 实验代码 package Shiyan4; public class Circle { private double radius; public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } p