分析进程创建的过程---linux内核学习笔记(六)

内容一:实验报告相关说明

 

所学课程:《Linux内核分析》MOOC课程  

链接:http://mooc.study.163.com/course/USTC-1000029000

 

内容二:进程控制块简析

  为了管理进程,内核必须对每个进程进行清晰的描述,内核所需了解的进程信息都记录在结构体 task_struct 中,所以进程控制块PCB

又被成为进程描述符。

      2.1    部分变量解释

1237    void *stack;                     //进程堆栈
1239    unsigned int flags;              /* per process flags, defined below */    
1242    #ifdef CONFIG_SMP              //代表多核情况
1251    int on_rq;                    //运行队列
1253    int prio, static_prio, normal_prio; //优先级
1255    const struct sched_class *sched_class;//进程调度相关
1413   /* 文件系统信息 */
1414	struct fs_struct *fs;
1415   /*打开的文件描述符列表 */
1416	struct files_struct *files;

1419    /*信号处理相关*/
1420	struct signal_struct *signal;
1421	struct sighand_struct *sighand;
 

      2.2  进程链表

1295    struct list_head tasks;    
//结构声明如下
23    struct list_head {
24          struct list_head *next, *prev;
25    };

作用,构建双向链表:

分析进程创建的过程---linux内核学习笔记(六)

  2.3  

//进程内存相关
1301    struct mm_struct *mm, *active_mm;

//记录进程的PID,以及PID的哈希表
1330    pid_t pid;
1331    pid_t tgid;
1360	struct pid_link pids[PIDTYPE_MAX];

  2.4  进程的父子关系描述部分

1338     * pointers to (original) parent process, youngest child, younger sibling,
1339     * older sibling, respectively.  (p->father can be replaced with
1340     * p->real_parent->pid)
1341     */
1342    struct task_struct __rcu *real_parent; /* real parent process */
1343    struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
1344    /*
1345     * children/sibling forms the list of my natural children
1346     */
1347    struct list_head children;    /* list of my children */
1348    struct list_head sibling;    /* linkage in my parent's children list */
1349    struct task_struct *group_leader;    /* threadgroup leader */

其关系描述可以简化为如下:

分析进程创建的过程---linux内核学习笔记(六)

  2.5  进程与cpu相关状态

1412    struct thread_struct thread;
//结构实现
468     struct thread_struct {   
470            struct desc_struct    tls_array[GDT_ENTRY_TLS_ENTRIES];
471            unsigned long        sp0;
472            unsigned long        sp;
......
483         unsigned long        ip;
......
525    };                         

类似与之前所学的mypcb中记录的IP和SP


内容三:进程的创建

  linux系统允许任何一个用户创建一个子进程,创建之后,子进程存于系统之中,并且独立于父进程。该子进程可以接受调度,可以分配得到系统资源。系统中,除了0号进程以外(0号进程是由系统创建的),任何一个进程都是由其他进程创建的。所以说 Linux中,1号进程是所有用户态进程的祖先,0号进程是所有内核线程的祖先。

  • fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;

  Linux是通过复制父进程来创建一个新进程,进程创建的大致框架就是:复制PCB,对复制的PCB进行修改、分配新的内核堆栈...

  3.1  fork函数

 

//函数原型
pid_t fork( void);

 

fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。

这个fork函数的最后的落脚点是do_fork

1703  SYSCALL_DEFINE0(fork)
1704  {
1705  #ifdef CONFIG_MMU
1706      return do_fork(SIGCHLD, 0, 0, NULL, NULL);
1707  #else
1708      /* can not support in nommu mode */
1709      return -EINVAL;
1710  #endif
1711  }

  3.2  clone过程分析

  clone如果不管条件编译的内容,实际上也是执行了do_fork

1746    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);

  所以分析从do_fork开始:按照老师所说的主要框架来找相关代码进行确认。

    3.2.1  复制:

1651    p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace);

  copy_process中dup_task_struct函数复制了整个PCB。

1240    p = dup_task_struct(current);

    3.2.2  对PCB进行修改

  copy_process函数中,从dup_task_struct函数后面的都是对复制的PCB进行修改,包括初始化内存、文件系统、信号等等,其中理解的关键是:

1396    retval = copy_thread(clone_flags, stack_start, stack_size, p);
135    struct pt_regs *childregs = task_pt_regs(p);    //SAVE_ALL地址
    //修改SP
139    p->thread.sp = (unsigned long) childregs;
140    p->thread.sp0 = (unsigned long) (childregs+1);
    //还处于父进程中,将父进程SAVE_ALL的内容拷贝过来
159    *childregs = *current_pt_regs();
    //修改IP,所以产生的子进程在系统调用处理过程中从ret_from_fork处开始执行。
164    p->thread.ip = (unsigned long) ret_from_fork;

  3.2.3  ret_from_fork

  程序接下来跳转到ret_from_fork。从此执行时内核堆栈只有之前存的一点点内容,中间的代码作用是怎么样的呢?

290  ENTRY(ret_from_fork)
291    CFI_STARTPROC
292    pushl_cfi %eax
293    call schedule_tail
294    GET_THREAD_INFO(%ebp)
295    popl_cfi %eax
296    pushl_cfi $0x0202        # Reset kernel eflags
297    popfl_cfi
298    jmp syscall_exit
299    CFI_ENDPROC
300  END(ret_from_fork)

可以查看 jmp syscall_exit,到syscall_exit时候,堆栈状态与系统调用前的是一样的。所以可以推断前面的292~297就是填充堆栈内容。

505  syscall_exit:
506    LOCKDEP_SYS_EXIT
507    DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
508                    # setting need_resched or sigpending
509                    # between sampling and the iret
510    TRACE_IRQS_OFF
511    movl TI_flags(%ebp), %ecx
512    testl $_TIF_ALLWORK_MASK, %ecx    # current->work
513    jne syscall_exit_work

  所以新创建的子进程获得cpu使用权时是从ret_from_fork开始执行,再返回到用户态,对应的是子进程的用户空间。

 

 内容四:GBD验证分析过程

  自己搭建的系统上调试。

  3.1:按老师要求更改menu,并make rootfs

 分析进程创建的过程---linux内核学习笔记(六)

   3.2:程序运行效果

分析进程创建的过程---linux内核学习笔记(六)

  3.3 GDB调试(图太多了,选了其中两张)

    设置断点,并运行至第一个断点

分析进程创建的过程---linux内核学习笔记(六)

  运行到第二个断点,后面跟踪不到了。

分析进程创建的过程---linux内核学习笔记(六)

 

内容五:小结

  通过本次课的学习,自己掌握了如下的知识:

  1:了解了进程的描述符,结构体 task_struct。对其中比较重要的声明有了一定的了解。

  2:初步的学习了进程启动的流程。

    2.1:通过fork创建一个新的进程,fork函数的特点是一次调用,两次返回。而其最终的落脚点是do_fork.

    2.2:  在进程创建的过程中,通过copy_process中的dup_task_struct复制父进程的PCB,然后紧接着修改需要修改的内容。

    2.3: 修改的内容有很多,其中很重要的一点是在copy_thread中,将子进程的IP设置为ret_from_fork。

    2.4: 所以当子进程获得CPU的使用权时,子进程是从ret_from_fork这个标号处开始执行,执行一系列堆栈内容填充指令后,跳转到syscall_exit,最后切换

到用户态,此时则处于子进程的用户空间中。

  3: 学习方法:linux的具体实现代码很多,细节也很多,如果直接看代码,很容易忽略主干,所以老师说,应该在看代码之前,思考并找出代码实现功能的基本框架,

然后在代码中找证据。