理解进程创建、可执行文件的加载和进程执行进程切换
学号后三位015。
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/。
一 阅读理解task_struct数据结构
1.进程的概念
进程是程序的一个执行实例,是正在执行的程序,是能分配处理器并由处理器执行的实体。操作系统使用数据结构来代表处理不同的实体,这个数据结构就是通常所说的进程描述符或进程控制块(PCB)。 在linux操作系统下这就是task_struct结构 ,在/include/linux/sched.h源文件中实现。
2.理解task_struct数据结构
代码链接
volatile long state; //表示进程状态
void *stack; //进程所属堆栈指针
unsigned int rt_priority;//进程优先级
int exit_state;//退出时状态
pid_t pid;//进程号,作为进程的全局标识符
pid_t tgid;//进程组号
struct task_struct __rcu *real_parent;//父进程
struct list_head children;//子进程
struct list_head sibling;//兄弟进程
struct task_struct *group_leader;//所属进程组的主进程
二 分析fork函数对应的内核处理过程do_fork
do_fork主要做了以下工作:
1.调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息;
2.初始化vfork的完成处理信息(如果是vfork调用);
3.调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行;
4.如果是vfork调用,需要阻塞父进程,直到子进程执行exec。
三 使用gdb跟踪分析一个fork系统调用内核处理函数do_fork
1.执行以下代码
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu //修改Makefile里的路径
mv test_fork.c test.c
make rootfs
2.在qemu中gdb
打断点:
执行结果
进程的创建的系统调用clone fork vfork都是调用do_fork实现的,而do_fork在做了一些参数检查之后。调用了copy_process函数,copy_process函数在进行安全性检查之后,使用dup_task_struct复制父进程的结构体。对新进程描述符的一些标志信息和时间信息进行初始化,之后将父进程的所有进程信息拷贝到子进程空间,包括IO、文件、内存信息等。然后,设置新进程的pid,将新进程加入进程调度队列中。子进程的eax设置为0,父进程则返回新进程的pid,所以在fork调用中,子进程返回的是0,父进程返回的是新进程的pid。
四 理解编译链接的过程和ELF可执行文件格式
程序的编译链接过程需要经历如下步骤:
ELF文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库。
1.可执行文件(应用程序)可执行文件包含了代码和数据,是可以直接运行的程序。
2.可重定向文件(.o)可重定向文件又称为目标文件,它包含了代码和数据(这些数据是和其他重定位文件和共享的object文件一起连接时使用的)。
.o文件参与程序的连接(创建一个程序)和程序的执行(运行一个程序),它提供了一个方便有效的方法来用并行的视角看待文件的内容,这些.o文件的活动可以反映出不同的需要。
Linux下,我们可以用gcc -c编译源文件时可将其编译成.o格式。
3.共享文件(*.so)也称为动态库文件,它包含了代码和数据(这些数据是在连接时候被连接器ld和运行时动态连接器使用的)。动态连接器可能称为ld.so.1,libc.so.1或者 ld-linux.so.1。
五 使用gdb跟踪分析一个execve系统调用内核处理函数do_execve
分别在sys_execve、do_execve() 、do_execve_common()、exec_binprm()、search_binary_handler()、load_elf_binary()、start_thread()设置断点
断点 do _execve处,可以看出其调用do_execve_common()函数。
六 理解Linux系统中进程调度的时机
我们先对schedule,pick_next_task,context_switch和__switch_to设置断点,观察程序运行的情况。
由以上跟踪结果可以得知,在进行进程间的切换时,各处理函数的调用顺序如下:pick_next_task -> context_switch -> __switch_to 。由此可以得出,当进程间切换时,首先需要调用pick_next_task函数挑选出下一个将要被执行的程序;然后再进行进程上下文的切换,此环节涉及到“保护现场”及“现场恢复”;在执行完以上两个步骤后,调用__switch_to进行进程间的切换。
总结
通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行,所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。 同理,硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。