• 深入理解Linux之进程的创建和可执行程序的加载


    SA12226242 施健

    一.进程的创建

      Linux创建进程是通过子进程复制父进程所拥有的资源来实现的。现代Linux通过写时复制、共享数据等方法优化这一过程,提高创建子进程的效率。

      在Linux中,进程创建实际上是通过do_fork函数处理的。do_fork函数的功能相对简单:

    代码在:kernel/fork.c
    1.检查是否或者哪个事件应该汇报给ptracer。
    2
    .通过copy_process创建进程描述符和子进程执行所需要的其它数据结构。
    3.执行wake_up_new_task函数,唤醒新进程。
    4
    .结束并返回子进程的ID

      copy_process则负责对进程创建的相关资源的申请:

    代码在:kernel/fork.c
    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

      task_struct进程控制块与进程地址空间的联系:

      在task_struct结构体内的struct mm_struct成员执行内存区描述符的指针。在进程描述符中,还应该存储进程空间的页表信息,和将逻辑地址转换成页号和页内偏移地址所需的相关信息。

      通过总结可以得到:进程的创建的系统调用clone fork vfork都是调用do_fork实现的,而do_fork在做了一些参数检查之后。调用了copy_process函数,copy_process函数在进行安全性检查之后,使用dup_task_struct复制父进程的结构体。对新进程描述符的一些标志信息和时间信息进行初始化,之后将父进程的所有进程信息拷贝到子进程空间,包括IO、文件、内存信息等。然后,设置新进程的pid,将新进程加入进程调度队列中。子进程的eax设置为0,父进程则返回新进程的pid,所以在fork调用中,子进程返回的是0,父进程返回的是新进程的pid。详细代码分析,请参加附录。

    二.可执行程序的加载

      在Linux中提供了一系列的函数,这些函数能用可执行文件所描述的新上下文代替进程的上下文。这样的函数名以前缀exec开始。所有的exec函数都是调用了execve()系统调用。

      sys_execve接受参数:1.可执行文件的路径  2.命令行参数字符串 3.环境变量字符串

      sys_execve是调用do_execve实现的。do_execve则是调用do_execve_common实现的,依次执行以下操作:

    do_exceve的代码在:fs/exec.h
    1
    . 在堆上分配一个linux_binprm结构。 2. 调用open_exec读取可执行文件。 3. 调用sched_exec(),确定最小负载的CPU以执行新程序,并把当前进程转移过去。 4. 调用bprm_mm_init()函数,为新程序初始化内存管理。 5. 调用prepare_bprm()函数填充linux_binprm数据结构。 6. 拷贝命令行参数argv,环境变量envp,可执行文件名filename到新进程中 7. 调用search_binary_handler()函数对formats链表进行扫描,并尝试每个load_binary函数,如果成功加载了文件的执行格式,对formats的扫描终止。 8. 释放linux_binprm数据结构,返回从该文件可执行格式的load_binary中获得的代码。

      对load_binary分析如下:

    1.检查存放在文件前128字节的一些魔数进行匹配以确认文件格式。
    2.读可执行文件的首部。
    3.从可执行文件中确定动态链接程序的路径名,并用它来确定共享库的位置并把他们映射到内存。
    4.获得动态链接程序的目录项对象。
    5.检查动态链接的执行许可权。
    6.把动态链接程序的前128字节拷贝到缓冲区。
    7.对动态链接程序类型执行一致性检查。
    8.调用arch_pick_mmap_layout(),以选择进程线性区的布局。
    9.调用setup_arg_pages()函数为进程的用户态堆栈分配一个新的线性区描述符。
    10.调用do_mmap()函数创建一个新线性区来对可执行文件正文段进行映射。
    11.调用装入动态链接程序的函数
    12.把可执行格式的linux_binfmt对象的地址放在进程描述符的binfmt字段。
    13.创建动态链接程序表,并把它们放在用户态堆栈。
    14.调用do_brk()函数创建一个新的匿名线性区来映射程序的bss段。
    15.调用start_thread()宏修改保存在内核态堆栈。
    16.返回0.

       总结:Linux内核使用execve系统调用就开始进行程序的装载。execve()系统调用相应的实现是sys_execve()。

      sys_execve()进行一些参数检查复制之后,调用do_execve()。

      do_execve会首先查找被执行的文件,如果找打文件。则读取文件的前128个字节以确定被检查的文件的格式。然后,继续调用search_binary_handle()去搜索合适的可执行文件装载处理过程。

      对ELF可执行文件的装载处理工程是load_elf_binary()。步骤是:检查elf文件的有效性,寻找动态链接库的“.interp”段,设置动态链接器的路径,根据ELF的可执行文件的程序头表的描述,对ELF文件进行映射,初始化ELF进程环境。

      最后,将系统调用的返回地址修改为ELF可执行文件的入口点。在设置了eip之后,sys_execve返回到用户态时,程序就返回到新的程序开始执行,ELF可执行文件装载完成。

      详细的代码分析,请参加附录。

      ELF文件格式与进程地址空间的联系:在壮载ELF文件时,会将文件头部的.init、.text、.rodata段装载到进程的代码段中,将ELF中数据相关的部分,装载到数据段中。

      在动态链接的过程中,ELF文件全局变量的数据区的部分会在进程空间中拷贝一份,代码段则多个进程进行共享。

      exec执行之后,EIP指向的地方:EIP指向新程序的入口点,在sys_execve执行完成进入用户态之后,将从新的程序入口点继续执行。
      

    三.附录

      代码的详细分析:深入理解Linux之进程初探

      

  • 相关阅读:
    thinkphp5日期时间查询比较和whereTime使用方法
    Bootstrap 标签
    MySQL 查询当天、周、月,最近一周、一月的数据,以及当年每月的统计数据
    把字符串解析到变量,数组变量中的函数 PHP parse_str() 函数
    PHP 数组转字符串,与字符串转数组
    php 获取最近一周,一个月,一年
    linux达人养成计划学习笔记(六)—— 挂载命令
    linux达人养成计划学习笔记(五)—— 关机和重启命令
    linux达人养成计划学习笔记(四)—— 压缩命令
    linux达人养成计划学习笔记(三)—— 帮助命令
  • 原文地址:https://www.cnblogs.com/sj20082663/p/3109259.html
Copyright © 2020-2023  润新知