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

1.以fork为例分析中断上下文的切换

正常的⼀个系统调⽤都是陷⼊内核态,再返回到⽤户态,然后继续执⾏系统调⽤后的下⼀条指令。 但是fork和其他系统调⽤不同之处是它在陷⼊内核态之后有两次返回,第⼀次返回到原来的⽗进程的位置继续向下执⾏,这和其他的系统调⽤是⼀样的。在⼦进程中fork也返回了⼀次,会返回到⼀个特定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调⽤返回到⽤户态,所以它稍微特殊⼀点。 下面来看一下fork的中断上下文切换。

 arch/x86/entry/syscalls/syscall_64.tbl  可以找到fork系统调⽤在x86-64系统中对应的内核处理函数为57号系统调⽤ __x64_sys_fork 。

查看fork.c代码:

/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return _do_fork(&args);
}
SYSCALL_DEFINE0(fork)
{
return _do_fork(&args);
}
SYSCALL_DEFINE0(vfork)
{
return _do_fork(&args);
}
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
{
return _do_fork(&args)
}

通过上⾯的代码可以看出forkvforkclone3个系统调⽤,以及do_forkkernel_thread内核函数都可以创建⼀个新进程,⽽且都是通过 _do_fork 函数来创建进程的,只不过传递的参数不同。下面来详细看一下 _do_fork 函数。

  _do_fork 函数主要完成了调⽤ copy_process() 复制⽗进程、获得pid、调⽤ wake_up_new_task 将⼦进程加⼊就绪队列等待调度执⾏等。

 copy_process() 是创建⼀个进程的主要的代码,详细实现代码见 /kernel/fork.c 代码中,这里只展示其粗略实现。

static __latent_entropy struct task_struct *copy_process(
struct pid *pid,
int trace,
int node,
struct kernel_clone_args *args)
{
//复制进程描述符task_struct、创建内核堆栈等
p = dup_task_struct(current, node);
/* copy all the process information */
shm_init_task(p);
…
// 初始化⼦进程内核栈和thread
retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
args->tls);
…
return p;//返回被创建的⼦进程描述符指针
}

copy_process函数主要完成了调⽤ dup_task_struct 复制当前进程(⽗进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时⼦进程置为就绪态)、采⽤写时复制技术逐⼀复制所有其他进程资源、调⽤ copy_thread_tls 初始化⼦进程内核栈、设置⼦进程pid等。其中最关键的就是 dup_task_struct 复制当前进程(⽗进程)描述符task_structcopy_thread_tls初始化⼦进程内核栈。

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

 

下面调用fork()函数看一下fork系统调用

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
 int main()
{
    int pid;
    pid=fork();
    if(pid<0)
    {
        printf("Error!\n");
        exit(-1);
    }
    else if(pid==0)
        printf("Child!\n");
    else
        printf("Parent!\n");
}

静态编译,输出结果如下图所示。

 来看一下fork子进程的内核堆栈:

 

 系统调用的内核堆栈:

 

 fork⼦进程的内核堆栈示意图中struct pt_regs就是内核堆栈中保存的中断上下⽂, struct inactive_task_frame就是fork⼦进程的进程上下⽂。__switch_to_asm汇编代码中完成内核堆栈切换后的代码,正好与structinactive_task_frame对应⼀⼀出栈,最后的__switch_to函数的最后ret正好出栈的是ret_addr,即⼦进程的起始点ret_from_fork

 

2.以execve为例分析中断上下文的切换

当前的可执⾏程序在执⾏,执⾏到execve系统调⽤时陷⼊内核态,在内核⾥⾯⽤do_execve加载可执⾏⽂件,把当前进程的可执⾏程序给覆盖掉。当execve系统调⽤返回时,返回的已经不是原来的那个可执⾏程序了,⽽是新的可执⾏程序。 execve返回的是新的可执⾏程序执⾏的起点,静态链接的可执⾏⽂件也就是main函数的⼤致位置,动态链接的可执⾏⽂件还需要ld链接好动态链接库再从main函数开始执⾏。

