码迷,mamicode.com
首页 > 系统相关 > 详细

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

时间:2020-06-14 23:32:17      阅读:70      评论:0      收藏:0      [点我收藏+]

标签:lazy   eve   可见   search   stat   ati   比较   qemu   try   

实验要求:

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

 

fork系统调用分析:

fork函数简介:

库函数fork是?户态创建?个?进程的系统调?API接?。fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。

分析fork系统调用和之前的其他的系统调用没有什么太大的区别,流程大抵如此:

?户态int $0x80或syscall指令触发系统调? ---> int $0x80指令触发entry_INT80_32,syscall指令触发entry_SYSCALL_64并sysret --->iret返回系统调?

1.首先在系统调用表中查找fork对应的系统调用号

因为笔者的Linux系统为64位系统,为了方便体现,这里只给出64位系统下的分析:

技术图片

 

 可以看到这里的三个系统调用都和fork函数有关,最重要的应当是57号系统调用__x64_sys_fork

 课上老师已经给出了该系统调用的源代码:
技术图片

 

 可见该系统调用也是调用了 _do_fork函数,然后再去源代码中寻找 _do_fork函数的原型。

在kernel/fork.c中能找到函数的原型,函数的源代码比较复杂,有70多行,课上老师也给出了简化的代码:

1 long _do_fork(struct kernel_clone_args *args) 
2 {     //复制进程描述符和执?时所需的其他数据结构      
3   p = copy_process(NULL, trace, NUMA_NO_NODE, args);     
4   wake_up_new_task(p);//将?进程添加到就绪队列   
5   return nr;//返回?进程pid(?进程中fork返回值为?进程的pid)
6  }

这里的代码比较清晰明了,这个函数主要做了三件事:

一.调用copy_process函数,复制父进程的进程描述符和执行时所需的其他数据结构到子进程去,也是本次系统调用分析的重点

二.调用wake_up_new_task函数,将子进程添加到就绪队列当中去

三.返回子进程PID

可以看出其中的copy_process函数是系统调用作用的重点,继续在源代码中查找它的原型:

同样给出课上老师给出的简化版:

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

可以看出这里比较重要的两个函数,一个用于复制父进程的进程描述符和创建子进程内核堆栈,一个用于初始化子进程内核栈和进程。其中最关键的就是 dup_task_struct复制当前进程(?进程)描述 符task_struct和copy_thread_tls初始化?进程内核栈。

课上同样也讲过这俩个函数的具体实现,总结下来,fork系统调用的大致流程如下:

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

技术图片

 

 2.实验验证:

上面已经介绍过整个系统调用的工作流程,现在开始进行实验验证:

首先先编写一个包含fork系统调用的c程序,将其编译,并展示运行效果:

这里张贴出相关代码:

 1 a#include<stdio.h>
 2 #include<unistd.h>
 3 int main()
 4 {
 5     pid_t pid;
 6     int count = 0;
 7     pid = fork();    //fork一个进程
 8     if(pid == 0) {               //pid为0,
 9         printf("this is child process, pid is %d\n",getpid());//getpid返回的是当前进程的PID
10         count+=2;
11         printf("count = %d\n",count);
12     } else if(pid > 0) {
13         printf("this is father process, pid is %d\n",getpid());
14         count++;
15         printf("count = %d\n",count);
16     } else {
17         fprintf(stderr,"ERROR:fork() failed!\n");
18     }
19     return 0;
20 }

这个程序的功能很简单,就是fork出一个子进程,然后在子进程中运行不同的程序:

技术图片

 

然后将编写好的程序复制进根目录系统的home目录下,并对其进行封装,然后在虚拟机上运行该程序:

 技术图片

 

 可见在QEMU虚拟机上该程序成功运行,然后再进行调试,对上文所述函数,依次打上断点:

技术图片

 

 进行调试,验证实验效果:

技术图片

 

 由此可见,fork系统调用的工作流程和上文分析的一致,得证。

 

 分析execve系统调用:

