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

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


实验要求

  • 从整理上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换
    • 阅读理解task_struct数据结构http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235;
    • 分析fork函数对应的内核处理过程do_fork,理解创建一个新进程如何创建和修改task_struct数据结构;
    • 使用gdb跟踪分析一个fork系统调用内核处理函数do_fork ,验证您对Linux系统创建一个新进程的理解,特别关注新进程是从哪里开始执行的?为什么从那里能顺利执行下去?即执行起点与内核堆栈如何保证一致。
    • 理解编译链接的过程和ELF可执行文件格式;
    • 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接;
    • 使用gdb跟踪分析一个execve系统调用内核处理函数do_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解;
    • 特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?
    • 理解Linux系统中进程调度的时机,可以在内核代码中搜索schedule()函数,看都是哪里调用了schedule(),判断我们课程内容中的总结是否准确;
    • 使用gdb跟踪分析一个schedule()函数 ,验证您对Linux系统进程调度与进程切换过程的理解;
    • 特别关注并仔细分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系;
    • 撰写一篇博客(署真实姓名或学号最后3位编号),并在博客文章中注明“原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ ”,博客内容的具体要求如下:
      • 题目自拟,内容围绕Linux系统的执行过程进行;
      • 博客中需要使用实验截图
      • 博客内容中需要仔细分进程创建、可执行文件的加载和进程执行进程切换
      • 总结部分需要阐明自己对Linux系统的执行过程的理解。

阅读理解task_struct数据结构

代码链接
什么是进程?

  1. 进程是程序的一个执行的实例;
  2. 进程是正在执行的程序
  3. 进程是能分配处理器并由处理器执行的实体

task_struct结构体中的主要信息

  1. 进程状态:记录进程是处于运行状态还是等待状态
  2. 调度信息:进程由哪个函数调度,具体怎样调度等
  3. 进程之间的通讯状况
  4. 进程之间的亲属关系:在父进程和子进程之间有task_struct类型的指针,将父进程和子进程联系起来
  5. 时间数据信息:每个进程执行所占用CPU的时间
  6. 进程的标志
  7. 进程的标识符:该进程唯一的标识符用来区别其他进程
  8. 信号处理信息
  9. 文件信息:可以进行读写操作的一些文件的信息
  10. 页面管理信息
  11. 优先级:相对于其他进程的优先级
  12. ptrace系统调用
  13. 虚拟内存处理

为了管理进程,操作系统必须对每个进程所做的事情进行清楚的描述,为此,操作系统使用数据结构来代表处理不同的实体,这个数据结构就是通常所说的进程描述符或进程控制块(PCB)。
在linux操作系统下这就是task_struct结构 ,所属的头文件#include <sched.h>每个进程都会被分配一个task_struct结构,它包含了这个进程的所有信息,在任何时候操作系统都能够跟踪这个结构的信息.

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

fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;

进入gbd调试模式,依次在这些地方设置断点
在这里插入图片描述在这里插入图片描述
在Linux中,进程创建实际上是通过do_fork函数处理的。do_fork函数的功能相对简单:  
1.检查是否或者哪个事件应该汇报给ptracer。
2.通过copy_process创建进程描述符和子进程执行所需要的其它数据结构。
3.执行wake_up_new_task函数,唤醒新进程。
4.结束并返回子进程的ID

copy_process则负责对进程创建的相关资源的申请:
1.调用security_task_create以及稍后调用的security_task_alloc执行附加的安全检查。
2.执行dup_task_struct复制父进程的task_struct描述符
3.初始化新结构体的各个字段:did_exec,utime,stime,gtime,irq_events,hardirqs_enabled等等
4.进行调度相关的初始化:perf_event_init_task,audit_alloc.
5.复制父进程的信息到子进程:copy_semundo,copy_files,copy_fs,copy_mm等
6.初始化其它进程相关字段
7.将total_forks增加1

通过总结可以得到:进程的创建的系统调用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。

