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

实验要求:

  • 以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位系统下的分析:

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

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

 课上老师已经给出了该系统调用的源代码:
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

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

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

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

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

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

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

三.返回子进程PID

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

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

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_?ags, args->stack, args->stack_size, p,  args->tls); … 
return p;//返回被创建的?进程描述符指针 
}

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

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

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

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

 2.实验验证:

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

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

这里张贴出相关代码:

a#include<stdio.h>
#include<unistd.h>
int main()
{
    pid_t pid;
    int count = 0;
    pid = fork();    //fork一个进程
    if(pid == 0) {               //pid为0,
        printf("this is child process, pid is %d\n",getpid());//getpid返回的是当前进程的PID
        count+=2;
        printf("count = %d\n",count);
    } else if(pid > 0) {
        printf("this is father process, pid is %d\n",getpid());
        count++;
        printf("count = %d\n",count);
    } else {
        fprintf(stderr,"ERROR:fork() failed!\n");
    }
    return 0;
}

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

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

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

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

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

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

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

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

 由此可见,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系统调用时有哪些上下文环境:

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

  在布局?个新的?户态堆栈时,实际上是把命令?参数内容和环境变量的内容通过指针的? 式传到系统调?内核处理函数,再创建?个新的?户态堆栈时会把这些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上下?的?“照?”,然后将导??张之前保存下来的其他进 程的所有状态信息恢复执?。

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

相关推荐