execve系统调用简介:

execve系统调?接?函数的函数原型如下:

int execve(const char *?lename, char *const argv[],char *const envp[]); 

?lename为可执??件的名字,argv是以NULL结尾的命令?参数数 组,envp同样是以NULL结尾的环境变量数组(使?命令man execve,可查看其说明)

 

evecve工作流程:

 Linux系统?般会提供了execl、execlp、execle、execv、execvp和execve等6个?以加载执? ?个可执??件的库函数,这些库函数统称为exec函数,差异在于对命令?参数和环境变量参数 的传递?式不同。exec函数都是通过execve系统调?进?内核,对应的系统调?内核处理函数为 sys_execve或__x64_sys_execve,它们都是通过调?do_execve来具体执?加载可执??件的 ?作。

整体的调?关系为sys_execve()或__x64_sys_execve -> do_execve() –> do_execveat_common() -> __do_execve_?le -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()。

 

 上下文环境切换流程:

先看一下再进行evecve系统调用时有哪些上下文环境:

技术图片

 

  在布局?个新的?户态堆栈时,实际上是把命令?参数内容和环境变量的内容通过指针的? 式传到系统调?内核处理函数,再创建?个新的?户态堆栈时会把这些char *argcv[]和char *envp[]等复制到?户态堆栈中,来初始化这个新的可执?程序的执?上下?环境。所以新 的程序可以从main函数开始把对应的参数接收过来,然后执?。 值得注意的是,在调?execve系统调?时,当前的执?环境是从?进程复制过来的, execve系统调?加载完新的可执?程序之后已经覆盖了原来?进程的上下?环境。execve 系统调?在内核中帮我们重新布局了新的?户态执?环境。 执?readelf -h可以查看ELF可执??件?部信息,如下所示程序??点Entry point address:0x804887f。如果是静态链接程序在execve系统调?加载完成后,堆栈上的返回地 址会修改为程序??点的地址。当系统调?从内核态返回时,会从该地址0x804887f继续执 ?。

 evecve的特别之处:

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

 

中断上下文切换和进程上下文切换对比:

系统调用和中断的机制类似,可以看作一种特殊的中断。因为前面大量分析了各类系统调用,所以这里使用系统调用中上下文切换来代替中断上下文切换。

中断上下文切换:

中断上下?代表当前进程执?,所以中断上下?中的get_current可获 取?个指向当前进程描述符的指针,即指向被中断进程,相应的中断 上下?切换的信息存储于该进程的内核堆栈中。中断有多种类型,? 如有不可屏蔽中断、可屏蔽中断、异常、陷阱(系统调?)等。
内核线程以进程上下?的形式运?在内核态,本质上还是进程,但它 有调?内核代码的权限,?如主动调?schedule()函数进?进程调 度。

进程上下文切换:

为了控制进程的执?,内核必须有能?挂起正在CPU上运?的进程,并 恢复执?以前挂起的某个进程。这种?为被称为进程切换,任务切换或 进程上下?切换。尽管每个进程可以拥有属于??的地址空间,但所有 进程必须共享CPU及寄存器。因此在恢复?个进程执?之前,内核必须 确保每个寄存器装?了挂起进程时的值。进程恢复执?前必须装?寄存 器的?组数据,称为进程的CPU上下?。您可以将其想象成对CPU的某 时刻的状态拍了?张“照?”,“照?”中有CPU所有寄存器的值。同样进 程切换就是拍?张当前进程所有状态的?“照?”保存下来,其中就包括 进程的CPU上下?的?“照?”,然后将导??张之前保存下来的其他进 程的所有状态信息恢复执?。

可以看出两类上下文切换,一类是一个进程内内核态和用户态的切换,而进程上下文切换往往是用户态内不同进程之间的切换。

 

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

标签:lazy   eve   可见   search   stat   ati   比较   qemu   try   

原文地址:https://www.cnblogs.com/hambug/p/13124020.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!