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

    分析Linux内核创建一个新进程的过程

    • Linux中创建进程一共有三个函数:
    
      1. fork,创建子进程
    
      2. vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。
    
      3. clone,主要用于创建线程
    
    

    一、实验部分

    • 增加fork命令,运行MenuOS 如下:

    • 使用gdb进行跟踪调试,并设置断点

    • 跟踪调试过程

    停在的do_fork()的位置上

    停在copy_process

    停在dup_task_struct

    停在copy_thread

    二、代码分析

    • 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;
    
        // ...
        
        // 复制进程描述符,返回创建的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代码分析
    
    /*
        创建进程描述符以及子进程所需要的其他所有数据结构
        为子进程准备运行环境
    */
    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的步骤
    1. 检查各种标志位

    2. 调用dup_task_struct复制一份task_struct结构体,作为子进程的进程描述符。

    3. 检查进程的数量限制。

    4. 初始化定时器、信号和自旋锁。

    5. 初始化与调度有关的数据结构,调用了sched_fork,这里将子进程的state设置为TASK_RUNNING。

    6. 复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等。

    7. 调用copy_thread,这又是关键的一步,这里设置了子进程的堆栈信息。

    8. 为子进程分配一个pid

    9. 设置子进程与其他进程的关系,以及pid、tgid等。这里主要是对线程做一些区分。

    三、课本知识

    • 运行状态(TASK_RUNNING)
      当进程正在被CPU执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态(running)。进程可以在内核态运行,也可以在用户态运行。当系统资源已经可用时,进程就被唤醒而进入准备运行状态,该状态称为就绪态。这些状态(图中中间一列)在内核中表示方法相同,都被成为处于TASK_RUNNING状态。
      可中断睡眠状态(TASK_INTERRUPTIBLE)
      当进程处于可中断等待状态时,系统不会调度该进行执行。当系统产生一个中断或者释放了进程正在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪状态(运行状态)。
    • 暂停状态(TASK_STOPPED)
      当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。
    • 僵死状态(TASK_ZOMBIE)
      当进程已停止运行,但其父进程还没有询问其状态时,则称该进程处于僵死状态。
    • 不可中断睡眠状态(TASK_UNINTERRUPTIBLE)
      与可中断睡眠状态类似。但处于该状态的进程只有被使用wake_up()函数明确唤醒时才能转换到可运行的就绪状态。
      当一个进程的运行时间片用完,系统就会使用调度程序强制切换到其它的进程去执行。另外,如果进程在内核态执行时需要等待系统的某个资源,此时该进程就会调用
      sleep_on()或sleep_on_interruptible()自愿地放弃CPU的使用权,而让调度程序去执行其它进程。进程则进入睡眠状
      态(TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE)。
      只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换操作。在内核态下运行的进程不能被其它进程抢占,而且一个进程不能改变另一个进程的状态。为了避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断。
  • 相关阅读:
    Maven笔记之面试题合集
    Maven笔记之核心概念及常用命令
    UML中的关系
    RocketMq核心概念
    linux安装rocketMq(包括安装maven,JDK)
    linux安装JDK,配置环境变量
    ASP.NET Core读取appsettings.json配置文件信息
    ASP.NET Core获取客户端IP地址
    ASP.NET Core根据环境切换NLog配置
    ASP.NET Core使用NLog记录日志
  • 原文地址:https://www.cnblogs.com/destiny-love/p/11787262.html
Copyright © 2011-2022 走看看