Linux系统⼀般会提供了execlexeclpexecleexecvexecvpexecve6个⽤以加载执⾏⼀个可执⾏⽂件的库函数,这些库函数统称为exec函数,差异在于对命令⾏参数和环境变量参数的传递⽅式不同。 exec函数都是通过execve系统调⽤进⼊内核,对应的系统调⽤内核处理函数为sys_execve__x64_sys_execve,它们都是通过调⽤do_execve来具体执⾏加载可执⾏⽂件的⼯作。

整体的调⽤关系为:

1)sys_execve()__x64_sys_execve 

2)do_execve()

3)do_execveat_common() 

4)__do_execve_file

5)exec_binprm()//关键 搜索一个文件解析模块 解析当前要加载的可执行程序

6)search_binary_handler()//找到解析当前程序的入口

7)load_elf_binary() 

8)start_thread() //关键  调用了start_thread_common

下面看一下start_thread_common

 

 最终execve系统调用从内核返回到用户态时,返回到的不是触发execve触发的下一条指令,下一条指令已经不存在了,已经被覆盖掉了,最终返回到new_ip,也就是ELF entry的位置。特殊之处像fork一样修改了内核堆栈栈低的关键的cpu上下文,但与fork不同,execve修改了中断上下文,fork修改的是进程上下文。

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

中断上下文和进程上下文:

进程上下⽂切换时需要保存要切换进程的相关信息(如thread.spthread.ip),这与中断上下⽂的切换是不同的。

中断是在⼀个进程当中从进程的⽤户态到进程的内核态,或从进程的内核态返回到进程的⽤户态。

切换进程需要在不同的进程间切换。

但⼀般进程上下⽂切换是嵌套到中断上下⽂切换中的,⽐如前述系统调⽤作为⼀种中断先陷⼊内核,即发⽣中断保存现场和系统调⽤处理过程。其中调⽤了schedule函数发⽣进程上下⽂切换,当系统调⽤返回到⽤户态时会恢复现场,⾄此完成了保存现场和恢复现场,即完成了中断上下⽂切换。

中断是由CPU实现的,所以中断上下⽂切换过程中最关键的栈顶寄存器sp和指令指针寄存器ip是由CPU协助完成的;

进程切换是由内核实现的,所以进程上下⽂切换过程中最关键的栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针寄存器ip的切换是在内核堆栈切换的基础上巧妙利⽤call/ret指令实现的 。

 

Linux系统的⼀般执⾏过程:

1)正在运⾏的⽤户态进程X

2)发⽣中断(包括异常、系统调⽤等), CPU完成load cs:rip(entry of a specific ISR),即跳转到中断处理程序⼊⼝。

3)中断上下⽂切换,具体包括如下⼏点:

swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了⼀个快照。

rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调⽤是由系统调⽤⼊⼝处的汇编代码实现⽤户堆栈和内核堆栈的切换。

save cs:rip/ss:rsp/rflags:将当前CPU关键上下⽂压⼊进程X的内核堆栈,快速系统调⽤是由系统调⽤⼊⼝处的汇编代码实现的。

此时完成了中断上下⽂切换,即从进程X的⽤户态到进程X的内核态。

4)中断处理过程中或中断返回前调⽤了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下⽂切换等。

5switch_to调⽤了__switch_to_asm汇编代码做了关键的进程上下⽂切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆
栈,并完成了进程上下⽂所需的指令指针寄存器状态切换。之后开始运⾏进程Y(这⾥进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下⼀⾏代码继续执⾏)。

6)中断上下⽂恢复,与(3)中断上下⽂切换相对应。注意这⾥是进程Y的中断处理过程中,⽽(3)中断上下⽂切换是在进程X的中断处理过程中,因为内核堆栈从进程X
切换到进程Y了。

7)为了对应起⻅中断上下⽂恢复的最后⼀步单独拿出来(6的最后⼀步即是7iret - pop cs:rip/ss:rsp/rflags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完
成了中断上下⽂的切换,即从进程Y的内核态返回到进程Y的⽤户态。注意快速系统调⽤返回sysretiret的处理略有不同。

8)继续运⾏⽤户态进程Y

posted @ 2020-06-15 01:50  ljj18  阅读(170)  评论(0)    收藏  举报