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

一、实验环境

1.操作系统:VirtualBox+Ubuntu 10.10 

2.硬件平台:32位X86

二、实验总结:

    进程就是处于执行期的程序。但进程并不是仅仅局限于一段可执行程序代码(Unix称其为代码段,text section)。通常进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread of execution),当然还包括用来存放全局变量的数据段等。实际上,进程就是正在执行的程序代码的实时结果,内核需要有效而透明地管理所有细节。

       Unix的进程创建很特别。许多其他的操作系统都要提供了产生进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix采用了与众不同的实现 方式,它把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在与PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数合起来使用的效果跟其他系统使用的单一函数的效果相似。

    可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行,当一个程序执行看系统调用或触发了某个异常,它就陷入了内核空间。此时,我们称内核为“代表进程执行”并处在进程上下文中。

      

附录:

1. fork()函数

      在Linux 中创建一个新进程的惟一方法是使用fork函数。fork 函数是Linux 中一个非常重要的函数,和读者以往遇到的函数也有很大的区别,它执行一次却返回两个值。

1)fork函数说明

      fork 函数用于从已存在进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。这两个分别带回它们各自的返回值,其中父进程的返回值是子进程的进程号,而子进程则返回0。因此,可以通过返回值来判定该进程是父进程还是子进程。使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。因此可以看出,使用fork函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得fork 函数的执行速度并不很快。

  (2) fork函数示例

#include <sys/types.h>

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>



int main(void)

{

	pid_t result;

	result = fork();

	if(result == -1){

		perror("fork");

		exit;

	}

	else if(result == 0){

		printf("The return value is %d\nIn child process!!\nMy PID is %d\n",result,getpid());

}

	else{

	printf("The return value is %d\nIn father process!!\nMy PID is %d\n",result,getpid());

	}

}
编译执行结果如下:






    从该实例中可以看出,使用fork 函数新建了一个子进程,其中的父进程返回子进程的PID,而子进程的返回值为0。

(3)fork函数的执行过程

      linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要的共享资源。然后由clone()去调用do_fork(),do_fork完成 了创建中的大部分工作。

1)查找pidmap_array位图,为子进程获取一个新的PID;
2)检查父进程的ptrace字段看是否有其他进程在跟踪父进程,因而,do_fork()检查debugger程序是否自己想跟踪子进程。
3)调用copy_process()函数,复制进程描述符。如果所有必须的资源都是可用的,它返回刚创建的描述符,task_struct描述符的地址;
4)结束并返回子进程的PID;
copy_process()函数的执行过程:
1)调用dup_task_struct()为新创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符相同的。
2)检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
3)子进程着手使自己与父进程区别开来。
4)子进程的状态被设置为TASK_UNINTERRUPTIBLE,以确保它不会投入运行。
5)copy_process()函数调用copy_flags()以更新task_struct的flags成员。
6)调用alloc_pid()为新进程分配一个有效的PID.
7)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
8)最后,copy_process()做扫尾工作并返回一个指向子进程的指针。
2.exec函数

        exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

  (1)exec函数说明

       与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

       在Linux下每当有进程认为自己不能为系统和用户做出任何贡献了,就可以调用任何一个exec,让自己以新的面貌重生;更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。

(2)exec函数示例

/*execl.c*/
#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>



int main()

{

	if(fork()==0){

		if(execl("/bin/ps","ps","-ef",NULL)<0)

			perror("execl error!");

	}

}
编译执行结果如下:


       在该程序中,首先使用fork函数新建一个子进程,然后在子进程里使用execl函数,该函数使用完整的文件目录来查找对应的可执行文件。此程序的运行结果与在Shell中直接键入命令“ps -ef”是一样的,当然,在不同的系统不同时刻都可能会有不同的结果。

(3)exec函数执行的过程

       do_execve的函数源代码位于./fs/exec.c文件中,当用户使用exec函数族的时候,系统统一调用do_execve来完成新的可执行(elf)文件运行前的准备工作,包括内存的分配、数据参数的载入以及对调用exec的旧进程的回收

        do_execve对可执行文件的载入分成两个阶段,第一个阶段是准备阶段,准备阶段完成对进程参数的预读(读入内核空间)和对于可执行文件格式的判断,并为对应格式的可执行文件选取加载器。第二个阶段就是载入阶段,完成对新进程数据段、代码段、bss等等信息向内存的载入。

粗略概括起来做了如下几件事情

1. 对新程序执行进程的证书,权限等信息的检查和控制
2. 可执行文件的检索,读取等
3. 可执行文件个格式的检测和匹配,例如shell脚本或者elf文件
4. 更新进程的运行环境,例如堆栈的分配
5. 可执行文件的加载和执行。


4.一个简单的shell程序

      该程序有两个进程,其中一个为父进程,一个该父进程创建的子进程并运行“ps  -ef”指令。父进程不阻塞自己,并等待子进程的退出消息,待收集到该信息,父进程就返回。

/*exp.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>



int main()
{ 
    pid_t child,child1;
    child1 = fork();
    
    /*error process */
    if ( child1 == -1 ){
        perror("child1 fork");
        exit(1);
        }

     else if (child1==0) {
             printf("In child1:execute 'ps -ef'\n");
         if (execl("/bin/ps","ps","ef",NULL)<0)
               perror(" child1 execl error!");
         }

      else {
               printf("In father process:\n");
               do {
                     child = waitpid( child1, NULL, WNOHANG );
                     if( child ==0) {
                         printf("The child1 process has not exited!\n");
                          sleep(1);
                           }
                }while ( child ==0 );
                if ( child == child1 ) 
                    printf("Get child1\n");
                  else 
                       printf("Error occured!\n");
             }
}
   
               
    
运行结果如下:



5. ELF文件和动态链接

(1) ELF文件

        目标文件既要参与程序链接又要参与程序执行。出于方便性和效率考虑,目标文件格式提供了两种并行视图,分别反映了这些活动的不同需求


      文件开始处是一个ELF 头部(ELF Header),用来描述整个文件的组织。节区部分包含链接视图的大量信息:指令、数据、符号表、重定位信息等等。

      程序头部表(Program Header Table),如果存在的话,告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。

     节区头部表(Section Heade Table)包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

   (2) 动态链接

    在构造使用动态链接技术的可执行文件时,连接编辑器向可执行文件中添加一个类型为PT_INTERP的程序头部元素,告诉系统要把动态链接器激活,作为程序解释器。系统所提供的动态链接器的位置是和处理器相关的。

Exec() 和动态链接器合作,为程序创建进程映像,其中包括以下动作:

  (1). 将可执行文件的内存段添加到进程映像中;

  (2). 把共享目标内存段添加到进程映像中;

  (3). 为可执行文件和它的共享目标执行重定位操作;

  (4). 关闭用来读入可执行文件的文件描述符,如果动态链接程序收到过这样的文件描述符的话;

  (5). 将控制转交给程序,使得程序好像从 exec直接得到控制。

        链接编辑器也会构造很多数据来协助动态链接器处理可执行文件和共享目标文件。这些数据包含在可加载段中,在执行过程中可用。如:

  类型为 SHT_DYNAMIC 的 .dynamic 节区包含很多数据。位于节区头部的结构保存了其他动态链接信息的地址。

  类型为 SHT_HASH 的 .hash 节区包含符号哈希表。

  类型为 SHT_PROGBITS 的 .got 和 .plt 节区包含两个不同的表:全局偏移表和过程链接表。

     因为任何符合ABI规范的程序都要从共享目标库中导入基本的系统服务,动态链接器会参与每个符合ABI规范的程序的执行。





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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值