进程创建、可执行文件的加载和进程执行进程切换的理解分析

学号165,原创作品转载请注明出处。
实验来源:https://github.com/mengning/linuxkernel/

一、进程创建

1.阅读理解task_struct数据结构

http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235 中可以找到task_struct结构,即为进程控制块(PCB)主要包含了进程状态、堆栈、标志、优先级等信息。

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

创建进程可以通过fork()、vfork()、clone()实现,系统调用均为do_fork。在内核启动时,除手动创建0号进程外,其他进程均由复制0号进程内容而得来。

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)

do_fork函数通过调用copy_process(),复制一份当前进程作为子进程,并设置上下文信息;调用wake_up_new_task(),将子进程放入调度器队列,此时该子进程可以被调度程序选中并运行;如果是vfork()调用,将初始化vfork的完成处理信息,并阻塞父进程,直到子进程执行。

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

使用的内核是Linux-5.0.1,执行以下命令
git clone https://github.com/mengning/menu.git
cd menu (记得修改Makefile信息)
mv test_fork.c test.c
make rootfs
进程创建、可执行文件的加载和进程执行进程切换的理解分析
打开gdb,并设置断点
进程创建、可执行文件的加载和进程执行进程切换的理解分析
观察执行
进程创建、可执行文件的加载和进程执行进程切换的理解分析
在do_fork中,以ret_from_fork函数为执行起点,复制父进程的内存堆栈和数据,并修改某些参数实现子进程的定义和初始化,创建子进程的工作完成后,通过sys_call_exit函数退出并pop父进程的内存堆栈,实现新进程的创建工作。

二、可执行文件的加载

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

  • “ELF"全称为"Executable Linking Format”,从源文件(如.c/.cpp文件)编译链接成可执行文件(如.out/.exe文件)需要经历预处理、编译、汇编、连接等步骤。
  • ELF可执行文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库。
  1. 可执行文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
  2. 可重定位文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
  3. 共享文件也称动态库,保存着代码和合适的数据,用来被不同的两个链接器链接。

2.使用exec*库函数加载一个可执行文件

编辑hello.c文件

#include <stdio.h>

int main()
{
     printf("Hello World!\n");
     return 0;
}

编译链接得到可执行文件hello
进程创建、可执行文件的加载和进程执行进程切换的理解分析
也可以静态编译得到hello.static
进程创建、可执行文件的加载和进程执行进程切换的理解分析
查看大小可以发现hello.static比hello大得多。
进程创建、可执行文件的加载和进程执行进程切换的理解分析

3.使用gdb跟踪分析一个execve系统调用内核处理函数do_execve

设置断点
进程创建、可执行文件的加载和进程执行进程切换的理解分析
观察情况
进程创建、可执行文件的加载和进程执行进程切换的理解分析
新的可执行程序通过修改内核堆栈eip作为新程序的起点;当execve系统调用返回时,返回新的可执行程序的执行起点(即main函数),能顺利执行;静态链接时,返回可执行程序的头部,动态链接时返回动态链接器的起点。

进程执行与切换

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

进程调度的时机:

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

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

进程创建、可执行文件的加载和进程执行进程切换的理解分析
进程创建、可执行文件的加载和进程执行进程切换的理解分析
在进程间切换时,首先调用pick_next_task函数挑选下一个执行的程序;然后进行上下文的切换,包括保护现场和现场回复,最后调用__switch_to进行进程间切换。

3.分析switch_to的汇编代码

asm volatile("pushfl\n\t"      /* 保存当前进程的标志位 */   
         "pushl %%ebp\n\t"        /* 保存当前进程的堆栈基址EBP   */ 
         "movl %%esp,%[prev_sp]\n\t"  /* 保存当前栈顶ESP   */ 
         "movl %[next_sp],%%esp\n\t"  /* 把下一个进程的栈顶放到esp寄存器中,完成了内核堆栈的切换,从此往下压栈都是在next进程的内核堆栈中。   */ 
       

		 "movl $1f,%[prev_ip]\n\t"    /* 保存当前进程的EIP   */ 
         "pushl %[next_ip]\n\t"   /* 把下一个进程的起点EIP压入堆栈   */    
         __switch_canary                   
         "jmp __switch_to\n"  /* 因为是函数所以是jmp,通过寄存器传递参数,寄存器是prev-a,next-d,当函数执行结束ret时因为没有压栈当前eip,所以需要使用之前压栈的eip,就是pop出next_ip。  */ 


		 "1:\t"               /* 认为next进程开始执行。 */         
		 "popl %%ebp\n\t"     /* restore EBP   */    
		 "popfl\n"         /* 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)

四、总结

通过系统调用do_fork()、copy_process()、dup_task_struct()、copy_thread()等实现进程的创建;通过execve()及相关调用实现可执行文件的加载;通过schedule()、context_switch()、switch_to()实现进程的上下文切换。