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

NeverAgain 2020-06-14

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

  • 以fork和execve系统调用为例分析中断上下文的切换
  • 分析execve系统调用中断上下文的特殊之处
  • 分析fork子进程启动执行时进程上下文的特殊之处
  • 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

一.fork分析

fork:fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。

使用如下代码进行分析上下文切换:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main (){
          pid_t pid;
          pid = fork();
          if (pid < 0){
                  printf("error in fork!\n");
          }
          else if (pid == 0){
                  printf("i am the child process, my process id is %d\n",getpid());
          }
          else{
                  printf("i am the parent process, my process id is %d\n",getpid());
         }
         return 0;
}

将以上代码静态编译为可执行程序fork,在_do_fork上断点,运行程序:

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

 查看函数调用堆栈:

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

 可以看到,在执行fork函数时,最终通过_do_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的执行过程大致如下:

1.调用 copy_process 为子进程复制出一份进程信息

2.如果是 vfork(设置了CLONE_VFORK和ptrace标志)初始化完成处理信息

3.调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU

4.如果是 vfork,父进程等待子进程完成 exec 替换自己的地址空间

其中copy_process做了如下操作:

1.调用 dup_task_struct 复制当前的 task_struct

2.检查进程数是否超过限制

3.初始化自旋锁、挂起信号、CPU 定时器等

4.调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING

5.复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等

6.调用 copy_thread_tls 初始化子进程内核栈

7.为新进程分配并设置新的 pid

二. execve分析

execve函数作用是执行一个新的程序,程序可以是二进制的可执行程序,也可以是shell、pathon脚本

使用如下代码进行分析execve:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[]){
    int pid;
    pid = fork();
    if (pid < 0){
        fprintf(stderr, "Fork Failed!\n");
        exit(-1);
    }
    else if (pid == 0){
        execlp("/bin/ls", "ls", NULL);
    }
    else{
        printf("Child Complete!\n");
        exit(0);
    }
}

断点在do_execve,可以看到函数调用堆栈如下: 

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

 查看do_execve方法可以发现其调用链如下:do_execve -> do_execveat_common -> _do_execve_file。最终 的逻辑在_do_execve_file中实现。

三、fork和execve的区别 

fork在原进程的基础上创建一个新进程,对于新创建的进程需要重新设置上下文环境,而execve会覆盖原进程的上下文环境而非创建一个新进程。

四、Linux系统的一般执行过程

1)正在运?的?户态进程X

2)发?中断(包括异常、系统调?等),CPU完成以下动作。

save cs:eip/ss:esp/eflags:当前CPU上下?压?进程X的内核堆栈。

load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack):加载当前进程内核堆栈相关信息,跳转到中断处理程序,即中断执?路径的起点。

3)SAVE_ALL,保存现场,此时完成了中断上下?切换,即从进程X的?户态到进程X的内核态。

4)中断处理过程中或中断返回前调?了schedule函数,其中的switch_to做了关键的进程上下?切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程
(本例假定为进程Y)的内核堆栈,并完成了进程上下?所需的EIP等寄存器状态切换。

5)标号1,即前述3.18.6内核的swtich_to代码第50?“”1:\t“ ”(地址为switch_to中的“$1f”),之后开始运?进程Y(这?进程Y曾经通过以上步骤被切换出去,因此可以
从标号1继续执?)

6)restore_all,恢复现场,与(3)中保存现场相对应。注意这?是进程Y的中断处理过程中,?(3)中保存现场是在进程X的中断处理过程中,因为内核堆栈从进程X
切换到进程Y了

7)iret - pop cs:eip/ss:esp/eflags,从Y进程的内核堆栈中弹出(2)中硬件完成的压栈内容。此时完成了中断上下?的切换,即从进程Y的内核态返回到进程Y的?户态

8)继续运??户态进程Y

相关推荐