进程的创建与可执行程序的加载

姓名:殷晓婷           学号:SA*****195

一、实验内容

  • 参考进程初探 编程实现fork(创建一个进程实体) -> exec(将ELF可执行文件内容加载到进程实体) -> running program
  • 参照C代码中嵌入汇编代码示例及用汇编代码使用系统调用time示例分析fork和exec系统调用在内核中的执行过程
  • 注意task_struct进程控制块,ELF文件格式与进程地址空间的联系,注意Exec系统调用返回到用户态时EIP指向的位置。
  • 动态链接库在ELF文件格式中与进程地址空间中的表现形式
  • 通过300-500字总结以上实验和分析所得,实验情况和分析的关键代码可以作为总结后面的附录以提供详细信息。

二、实验过程

1)进程的创建

要想彻底清楚Linux的fork,需要了解以下知识:

  • Linux的进程是怎样执行的
  • 父子进程的关系
  • 进程是如何切换的
  • fork的返回值

1.1 Linux的进程是怎样执行的?

Linux是多用户和多进程的操作系统,进程在操作系统中的创建,都会生成一个进程描述符(task_struct),描述进程的所有信息,包括数据段、代码段、堆栈段的地址,进程的环境变量、文件描述符等。

task_struct

为了描述和控制进程的运行,操作系统为每个进程定义了一个数据结构,即进程控制块(Process Control Block,PCB)。我们通常所说的进程实体包含程序段,数据段和PCB三部分。PCB在进程实体中占据重要的地位。所谓的创建进程,实质上就是创建PCB的过程;而撤销进程,实质上也就是对PCB的撤销。在Linux内核中,PCB对应着一个具体的结构体—task_struct,也就是所谓的进程描述符(process descriptor)。该数据结构中包含了与一个进程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。include/linux/sched.h包含有struct task_struct的定义:


Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info,进程的内核堆栈。


1.2 父子进程的关系

子进程由父进程创建,子进程是父进程的copy,它们共享代码段,拥有各自的堆和栈(自己的地址空间)。

1.3 进程是如何切换的

每个进程由操作系统分配一个给定的时间片,时间片到了就从可运行进程队列中取出一个可运行进程,切换进程上下文,执行切换后的进程。

1.4 fork的返回值

fork函数创建一个新的进程,前面我们提到过这个新的进程(child process)是父进程(parent process)的一个拷贝,子进程和父进程使用相同的代码段,子进程复制父进程的堆栈段和数据段。子进程虽然继承了父进程的数据但它们之间并不共享数据,通过进程间通信来实现信息的交互。操作系统通过fork函数的返回值来区分父子进程,对于父进程,fork函数返回子进程的进程号PID,对于子进程,fork函数返回0。

2)可执行程序的加载

exec函数的作用是在当前进程里执行可执行文件,也就是根据指定的文件名找到可执行文件,用它来取代当前进程的内容,并且这个取代是不可逆的,即被替换掉的内容不再保存,当可执行文件结束,整个进程也随之僵死。因为当前进程的代码段、数据段和堆栈等都已经被新的内容替代,所以exec函数族的函数执行成功后不会返回,失败后返回-1。

exec函数族不止一个,在Linux中它们分别是:execl,execlp,execle,execv,execve,execvp,下面以execl为例,其他函数与execl的区别可通过man exec命令来了解它们的具体情况。

execl的原型如下:

int execlp(const char *file , const char *arg ,...);

编程实现fork(创建一个进程实体) -> exec(将ELF可执行文件内容加载到进程实体) -> running program 参见附录一

3)fork系统调用在内核中的执行过程

c代码中嵌入汇编代码示例及用汇编代码使用系统调用time示例参见附录二

用户程序并不直接使用系统调用,而是通过C库的API,而系统调用在内核中也不是直接实现的,而是通过调用各自对应的服务例程。fork系统调用在内核中对应的服务例程是sys_forksys_fork内部调用了do_fork(),而do_fork()又调用了copy_process(),可用下图来描述上述关系:


4)exec系统调用在内核中的执行过程

在内核中,exec函数族拥有统一的函数入口sys_execve,在用户态下调用execve(),引发系统中断后,在内核态执行的相应函数是do_sys_execve(),而do_sys_execve()会调用do_execve()函数。do_execve()首先会读入可执行文件,如果可执行文件不存在,则报错。否则检查可执行文件的权限,如果文件不是当前用户可执行的,则execve()会返回-1,报permission denied的错误,否则继续读入运行可执行文件所需的信息,接着系统调用search_binary_handler(),根据可执行文件的类型查找到相应的处理函数,然后执行相应的load_binary()函数开始加载可执行文件。

加载ELF类型文件的handler是load_elf_binary(),它先读入ELF文件的头部,根据ELF文件的头部信息读入各种数据(header information),再次扫描程序段描述表,找到类型为PT_LOAD的段,将其映射(elf_map())到内存的固定地址上。如果没有动态链接器的描述段,把返回的入口地址设置成应用程序的入口,完成这个功能的是start_thread(),start_thread()并不启用一个线程,而只是用来修改了pt_regs中保存的PC等寄存器的值,使其指向加载的应用程序的入口。这样当内核操作结束,返回用户态的时候,接下来执行的就是应用程序了。

如果应用程序中使用了动态链接库,内核出了加载指定的可执行文件,还要把控制权交给动态链接器以处理动态链接的程序。内核搜寻段表,找到标记为PT_INTERP的段中所对应的动态链接器的名称,并使用load_elf_interp()加载其映像,并把返回的入口地址设置成load_elf_interp()的返回值,即动态链接器入口。当execve退出的时候动态链接器接着运行。动态链接器检查应用程序对共享连接库的依赖性,并在需要时对其进行加载,对程序的外部引用进行重定位。然后动态链接器把控制权交给应用程序,从EFL文件头部中定义的程序进入点开始执行。

5)ELF文件格式与进程地址空间的关系

ELF文件格式如下图所示:


Exec系统调用返回到用户态时EIP指向的位置是ELF可执行程序的入口。

每个进程都有自己独立的进程地址空间,在Linux中默认高1G为内核空间,低3G为用户空间。ELF格式中的ELF头部、段头部表、.init、.text、.rodata段对应进程地址空间中的代码段,在加载可执行文件时,会把它们映射到进程地址空间中的代码段区域,ELF格式中的.data、.bss段对应进程地址空间中的数据段,在加载可执行文件时,会把它们映射到进程地址空间中的数据段区域。

6)动态链接库在ELF文件格式中与进程地址空间中的表现形式

动态链接库在ELF文件中对应着.dynamic段所包含的信息,包括动态链接器所需要的相关信息,动态链接库会像普通共享对象一样被映射到进程地址空间,不同的是动态链接库被映射到共享库区域段。

三、实验总结

父进程fork出一个子进程,子进程使用exec函数族来脱离和父进程的关系,加载可执行文件到内存中。如果加载的可执行文件使用了动态链接库,就需要加载动态链接器,进一步加载可执行文件使用到的动态链接库到内存,并重定位以供可执行文件调用,最后从可执行文件的入口地址开始执行。以上就是Linux下多进程的内核执行过程分析,通过API函数fork()和exec()函数族,用户可以方便地创建子进程加载可执行文件并执行。

四、附录

附录一



附录二

     C代码中嵌入一般汇编代码的方式


     C代码中嵌入系统调用汇编代码












评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值