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

实验要求:

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

一  相关知识

1 进程调度的时机

  • ?户进程上下?中主动调?特定的系统调?进?中断上下?,系统调?返回?户态之前进?进程调度。
  • 内核线程或可中断的中断处理程序,执?过程中发?中断进?中断上下?,在中断返回前进?进程调度。
  • 内核线程主动调?schedule函数进?进程调度。

第?种和第?种情况可以统?起来,中断处理程序执?过程主动调?schedule函数进?进程调度,与前述两类调度时机对应

2 中断上下文

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

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

进程上下?切换时需要保存要切换进程的相关信息(如thread.sp与thread.ip),这与中断上下?的切换是不同的。中断是在?个进程当中从进程的?户态到进程的内核态,或从进程的内核态返回到进程的?户态,?切换进程需要在不同的进程间切换。但?般进程上下?切换是嵌套到中断上下?切换中的,?如前述系统调?作为?种中断先陷?内核,即发?中断保存现场和系统调?处理过程。其中调?了schedule函数发?进程上下?切换,当系统调?返回到?户态时会恢复现场,?此完成了保存现场和恢复现场,即完成了中断上下?切换。

二 实验步骤

1 fork函数

fork系统调?创建?进程,也就?个进程变成了两个进程,两个进程执?相同的代码,只是fork系统调?在?进程和?进程中的返回值不同。
打开linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl 文件,56、 57、 58号系统调?__x64_sys_clone、 __x64_sys_fork、__x64_sys_vfork,即如下kernel/fork.c代码。
 
Linux实验三 结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
 
进入linux-5.4.34/kernel/fork.c 查看 fork 源代码:
Linux实验三 结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
Linux实验三 结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程

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

/*
* 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函数主要完成了调?copy_process()复制?进程、获得?wake_up_new_task将?进程加?就绪队列等待调度执?等。

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

//_do_fork关键部分代码
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()是创建?个进程的主要的代码如下是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_structcopy_thread_tls

dup_task_struct作用:

  • 在专业高速缓冲内存上分配task_struct,并完成初始化
  • 在普通内存中分配thread_info及连续的两个页面,完成初始化
  • 将task_struct和thread_info联系起来

主要代码:

static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{…
//实际完成进程描述符的拷?,具体做法是*tsk = *orig
err = arch_dup_task_struct(tsk, orig);
…
tsk->stack = stack;
...
//实际完成进程描述符的拷?,具体做法是*tsk = *orig
setup_thread_stack(tsk, orig);
clear_user_return_notifier(tsk);
clear_tsk_need_resched(tsk);
set_task_stack_end_magic(tsk);
}

 

copy_thread_tls作用:

负责构造fork系统调?在?进程的内核堆栈,也就是fork系统调?在??进程各返回?次,?进程中和其他系统调?的处理过程并??致,?在?进程中的内核函数调?堆栈需要特殊构建,为?进程的运?准备好上下?环境。

主要代码:

int copy_thread_tls(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p, unsigned long tls)
{
frame->ret_addr = (unsigned long) ret_from_fork;
p->thread.sp = (unsigned long) fork_frame;
*childregs = *current_pt_regs();
childregs->ax = 0;
...
/*
* Set a new TLS for the child thread?
*/
if (clone_flags & CLONE_SETTLS) {
err = do_arch_prctl_64(p, ARCH_SET_FS, tls);

do_fork 总结

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

 

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

 

2 execve系统调用

execve(执行文件)在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。

表头文件:

#include<unistd.h>

定义函数:

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

execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。成功无返回值,失败返回-1。

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

 

execve系统调过程:

sys_execve-->do_execve
                 -->do_execveat_common
                       -->__do_execve_file
                            -->search_binary_handler 
                                   -->load_elf_binary
                                        -->start_thread
  1. execve陷入内核,传入命令行参数和shell上下文环境
  2. sys_execve调用do_execve封装命令行参数和shell上下文
  3. 调用do_execveat_common,do_execveat_common调用__do_execve_file,打开ELF文件并把信息的装入linux_binprm结构体
  4. __do_execve_file中调用search_binary_handler,寻找解析ELF文件的函数
  5. search_binary_handler找到ELF文件解析函数load_elf_binary,解析ELF文件,把ELF文件装入内存,修改进程的用户态堆栈,修改进程的数据段代码段
  6. load_elf_binary调用start_thread修改进程内核堆栈
  7. 返回用户态,此时ip指向ELF文件的main函数地址

三 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关键的进程上下?切换等。

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

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

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

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

 

 

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

上一篇:JQuery_ajax


下一篇:在浏览器控制台调试php程序