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

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

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

用户空间和内核空间

Linux将内存分为两个部分,一个是用户空间和内核空间

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

 

 

 

Linux内部结构可以分为三个部分

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

 

 

 

程序在执行过程中通常有用户态和内核态两种状态,CPU对处于内核态根据上下文环境进一步细分,因此有了下面三种状态:

(1)内核态,运行于进程上下文,内核代表进程运行于内核空间。
2)内核态,运行于中断上下文,内核代表硬件运行于内核空间。
3)用户态,运行于用户空间。

 

上下文context: 上下文简单说来就是一个环境。

  用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递 很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存 器值、变量等。所谓的进程上下文,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

  相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。

1)用户级上下文: 正文、数据、用户堆栈以及共享存储区;
2)寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP)
3)系统级上下文: 进程控制块task_struct、内存管理信息(mm_structvm_area_structpgdpte)、内核栈。

当发生进程调度时,进行进程切换就是上下文切换(context switch).操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。而系统调用进行的模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。

 

二、 fork()函数系统调用

fork()函数又叫计算机程序设计中的分叉函数,fork是一个很有意思的函数,它可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

Fork()函数内核处理过程

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

 

 

 

其中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;
}

 

其中copy_process函数执行步骤

  1. p = dup_task_struct(current); 为新进程创建一个内核栈、thread_iofotask_struct,这里完全copy父进程的内容,所以到目前为止,父进程和子进程是没有任何区别的。
  2. 为新进程在其内存上建立内核堆栈
  3. 对子进程task_struct任务结构体中部分变量进行初始化设置,检查所有的进程数目是否已经超出了系统规定的最大进程数,如果没有的话,那么就开始设置进程描诉符中的初始值,从这开始,父进程和子进程就开始区别开了。
  4. 把父进程的有关信息复制给子进程,建立共享关系
  5. 设置子进程的状态为不可被TASK_UNINTERRUPTIBLE,从而保证这个进程现在不能被投入运行,因为还有很多的标志位、数据等没有被设置
  6. 复制标志位(falgs成员)以及权限位(PE_SUPERPRIV)和其他的一些标志
  7. 调用get_pid()给子进程获取一个有效的并且是唯一的进程标识符PID
  8. return ret_from_fork;返回一个指向子进程的指针,开始执行

 

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

 

中断分硬件中断和软件中断,forkexecve系统调用都是利用陷阱(trap)这种软件中断方式主动从用户态进入内核态的

 

execve系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve系统调用通常与 fork系统调用配合使用。从一个进程中启动另一个程序时,通常是先fork一个子进程,然后在子进程中使用 execve变为运行指定程序的进程。我们在shell中输入ls等命令时就触发了execve系统调用,调用关系如下

 

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

 

  • 信号处理表:将父进程的信号处理表复制过来(可能已经复制了,也可能没有复制,通过检查信号处理表的共享参数就可以知道)。信号处理分3种情况:一对该信号不理睬,二对信号采取默认动作,三对信号采取指定动作。前面2种可以直接复制,最后一种,所谓的默认动作就是用户指定了程序处理,这段程序的代码必然是父进程自己拥有的,他的位置就存在用户空间中,下面我们会放弃继承到的父进程用户空间,对第3种情况的处理就是将其改成默认处理。所以,在新建的子进程中,对信号如果子进程没有采取指定处理,那么一律都会是默认处理,当然如果父进程对某个信号采取了不理睬,子进程也会不理睬,除非子进程后来又做了修改
  • 用户空间,放弃原来的用户空间(子进程可能有自己的页面,或者就是通过指针共享了父进程的页面)这些一律放弃,将进程控制块task_struct中对用户空间的描述的数据结构mm_struct的下属结构vma全部置0.简而言之就是现在子进程的用户空间是个空架子,一个页面也没有,父进程空间被放弃
  • 进程控制块task_struct中有file的指针记录了进程打开的文件信息,子进程对继承到的文件采取关闭应当关闭的信息。file的数据结构中有位图记录了应当关闭的文件,子进程放弃这些文件。

 

最终,调用start_thread()后,新程序的ipsp存入堆栈,覆盖掉了之前的ip,sp,返回到子进程用户态后,就开始执行了装载的新代码

 

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

 

内核实现了很多的系统调用函数, 这些函数会有自己的名字, 以及编号. 用户要调用系统调用, 首先需要使用  int 0x80 触发软中断. 这个指令会在0x80代表十进制的128, 所以这个指令会找终端向量表的128, 找到以后, 跳转到相应的函数, 这个处理函数就是system_call. 这个中断向量表的设置, 是在操作系统初始化的时候, 通过trap_init()函数设置的. . 在进入中断处理函数system_call以后, 首先要进行一般的中断处理流程, 即保护现场. 这个体现在指令SAVE_ALL(494). 然后有一个重要的函数调用 call *sys_call_table(,%eax,4). 这个表示查找系统调用函数表(), 然后调用相应的系统调用函数. 对于32位的系统, 函数位置存了4Bytes, eax中是我们传入的系统调用号, 所以4*eax,就可以找到对应的系统调用函数, 执行函数. 之后还需要进行返回值的保存等工作.

 

   完成了上面的第一阶段内容, jne syscall_exit_work. 这条指令导致了在系统调用函数执行完以后, 可能会进入syscall_exit_work. 我们先考虑没有进入这个函数的情况. 如果没有进入, 下面就有restore_all, 会进行恢复现场的工作, 这个和刚进入中断处理函数的时候是对应的, 一开始保护现场, 保存了寄存器的值. 现在当然要恢复寄存器到原来的值. 最后通过irq_return: INTERRUPT_RETURN, 效果等同与iret, 返回到用户态程序继续执行.

 

 

 

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

上一篇:Linux 卸载CUDA Toolkit


下一篇:ansible管理windows主机