在这里插入图片描述
每一部分的具体信息参见 https://baike.baidu.com/item/ELF/7120560?fr=aladdin
在linux下输入“man elf”即可查看其详细的格式定义。

静态链接与动态链接

  1. 静态链接
    在编译链接时直接将需要的执行代码复制到最终可执行文件中,有点是代码的装在速度块,执行速度也比较快,对外部环境依赖度低。编译时它会把需要的所有代码都链接进去,应用程序相对较大。

  2. 动态链接
    动态链接是在程序运行时由操作系统将需要的动态库加载到内存中。动态链接分为装载时动态链接和运行时动态链接。

程序装载
编程使用exec*库函数加载一个可执行文件
在之前的fork程序中加入一句execlp("/bin/ls",“ls”,NULL);重新编译
在这里插入图片描述

Linux提供了execl、execlp、execle、execv、execvp和execve等6个用以执行一个可执行文件的函数。这些函数的本质都是调用sys_execve()来执行一个可执行文件。使用gdb跟踪do_execve
在这里插入图片描述
在这里插入图片描述

整体调用关系为sys_execve()->do_execve()->do_execveat_common()->__do_execve_file()->prepare_binprm()->search_binary_handler()->load_elf_binary()->start_thread().

进程调度的时机
进程调度的时机

  • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
  • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
  • 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
    schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换。context_switch首先调用switch_mm切换CR3,然后调用宏switch_to来进行硬件上的上下文切换。
    使用 gdb跟踪schedule函数
next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
context_switch(rq, prev, next);//进程上下文切换
//switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
#define switch_to(prev, next, last)                    
do {                                 
  /*                              
   * Context-switching clobbers all registers, so we clobber  
   * them explicitly, via unused output variables.     
   * (EAX and EBP is not listed because EBP is saved/restored  
   * explicitly for wchan access and EAX is the return value of   
   * __switch_to())                     
   */                                
  unsigned long ebx, ecx, edx, esi, edi;                
                                  
  asm volatile("pushfl\n\t"      /* save    flags */    
           "pushl %%ebp\n\t"        /* save    EBP   */  //当前进程堆栈基址压栈
           "movl %%esp,%[prev_sp]\n\t"  /* save    ESP   */  //将当前进程栈顶保存prev->thread.sp
           "movl %[next_sp],%%esp\n\t"  /* restore ESP   */  //将下一个进程栈顶保存到esp中
           "movl $1f,%[prev_ip]\n\t"    /* save    EIP   */  //保存当前进程的eip
           "pushl %[next_ip]\n\t"   /* restore EIP   */     //将下一个进程的eip压栈,next进程的栈顶就是他的的起点
           __switch_canary                   
           "jmp __switch_to\n"  /* regparm call  */ 
           "1:\t"                        
           "popl %%ebp\n\t"     /* restore EBP   */    
           "popfl\n"         /* restore flags */   //开始执行下一个进程的第一条命令
                                 
           /* output parameters */                	
           : [prev_sp] "=m" (prev->thread.sp),		
             [prev_ip] "=m" (prev->thread.ip),		
             "=a" (last),                 
                                  
             /* clobbered output registers: */     
             "=b" (ebx), "=c" (ecx), "=d" (edx),      
             "=S" (esi), "=D" (edi)             
                                       
             __switch_canary_oparam                
                                  
             /* input parameters: */                
           : [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)

通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行,所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行.
同理,硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。
Linux内核工作在进程上下文或者中断上下文。提供系统调用服务的内核代码代表发起系统调用的应用程序运行在进程上下文;另一方面,中断处理程序,异步运行在中断上下文。中断上下文和特定进程无关。
运行在进程上下文的内核代码是可以被抢占的(Linux2.6支持抢占)。但是一个中断上下文,通常都会始终占有CPU(当然中断可以嵌套,但我们一般不这样做),不可以被打断。正因为如此,运行在中断上下文的代码就要受一些限制。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值