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


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

    黄韧(原创作品转载请注明出处)

    《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

    【学习笔记】

    进程的描述

    一、进程描述符task_struct数据结构

    1.操作系统三大功能

    • 进程管理
    • 内存管理
    • 文件系统

    2.进程控制块PCB——task_struct

    也叫进程描述符,为了管理进程,内核需要对每个进程进行描述,它就提供了内核所需了解的进程信息。

    struct task_struct数据结构很庞大,1235行~1644行

    3.Linux进程状态

    Linux进程的状态与操作系统原理中的描述的进程状态有所不同

    操作系统状态:

    • 就绪态
    • 运行态
    • 阻塞态

    为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。

    • struct task_struct数据结构很庞大
    • Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_RUNNING,为什么呢?
    • 进程的标示pid
    • 所有进程链表struct list_head tasks;     内核的双向循环链表的实现方法 - 一个更简略的双向循环链表
    • 程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
    • Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈               

          进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈,那为什么PCB中没有用户态堆栈?用户态堆栈是怎么设定的?

          内核控制路径所用的堆栈很少,因此对栈和Thread_info来说,8KB足够了

    •  struct thread_struct thread; //CPU-specific state of this task
    • 文件系统和文件描述符
    • 内存管理——进程的地址空间

    Linux内核状态转换图:

    双向循环链表图如下:

    进程的父子关系直观图:

    进程的创建

    1.进程的创建概览及fork一个进程的用户态代码

    (1)进程的起源再回顾

    • 道生一(start_kernel...cpu_idle)
    • 一生二(kernel_init和kthreadd)
    • 二生三(即前面的0、1、2三个进程)
    • 三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)

    (2)0号进程手工写,1号进程复制、加载init程序

    (3)shell命令行是如何启动进程的

    fork一个子进程的代码:

    复制代码
     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <unistd.h>
     4 int main(int argc, char * argv[])
     5 {
     6     int pid;
     7     /* fork another process */
     8     pid = fork();
     9     if (pid < 0)   出错处理
    10     { 
    11         /* error occurred */
    12         fprintf(stderr,"Fork Failed!");
    13         exit(-1);
    14     } 
    15     else if (pid == 0) 
    16     {
    17         /* child process */  子进程   pid=0时 if和else都会执行  fork系统调用在父进程和子进程各返回一次
    18         printf("This is Child Process!
    ");
    19     } 
    20     else 
    21     {  
    22         /* parent process  */
    23         printf("This is Parent Process!
    ");
    24         /* parent will wait for the child to complete*/
    25         wait(NULL);
    26         printf("Child Complete!
    ");
    27     }
    28 }
    复制代码

    2.理解进程创建过程复杂代码的方法

    (1)系统调用再回顾

    (2)fork的子进程是从哪里开始执行的?

    与基于mykernel写的精简内核对照起来。

    (3)创建一个新进程在内核中的执行过程

    • fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
    • Linux通过复制父进程来创建一个新进程,那么这就给我们理解这一个过程提供一个想象的框架:
    • 复制一个PCB——task_struct
      err = arch_dup_task_struct(tsk, orig);
    • 要给新进程分配一个新的内核堆栈
    ti = alloc_thread_info_node(tsk, node);
    tsk->stack = ti;
    setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
    • 要修改复制过来的进程数据,比如pid、进程链表等等都要改改吧,见copy_process内部。
    • 从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,父进程从系统调用中返回比较容易理解,子进程从系统调用中返回,那它在系统调用处理过程中的哪里开始执行的呢?这就涉及子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,这是在哪里设定的?copy_thread in copy_process
    1 *childregs = *current_pt_regs(); //复制内核堆栈
    2 childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!
    3  
    4 p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
    5 p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址

    (4)理解复杂事物要预设一个大致的框架。

    (5)创建新进程是通过复制当前进程来实现的。

    (6)设想创建新进程过程中需要做哪些事

    3.浏览进程创建过程相关的关键代码

    (1)系统调用内核处理函数sys_fork、sys_clone、sys_vfork

    最终都是执行do_fork()。

    do_fork()里的复制进程的函数:

    具体:

    打开复制PCB的具体函数:

    打开alloc_thread_info():

     

    拷贝内核堆栈数据和指定新进程的第一条指令地址。

    4.创建的新进程是从哪里开始执行的?

    (1)复制内核堆栈时

    打开pt_regs:

    int指令和SAVE_ALL压到内核栈的内容。

    下面分析entry_32.S,也就是总控程序。

    5.使用gdb跟踪创建新进程的过程(见作业)

    三、课后作业

    fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建,do_fork完成了创建中的大部分工作,该函数调用copy_process()函数,然后让进程开始运行。copy_process()函数工作如下:

    • 1、调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同
    • 2、检查
    • 3、子进程着手使自己与父进程区别开来。进程描述符内的许多成员被清0或设为初始值。
    • 4、子进程状态被设为TASK_UNINTERRUPTIBLE,以保证它不会投入运行
    • 5、copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置
    • 6、调用alloc_pid()为新进程分配一个有效的PID
    • 7、根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等
    • 8、最后,copy_process()做扫尾工作并返回一个指向子进程的指针

    3.使用gdb跟踪分析一个fork系统调用内核处理函数sys_clone ,验证您对Linux系统创建一个新进程的理解,推荐在实验楼Linux虚拟机环境下完成实验。

    分析过程如下:

    更新menu代码到最新版、make rootfs,用help查看,新添加fork命令:

    使用gdb跟踪调试内核,在一些重要函数处设置断点:

    执行一个fork,会发现只输出一个fork的命令描述,后面并没有执行,因为它停在了sys_clone这个位置。

    4.特别关注新进程是从哪里开始执行的?为什么从哪里能顺利执行下去?即执行起点与内核堆栈如何保证一致。

    答:ret_from_fork;决定了新进程的第一条指令地址。

    原因如下:

    copy_process()主要完成进程数据结构,各种资源的初始化。

    p = dup_task_struct(current);
    • (省略的IF语句)检查clone_flags参数,防止无效的组合进入
    • p = dup_task_struct(current);调用dup_task_struct()为新进程创建一个内核栈
    • 判断权限及允许范围的代码
    • 对子进程的描述符初始化和复制父进程的资源给子进程
      • retval = sched_fork(clone_flags, p);完成调度相关的设置,将这个task分配给CPU
      • if (retval)语句群,复制共享进程的的各个部分
      • retval = copy_thread(clone_flags, stack_start, stack_size, p);复制父进程堆栈的内容到子进程的堆栈中去.这其中,copy_thread()函数中的语句p->thread.ip = (unsigned long) ret_from_fork;决定了新进程的第一条指令地址.
    总结原因也就是说:
    • 在ret_from_fork之前,也就是在copy_thread()函数中*childregs = *current_pt_regs();该句将父进程的regs参数赋值到子进程的内核堆栈,
    • *childregs的类型为pt_regs,里面存放了SAVE ALL中压入栈的参数
    • 故在之后的RESTORE ALL中能顺利执行下去.

    总结:

    • Linux通过复制父进程来创建一个新进程,通过调用do_fork来实现
    • Linux为每个新创建的进程动态地分配一个task_struct结构.
    • 为了把内核中的所有进程组织起来,Linux提供了几种组织方式,其中哈希表和双向循环链表方式是针对系统中的所有进程(包括内核线程),而运行队列和等待队列是把处于同一状态的进程组织起来
    • fork()函数被调用一次,但返回两次
  • 相关阅读:
    spring事务传播机制实例讲解
    一篇关于交叉编译的文章
    Java中的泛型方法
    层序遍历打印二叉树
    寻找第K大 网易2016实习研发工程师编程题
    二叉树的非递归遍历
    二叉树 网易2016实习研发工程师编程题
    比较重量 网易2016实习研发工程师编程题
    网络地址为172.16.0.0,采用子网掩码255.255.224.0 .以下说法正确的是?
    2.7链表 回文链表
  • 原文地址:https://www.cnblogs.com/huangbobo/p/5340844.html
Copyright © 2020-2023  润新知