从整理上理解进程创建、可执行文件的加载和进程执行进程切换

学号:010
原创作品转载请注明出处 : https://github.com/mengning/linuxkernel/

一.实验要求

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

二.实验内容

1.阅读理解task_struct数据结构

源码链接
进程的相关信息存储在进程控制块(PCB)里,在Linux环境下,内核中的进程控制块用task_struct结构体描述。
task_struct 包含了这些内容:
(1)标示符 : 描述本进程的唯一标识符,用来区别其他进程。
(2)状态 :任务状态,退出代码,退出信号等。
(3)优先级 :相对于其他进程的优先级。
(4)程序计数器:程序中即将被执行的下一条指令的地址。
(5)内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
(6)上下文数据:进程执行时处理器的寄存器中的数据。
(7) I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
(8) 已运行信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

2.分析fork函数对应的内核处理过程do_fork,理解创建一个新进程如何创建和修改task_struct数据结构

Linux系统中,内核启动时,自动创建0号进程,其他进程是通过do_fork()复制0号进程得到的。
int do_fork()有以下参数:
unsigned long clone_flags
unsigned long stack_start
struct pt_regs *regs
unsigned long stack_size)

3.进程的创建过程:

linux下进程创建有三种方式:fork、vfork和clone,都是通过调用do_fork()来进行
详细过程为:
fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_fork()

3.1 do_fork()函数内容:

调用copy_process,复制进程描述符,返回创建的task_struct的指针,并且为子进程设置相应地上下文信息
初始化vfork的参数信息,并确保父进程后运行
调用wake_up_new_task,将子进程放入调度器的队列中

3.2 copy_process()函数内容:

首先调用dup_task_struct()复制当前的task_struct,检查进程数是否超过限制;
接着初始化自旋锁、挂起信号、CPU 定时器等;
然后调用sched_fork初始化进程数据结构,并把进程状态设置为TASK_RUNNING,复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等;
调用copy_thread()初始化子进程内核栈,为新进程分配并设置新的pid。

3.3 dup_task_struct()函数内容

调用alloc_task_struct_node()分配一个 task_struct 节点;
调用alloc_thread_info_node()分配一个 thread_info 节点,其实是分配了一个thread_union联合体,将栈底返回给 ti;
最后将栈底的值 ti 赋值给新节点的栈。

3.4 copy_thread()函数内容

获取子进程寄存器信息的存放位置
对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。
如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread,将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出
将父进程的寄存器信息复制给子进程。
将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0.
子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。

3.5 新建进程之后

新进程从ret_from_fork处开始执行,子进程的运行是由这几处保证的
dup_task_struct中为其分配了新的堆栈
copy_process中调用了sched_fork,将其置为TASK_RUNNING
copy_thread中将父进程的寄存器上下文复制给子进程,这是非常关键的一步,这里保证了父子进程的堆栈信息是一致的。
将ret_from_fork的地址设置为eip寄存器的值,这是子进程的第一条指令。

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

基于上次已编译完成的MENU OS,进行GDB调试,设置断点:

 b sys_clone
 b do_fork
 b dup_task_struct
 b copy_process
 b copy_thread
 b ret_from_for

在这里插入图片描述
在这里插入图片描述
观察可知:在do_fork中,以ret_from_fork函数为执行起点,复制父进程的内存堆栈和数据,并修改某些参数实现子进程的定义和初始化,创建子进程的工作完成后,通过sys_call_exit函数退出并pop父进程的内存堆栈,实现新进程的创建工作。

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

在这里插入图片描述
ELF可执行文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库。
可执行文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
可重定位文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
共享文件也称动态库,保存着代码和合适的数据,用来被不同的两个链接器链接。

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

首先编译一个简单的HelloWorld文件

#include <stdio.h>
int main()
{
     printf("Hello World!\n");
     return 0;
}

编译链接得到可执行文件hello
在这里插入图片描述
静态编译(完全把所有需要执行所依赖的东西放到程序内部)得到hello.static
在这里插入图片描述
hello.static比hello文件大很多,原因是:
静态链接方式:在程序运行之前完成所有的组装工作,生成一个可执行的目标文件
动态链接方式:在程序已经为了执行被装载入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝

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

在这里插入图片描述
在这里插入图片描述
观察可知:
新的可执行程序通过修改内核堆栈eip作为新程序的起点;当execve系统调用返回时,返回新的可执行程序的执行起点(即main函数),能顺利执行;静态链接时,返回可执行程序的头部,动态链接时返回动态链接器的起点。

8.进程执行与切换的时机

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

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

在这里插入图片描述
在这里插入图片描述
在进程间切换时,首先调用pick_next_task函数挑选下一个执行的程序;然后进行上下文的切换,包括保护现场和现场回复,最后调用__switch_to进行进程间切换。

10.分析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)

首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。然后将prev的内核堆栈指针esp存入prev->thread.esp中。把将next进程的内核栈指针next->thread.esp置入esp寄存器中,将当前进程的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度,通过jmp指令转入一个函数__switch_to,__switch_to中jmp与return的匹配,return 会弹出返回地址,因为jmp不会压栈,return弹出的则是栈顶地址即$1f标识之处。恢复next上次被调离时推进堆栈的内容。next进程开始执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值