Linux小白实验之进程和程序
学号后三位:072
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
一:实验要求
从整理上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换:
(1) 阅读理解task_struct数据结构,分析fork函数对应的内核处理过程do_fork,理解创建一个新进程如何创建和修改task_struct数据结构,并使用gdb跟踪分析一个fork系统调用内核处理函数do_fork ,验证您对Linux系统创建一个新进程的理解。
(2) 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接;并使用gdb跟踪分析一个execve系统调用内核处理函数do_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解。
(3) 理解Linux系统中进程调度的时机,可以在内核代码中搜索schedule()函数,使用gdb跟踪分析一个schedule()函数 ,验证您对Linux系统进程调度与进程切换过程的理解。
二:实验环境
本次实验主要使用个人PC及其安装的Ubuntu来完成。
三:实验过程
1. 进程创建
1.1 进程描述
1.进程描述符(task_struct)
用来描述进程的数据结构,可以理解为进程的属性。比如进程的状态、进程的标识(PID)等,都被封装在了进程描述符这个数据结构中,该数据结构被定义为task_struct
2.进程控制块(PCB)
是操作系统核心中一种数据结构,主要表示进程状态。
1.2 实验过程
第一步:打开gdb进行远程调试
第二步:设置断点
第三步:跟踪调试断点
第四步:核心代码分析
1.fork一个子进程的代码
2.do_fork部分代码分析
do_fork 流程:
(1) 调用 copy_process 为子进程复制出一份进程信息
(2) 如果是 vfork 初始化完成处理信息
(3) 调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU
(4) 如果是 vfork,父进程等待子进程完成 exec 替换自己的地址空间
2. 可执行文件的加载
第一步:了解可执行文件的生成
第二步:可执行性文件查看
第三步:生成共享库和运行时链接库
第四步:gdb调试跟踪装载过程
第五步:代码分析
1.execve和上面分析的fork系统一样,是一种特殊的系统调用。fork的特殊在于系统调用后两次返回,生成了新进程,而不单单是在原来程序的系统调用的下一条语句。而execve的特殊在于它返回之后,执行的是一个新的程序了(例如返回程序的main入口,修改的是elf_entry),而不是以前调用execve的进程shell了。 内核处理函数sys_execve内部会解析可执行文件格式,它的内部执行流程如下:
do_execve -> do_execve_common -> exec_binprm。
gdb断点设置:b sys_execve ;停到该位置,继续设置断点 b load_elf_binary;
b start_thread。 其中的一些函数解释:
1)search_binary_handler符合寻找文件格式对应的解析模块,如下:
对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary
2)Linux内核是如何支持多种不同的可执行文件格式的?
elf_format 和 init_elf_binfmt,就是观察者模式中的观察者。
3)可执行文件开始执行的起点在哪里?如何才能让execve系统调用返回到用户态时执行新程序? load_elf_binary -> start_thread中通过修改内核堆栈中的EIP的值作为新程序的起点。即修改一开始int 0x80压入内核堆栈的EIP。start_thread中的new_ip是返回到用户态第一条指令的地址,与可执行程序的头中的入口地址相同。
3. 进程切换
第一步:设置断点
第二步:按c(/continue)后,模拟器继续运行,在第一个断点处停止,即schedule函数处,使用l(/list)命令查看其代码,s(/step)命令逐条分析。
第三步:继续运行,到第二个断点处停止了,即context_switch处停下。与上相同,继续单步执行。在这个过程中,要留心一下switch_to。
第四步:通过list查看代码发现, context_switch中调用了switch_to函数
第五步:核心代码分析
1.在switch_to函数中,通过嵌入如下的汇编代码实现进程上下文的切换以及与中断上下文的切换:
2.schedule
3.switch_to
四:实验总结
1.创建一个新进程在内核中的执行过程大致如下:
(1) 使用系统调用Sys_clone(或fork,vfork)系统调用创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
(2) Linux通过复制父进程PCB的task_struct来创建一个新进程,要给新进程分配一个新的内核堆栈;
(3) 要修改复制过来的进程数据,比如pid、进程链表等等执行copy_process和copy_thread
(4) p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
(5) p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
2.新的可执行程序是从new_ip开始执行,start_thread实际上是把返回到用户态的位置从Int 0x80的下一条指令,变成了规定的新加载的可执行文件的入口位置,即修改内核堆栈的EIP的值作为新程序的起点。 当执行到execve系统调用时,陷入内核态,用execve加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的执行起点(main函数位置),所以execve系统调用返回后新的可执行程序能顺利执行。 对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时,如果是静态链接,elf_entry指向可执行文件规定的头部(main函数对应的位置0x8048***);如果需要依赖动态链接库,elf_entry指向动态链接器的起点。动态链接主要是由动态链接器ld来完成的。
3.通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;加载一个新的可执行程序后返回到用户态的情况,如execve。