实验:从整理上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和

2019-07-14 10:38发布

学号375
转载请注明出处 https://github.com/mengning/linuxkernel/

阅读理解task_struct数据结构

task_struct实际上就是进程PCB以下是pcb的重要参数:
volatile long state;//表示进程的当前状态:
unsigned long flags; //进程标志:
long priority; //进程优先级。
long counter; //在轮转法调度时表示进程当前还可运行多久。
unsigned long policy; //进程调度策略

分析fork函数对应的内核处理过程do_fork

具体过程如下:fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_fork()
即do_fork会调用copy_process,将当前进程复制一份出来作为新创建的子进程,并设置上下文信息。之后会调用相关函数把新创建的线程放入就绪队列等待调度。如果调用了vfork,这时就会阻塞父进程,因为需要执行exec。

使用gdb跟踪分析一个fork系统调用内核处理函数do_fork

简单的fork函数准备调试:
在这里插入图片描述
设置断点: b sys_clone b do_fork b dup_task_struct b copy_process 在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

理解编译链接的过程和ELF可执行文件格式

ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且他们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接

静态链接
在编译链接时直接将需要的执行代码复制到最终可执行文件中,有点是代码的装在速度块,执行速度也比较快,对外部环境依赖度低。编译时它会把需要的所有代码都链接进去,应用程序相对较大。 动态链接
动态链接是在程序运行时由操作系统将需要的动态库加载到内存中。动态链接分为装载时动态链接和运行时动态链接
exec本质是调用了sys_execve()来执行一个可执行文件。使用gdb跟踪do_execve
在这里插入图片描述
使用gdb跟着 do_exec
在这里插入图片描述
在这里插入图片描述
整体调用关系为sys_execve()->do_execve()->do_execveat_common()->__do_execve_file()->prepare_binprm()->search_binary_handler()->load_elf_binary()->start_thread().

理解Linux系统中进程调度的时机

进程调度的时机:
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

使用gdb跟踪分析一个schedule()函数

在这里插入图片描述

分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系;

asm volatile("pushfl " /* 保存当前进程的标志位 */ "pushl %%ebp " /* 保存当前进程的堆栈基址EBP */ "movl %%esp,%[prev_sp] " /* 保存当前栈顶ESP */ "movl %[next_sp],%%esp " /* 把下一个进程的栈顶放到esp寄存器中,完成了内核堆栈的切换,从此往下压栈都是在next进程的内核堆栈中。 */ "movl $1f,%[prev_ip] " /* 保存当前进程的EIP */ "pushl %[next_ip] " /* 把下一个进程的起点EIP压入堆栈 */ __switch_canary "jmp __switch_to " /* 因为是函数所以是jmp,通过寄存器传递参数,寄存器是prev-a,next-d,当函数执行结束ret时因为没有压栈当前eip,所以需要使用之前压栈的eip,就是pop出next_ip。 */ "1: " /* 认为next进程开始执行。 */ "popl %%ebp " /* restore EBP */ "popfl " /* restore flags */ /* output parameters 因为处于中断上下文,在内核中 prev_sp是内核堆栈栈顶 prev_ip是当前进程的eip */ : [prev_sp] "=m" (prev->thread.sp), [prev_ip] "=m" (prev->thread.ip), //[prev_ip]是标号 "=a" (last), /* clobbered output registers: */ "=b" (ebx), "=c" (ecx), "=d" (edx), "=S" (esi), "=D" (edi) __switch_canary_oparam /* input parameters: next_sp下一个进程的内核堆栈的栈顶 next_ip下一个进程执行的起点,一般是$1f,对于新创建的子进程是ret_from_fork*/ : [next_sp] "m" (next->thread.sp), [next_ip] "m" (next->thread.ip), /* regparm parameters for __switch_to(): */ [prev] "a" (prev), [next] "d" (next) __switch_canary_iparam : /* reloaded segment registers */ "memory"); } while (0)

总结

linux系统执行过程中内核进程可以主动调用schedule函数来进行进程间的切换。switch_to函数用来完成寄存器间参数的的切换。与内核态不同的是用户态的进程只能通过中断进入内核态再调度执行。