• 结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程


    一、以fork和execve系统调用为例分析中断上下文的切换

    1.fork系统调用

       一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。do_fork的代码如下:

    long _do_fork(struct kernel_clone_args *args)
    {
        u64 clone_flags = args->flags;
        struct completion vfork;
        struct pid *pid;
        struct task_struct *p;
        int trace = 0;
        long nr;
    
        /*
         * Determine whether and which event to report to ptracer.  When
         * called from kernel_thread or CLONE_UNTRACED is explicitly
         * requested, no event is reported; otherwise, report if the event
         * for the type of forking is enabled.
         */
        if (!(clone_flags & CLONE_UNTRACED)) {
            if (clone_flags & CLONE_VFORK)
                trace = PTRACE_EVENT_VFORK;
            else if (args->exit_signal != SIGCHLD)
                trace = PTRACE_EVENT_CLONE;
            else
                trace = PTRACE_EVENT_FORK;
    
            if (likely(!ptrace_event_enabled(current, trace)))
                trace = 0;
        }
        p = copy_process(NULL, trace, NUMA_NO_NODE, args);
        add_latent_entropy();
    
        if (IS_ERR(p))
            return PTR_ERR(p);
    
        /*
         * Do this prior waking up the new thread - the thread pointer
         * might get invalid after that point, if the thread exits quickly.
         */
        trace_sched_process_fork(current, p);
        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);
    
        if (clone_flags & CLONE_PARENT_SETTID)
            put_user(nr, args->parent_tid);
        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }
        wake_up_new_task(p);
    
        /* forking complete and child started to run, tell ptracer */
        if (unlikely(trace))
            ptrace_event_pid(trace, pid);
        if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }
    
        put_pid(pid);
        return nr;
    }

      do_fork函数主要完成了调用copy_process复制父进程、获得pid、调用wake_up_new_task将子进程加入就绪队列等待调度执行等。我们知道,在Linux中,除了0号进程由手工创建外,其他进程都是通过复制已有进程创建而来,而这正是fork的主要工作,具体的任务交由copy_process完成。

      copy_process函数主要完成了调用dup_task_struct复制当前进程(父进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时⼦进程置为就绪态)、采⽤写时复制技术逐⼀复制所有其他进程资源、调⽤copy_thread_tls初始化子进程内核栈、设置子进程pid等。其中copy_thread_tls所做的工作是关键。我们知道执行fork系统调用之后,会由内核态返回两次:一次返回到父进程,这与一般的系统调用返回流程别无二致;而另一次则返回到子进程,为了实现这一点,就需要为子进程构造出合适的执行上下文,也就是初始化其内核栈和进程描述符的thread字段。这正是copy_thread_tls的任务。

      ⽗进程通过fork系统调⽤进⼊内核_do_fork函数,复制进程描述符及相关进程资源(采⽤写时复制技术)、分配⼦进程的内核堆栈并对内核堆栈和thread等进程关键上下⽂进⾏初始化,最后将⼦进程放⼊就绪队列,fork系统调⽤返回;⽽⼦进程则在被调度执⾏时根据设置的内核堆栈和thread等进程关键上下⽂开始执⾏。

    二、分析execve系统调用中断上下文的特殊之处

      中断分硬件中断和软件中断,fork和execve系统调用都是利用陷阱(trap)这种软件中断方式主动从用户态进入内核态的execve系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve系统调用通常与 fork系统调用配合使用。从一个进程中启动另一个程序时,通常是先fork一个子进程,然后在子进程中使用 execve变为运行指定程序的进程。我们在shell中输入ls等命令时就触发了execve系统调用,调用关系为:sys_execve() -> do_execve() -> do_execveat_common() -> do_execve_file -> exec_binprm() -> search_binary_handler() -> load_elf_binary() -> start_thread()。

      其中,上下文切换的特殊之处主要发生在调用exec_binprm后。search_binary_handler会寻找符合文件格式对应的解析模块,然后装入elf映像,之后要做的就是放弃以前从父进程继承来的资源。主要是对信号处理表,用户空间和文件3大资源的处理。

    三、分析fork子进程启动执行时进程上下文的特殊之处

      fork系统调用将创建一个与父进程几乎一样的新进程,之后继续执行下面的指令。程序可以根据 fork() 的返回值,确定当前处于父进程中,还是子进程中——在父进程中,返回值为新创建子进程的进程 ID,在子进程中,返回值是0,其中代码do_fork函数已经展示,而fork系统调用特殊之处在于其创建了一个新的进程,并且在父子进程中各有一次返回。对于fork的父进程来说,fork系统调用和普通的系统调用基本相同。但是对fork子进程来说,需要设置子进程的进程上下文环境,这样子进程才能从fork系统调用后返回。
    而对于execve而言,由于execve使得新加载可执⾏程序已经覆盖了原来⽗进程的上下⽂环境,而原来的中断上下文就是保存的是原来的、被覆盖的进程的上下文,因此需要修改原来的中断上下文,使得系统调用返回后能够指向现在加载的这个可执行程序的入口。

    四、以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

    1、正在运行的用户态进程X

    2、发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

    3、SAVE_ALL //保存现场

    4、中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换

    5、标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)

    6、restore_all //恢复现场

    7、iret - pop cs:eip/ss:esp/eflags from kernel stack

    8、继续运行用户态进程Y

  • 相关阅读:
    工具函数(代码块的大小,代码块起始地址,提升进程权限)
    在共享DLL中使用MFC 和在静态库中使用MFC的区别
    虚拟机检测绕过总结--不定时更新
    OSGI原形(.NET)
    iOS开发技术分享(1)— iOS本地数据存储
    将JSON映射为实体对象(iOS篇)
    灵活的路由(上)
    github开源项目
    EF里查看/修改实体的当前值、原始值和数据库值以及重写SaveChanges方法记录实体状态
    实体能否处于非法状态
  • 原文地址:https://www.cnblogs.com/sovegetabable/p/13137697.html
Copyright © 2020-2023  润新知