zoukankan      html  css  js  c++  java
  • 2019-2020-1 20199319《Linux内核原理与分析》第七周作业

    进程的描述和进程的创建

    进程的描述

    1、操作系统内核实现操作系统的三大管理功能:

    • 进程管理
    • 内存管理
    • 文件系统。
      其中最核心的功能是进程管理。

    2、对进程的描述:在操作系统原理中,通过进程控制块PCB描述进程。在Linux内核中通过一个数据结构struct task_struct来描述进程,称其为进程描述符。
    3、对进程状态的描述:

    • 在操作系统原理中,进程有就绪态、运行态和阻塞态3种基本状态;
    • 在Linux内核中,当时用fork()系统调用来创建一个进程时,新进程的状态是TASK_RUNNING(就绪态但没有运行)。就绪态和运行态在Linux内核中都是TASK_RUNNING状态,此状态下进程是可运行的,也就是就绪态,是否在运行取决于它有没有获得CPU的控制权,即这个进程有没有在CPU中实际执行。如果在CPU中实际执行了,进程状态就是运行态;如果被内核调度出去了,在等待队列里就是就绪态。
    • 对于一个正在运行的进程,调用用户态库函数exit()会陷入内核执行该内核函数do_exit(),进程会进入TASK_ZOMBIE状态,即进程的终止状态。TASK_ZOMBIE状态的进程一般叫做僵尸进程,Linux内核会在适当的时候把僵尸进程处理掉,然后释放进程描述符。
    • 一个正在运行的进程在等待特定事件或资源时会进入阻塞态,阻塞态分为两种:
      • TASK_INTERRUPTIBLE
      • TASK_UNINTERRUPTIBLE。
        前者可以被信号和wake_up()唤醒,后者只能被wake_up()唤醒。

    进程的创建

    1、0号进程的初始化:
    init_task为第一个进程(0号进程)的进程描述符结构体变量,它的初始化是通过硬编码方式固定下来的,除此之外的其它进程的初始化都是通过do_fork复制父进程的方式初始化的。
    2、fork、vfork、clone这3个系统调用和kernel_thread内核函数都可以创建一个新进程,而且都是通过do_fork函数来创建进程的,只不过传递的参数不同。
    3、进程的创建过程:
    利用fork()系统调用来创建新进程,把当前进程的描述符等相关进程资源复制一份,从而产生一个子进程,并根据子进程的需要对复制的进程描述符做一些修改,然后把创建好的子进程放入运行队列。在进程调度时,新创建的子进程处于就绪状态有机会被调度执行。
    4、进程创建过程中的重要函数或数据结构:

    • do_fork():主要完成调用copy_process()复制父进程信息、获得pid、调用wake_up_new_task将子进程加入调度器队列等待获得分配CPU资源运行、通过clone_flags标志做一些辅助工作。
    • copy_process():主要完成调用dup_task_struct复制当前进程(父进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(就绪态)、采用写时复制技术逐一复制所有其他进程资源、调用copy_thread初始化子进程内核栈、设置子进程pid等。
    • dup_task_struct():复制当前进程(父进程)描述符task_struct和copy_thread初始化子进程内核栈。
    • thread_info:小型的进程描述符,占据连续的两个页框,通过task指针指向进程描述符。内核栈有高地址到低地址增长,thread_info结构有低地址到高地址增长。
    • copy_thread():完成内核栈关键信息的初始化。如果创建的是内核线程,则子进程开始执行的起点是ret_from_kernel_thread;如果创建的是用户态进程,则子进程开始执行的起点是ret_from_fork。

    通过实验跟踪分析进程创建的过程

    增加fork系统调用

    实验方法与上次实验类似,首先删除并克隆一份新的menu,然后将test.c覆盖掉,因为之前用过test.c可能会有影响。进入menu执行make rootfs,命令如下:

    cd LinuxKernel
    rm -rf menu
    git clone https://github.com/mengning/menu.git
    cd menu
    make rootfs
    

    编译运行出来之后可以查看到列表中增加了fork,执行fork可以看到父进程和子进程输出信息。

    启动内核

    启动内核,可以看到窗口被冻结起来

    gdb跟踪

    启动gdb,将内核加载进来,建立连接,并在sys_clone、do_fork、dup_task_struct、copy_process、copy_thread、ret_from_fork处各设置断点。

    然后开始执行,发现只输出了一个命令描述,后面并没有执行,而是停在了sys_clone这里。

    再继续执行,停在do_fork处。

    再继续执行,停在copy_process处。

    再继续执行,停在dup_task_struct函数,进入dup_task_struct内部,将当前进程内核压栈压得那一部分寄存器复制到子进程中,以及赋值子进程的起点。

    单步执行:

    关键代码分析

    do_fork()

    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;//子进程pid
    
            ...
    
        p = copy_process(clone_flags, stack_start, stack_size,
                 child_tidptr, NULL, trace);//创建子进程的描述符和执行时所需的其他数据结构
    
        if (!IS_ERR(p)) {//如果copy_process执行成功
            struct completion vfork;//定义完成量
            struct pid *pid;
                    ...
            pid = get_task_pid(p, PIDTYPE_PID);//获得task结构体中的pid
            nr = pid_vnr(pid);//根据pid结构体中获得进程pid
                    ...
                    //如果clone_flags包含CLONE_VFORK标志,就将完成量vfork赋值给进程描述符中的vfork_done字段,此处只是对完成量进行初始化
            if (clone_flags & CLONE_VFORK) {
                p->vfork_done = &vfork;
                init_completion(&vfork);
                get_task_struct(p);
            }
    
            wake_up_new_task(p);//将子进程添加到调度器的队列,使之有机会获得CPU
    
            /* forking complete and child started to run, tell ptracer */
                    ...
                    //如果clone_flags包含CLONE_VFORK标志,就将父进程插入等待队列直到子进程调用exec函数或退出,此处是具体的阻塞
            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;//返回子进程pid(父进程fork函数返回值为子进程pid原因)
    }
    

    dup_task_struct()

    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;
    
        tsk = alloc_task_struct_node(node);//为子进程创建进程描述符分配存储空间
            ...
        ti = alloc_thread_info_node(tsk, node);//创建了两个页,一部分存放thread_info,一部分就是内核堆栈
            ...
        err = arch_dup_task_struct(tsk, orig);//复制父进程的task_struct信息
            ...
        tsk->stack = ti;//将栈底的值赋给新结点的stack
            ...
            //对子进程的thread_info初始化(复制父进程thread_info,然后将task指针指向子进程的进程描述符)
        setup_thread_stack(tsk, orig);  //这里只是复制thread_info,而非复制内核堆栈
            ...
        return tsk;//返回新创建的进程描述符指针
            ...
    }
    

    copy_thread()

    int copy_thread(unsigned long clone_flags, unsigned long sp,
        unsigned long arg, struct task_struct *p)
    {
        struct pt_regs *childregs = task_pt_regs(p);
        struct task_struct *tsk;
        int err;
    
        p->thread.sp = (unsigned long) childregs;
        p->thread.sp0 = (unsigned long) (childregs+1);
        memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
    
        if (unlikely(p->flags & PF_KTHREAD)) {
            /* kernel thread */
            memset(childregs, 0, sizeof(struct pt_regs));
                    //如果创建的是内核线程,则从ret_from_kernel_thread开始执行
            p->thread.ip = (unsigned long) ret_from_kernel_thread;
            task_user_gs(p) = __KERNEL_STACK_CANARY;
            childregs->ds = __USER_DS;
            childregs->es = __USER_DS;
            childregs->fs = __KERNEL_PERCPU;
            childregs->bx = sp; /* function */
            childregs->bp = arg;
            childregs->orig_ax = -1;
            childregs->cs = __KERNEL_CS | get_kernel_rpl();
            childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
            p->thread.io_bitmap_ptr = NULL;
            return 0;
        }
    
            //复制内核堆栈(复制父进程的寄存器信息,即系统调用int指令和SAVE_ALL压栈的那一部分内容)
        *childregs = *current_pt_regs();
        childregs->ax = 0;//将子进程的eax置0,所以fork的子进程返回值为0
            ...
            //ip指向ret_from_fork,子进程从此处开始执行
        p->thread.ip = (unsigned long) ret_from_fork;
        task_user_gs(p) = get_user_gs(current_pt_regs());
            ...
        return err;
    }
    

    总结

    进程的创建过程大致是复制进程描述符、一一复制其它进程资源(采用写时复制技术)、分配子进程的内核堆栈并对内核堆栈关键信息进行初始化。
    问题是fork、vfork、clone这三种系统调用的不同,通过查阅资料发现:

    • fork():子进程拷贝父进程的数据段,代码段,父子进程的执行次序不确定;
    • vfork():创建的子进程与父进程共享数据段,而且子进程将先于父进程运行;
    • clone():是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享。
  • 相关阅读:
    EasyNVR摄像机网页无插件直播方案H5前端构建之:通道内部搜索功能的实现方案与代码
    EasyNVR摄像机网页无插件直播方案H5前端构建之:bootstrap弹窗功能的实现方案与代码
    EasyNVR摄像机网页无插件直播方案H5前端构建之:bootstrap-datepicker日历插件的实时动态展现
    EasyNVR摄像机网页无插件直播方案H5前端构建之:如何播放HLS(m3u8)直播流
    EasyNVR摄像机网页无插件直播方案H5前端构建之:区分页面是自跳转页面还是分享页面
    EasyNVR摄像机网页无插件直播方案H5前端构建之:如何区分PC端和移动端
    EasyDSS高性能流媒体服务器开发RTMP直播同步输出HLS(m3u8)录像功能实现时移回放的方案
    Stack Overflow: The Architecture
    The week in .NET
    十分钟轻松让你认识Entity Framework 7
  • 原文地址:https://www.cnblogs.com/fanxiaonan/p/11767757.html
Copyright © 2011-2022 走看看