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

来源:互联网 发布:卖淘宝店铺有风险吗 编辑:程序博客网 时间:2024/06/16 04:51

SA6373 陈**

.多进程的意义

当用户敲入一个命令来执行程序的时候,系统就会启动一个进程来执行相应的程序,而在执行程序的这个进程中,系统可能需要再次启动一个或多个进程来完成任务。其中两个基本的操作(fork,exec)用于创建和修改进程。

1.fork()

函数fork()用来创建一个新的进程,该进程是当前进程的拷贝。由fork创建的新进程被成为子进程,fork函数被调用一次,但返回两次,两次返回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID,出错则返回-1。子进程是父进程的副本,子进程获得附近成数据空间、堆和栈,但是父子进程并不共享这些存储空间部分,父子进程共享正文段。

#include<unistd.h>

pid_tfork(void)

2.exec()

函数族exec()用来启动另外的进程以取代当前运行的进程,该进程执行的程序完成替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以今后的进程ID并不会改变,exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。

6中不同的exec函数可供使用,这些exec函数使进程控制原语更加完美。用fork创建新进程,用exec可以执行新程序。exec()若成功则不返回值,若出错则返回-1


.程序演示

我们首先编写一个程序,来打印一句话:

childp.c

#include<stdio.h> int  main() { printf("子进程正在运行新程序\n"); return 0; }

然后通过编译生成可执行文件childp

接着我们编写进程创建程序,在子进程中调用该执行文件:

fork.c

#include<stdio.h> #include<stdlib.h> #include<unistd.h> void main()  {      int i;      if ( fork() == 0 )      {         /* 子进程程序 */        printf("子进程开始运行\n");        execl("./childp","childp",NULL);        printf("子进程运行结束\n");   }    else    {       /* 父进程程序*/       printf("父进程开始运行\n");       printf("父进程运行结束\n");    } }

该程序在执行后会在子进程中调用新程序childp来进行,而父进程时打印一句话。

下图是执行结果:

其流程如图

所以由运行结果可知未能输出”子进程运行结束”该语句,当执行exec时新的程序完全替代的原有的程序,其进程也被替代.

.forkexec在内核中的执行过程

1.fork()

1)通过查找pidmap_array位图,为子进程分配新的PID.

2)检查父进程的ptrace字段.

3)调用copy_process()复制进程描述符,如果所有必须的资源是可用的,该函数返回刚创建的task_struct描述符地址.

4)根据CLONE_STOPPED标志进行不同的操作.

5)如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间.

6)结束并返回子进程PID.

示例图如下所示:

2.exec()执行过程

系统中存在一个formats链表,其链表结构分别对应一种可执行文件的执行方法,execl()函数对应的系统调用sys_exec()函数会分配一个linux_binprm数据结构并将可执行文件的数据拷贝到其中,并依次扫描formats链表试图执行这个可执行文件,一旦找到了就执行链表结构中的load_binary方法,其主要步骤为:

1)将可执行文件的首部拷贝至内存;

2)根据动态链接程序路径名将共享库对应函数映射到内存;

3)释放原进程的内存描述符、线性区描述符、所有页框;

4)选择线性区的布局;

5)为可执行文件的代码段、数据段以及动态链接程序的代码段、数据段分别进行内存映射;

6)修改内核态堆栈中eipesp寄存器的值,使其分别指向程序的入口点以及新的用户态堆栈顶并返回;

.task_struct \ELF及动态链接

         在linux中每一个进程都由task_struct数据结构来定义.task_struct就是我们通常所说的PCB.当我们调用fork(),系统会为我们产生一个task_struct结构。然后从父进程,那里继承一些数据,并把新的进程插入到进程树中,以待进行进程管理。具体结构如图所示(图片引用)

ELF文件格式中的ELF头部、段头部表对应进程地址空间中的代码段,在加载可执行文件时,会把它们映射到进程地址空间中的代码段区域。ELF文件格式中的.data对应进程地址空间中的数据段,在加载可执行文件时,会把它们映射到进程地址空间的数据段区域。具体格式如图:

动态链接要符合两个特点:

1、动态的加载,就是当这个运行的模块在需要的时候才被映射入运行模块的虚拟内存空间中,如一个模块在运行中要用到mylib.so中的myget函数,而在没有调用mylib.so这个模块中的其它函数之前,是不会把这个模块加载到你的程序中(也就是内存映射),这些内容在内核中实现,用的是页面异常机制(我可能在另一篇文章中提到这个问题)。

2、动态的解析,就是当要调用的函数被调用的时候,才会去把这个函数在虚拟内存空间的起始地址解析出来,再写到专门在调用模块中的储存地址内,如前面所说的你已经调用了myget,所以mylib.so模块肯定已经被映射到了程序虚拟内存之中,而如果你再调用mylib.so中的myput函数,那它的函数地址就在调用的时候才会被解析出来。

连接器在可执行文件中标记出程序调用外部函数的位置,并不把代码复制进去,只是标出函数在动态连接库中的位置。用这样的方式生成的特殊可执行文件就是动态连接的。在运行这种动态程序时,系统在运行时把该程序调用的外部函数地址映射到程序地址,这就是所谓的动态连接,系统就有一个程序叫做动态连接器,在动态连接的程序执行前都要先把地址映射好。elf的动态连接库是内存位置无关的,它的动态连接库是在运行时处理符号的,这是通过用符号表和再布置表来实现的。


原创粉丝点击