您的位置:首页 > 其它

【实验二】进程的创建与可执行程序的加载

2013-05-30 14:46 387 查看
嵌一班 王群峰 SG***028

话题引入

先由一个简单的进程创建的例子引入话题。

且看代码:

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
pid_t pid;
pid = fork();
//创建一个新的进程
if(pid==0)
{//子进程执行块
printf("I'm the child, I'm going to execute .\n");
execl("/usr/bin/tree","tree",NULL);
//执行/usr/bin/tree
exit(-1);//如果执行到此处,说明execl发生了错误
}
else if(pid>0)
{//父进程执行块
printf("I'm the parent, my child's pid is %d\n",pid);
//收集僵尸进程
wait(NULL);
}
else
{perror("error while forking...");}
}

上述代码演示了一个进程创建和执行的过程。

进程的输出结果如下:



由例子可以看出,1.我们可以在应用程序中使用fork()系统调用来把一个正在执行的进程分成两个部分,并且使两个部分都被操作系统执行,即创建一个新的进程;2.我们可以使用execl()系统调用来使当前进程放弃自己的工作,转去执行另外一个可执行程序。

Unix采用了一个独特的方式来实现进程的创建。它把整个进程创建的步骤分为两个部分:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID、PPID和某些资源的统计量(例如:挂起的信号,这个没有必要被继承)。Exec()函数负责读取可执行文件并将其载入地址空间开始运行。

由此,我们展开分析如下节。

 

fork()系统调用

Linux通过clone()系统调用来实现fork()。这个系统调用通过一系列的参数标志来指明父、子进程需要共享的资源。Fork()、vfork()、__clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()。

Do_fork()完成了创建进程的大部分工作。该函数调用copy_process()函数,然后让进程开始运行。Copy_process()函数主要完成以下工作:

1.调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。

2.检查并确保新创建这个子进程后,当前用户拥有的进程数目没有超出给它分配的资源的限制。

3.将子进程与父进程区别开来,主要是将进程描述符内的许多成员初始化,设置子进程的状态,并调用alloc_pid()为新进程分配一个有效的PID。

4.根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。

5.最后copy_process()做扫尾工作并返回一个指向子进程的指针

再回到do_fork()函数中,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。

execl()函数族

Linux提供了一组exec()函数,用来把当前进程映像替换成新的程序文件,并且停止原有进程开始执行新的程序。在Linux中使用fork()创建子进程后,子进程往往要调用一种exec()函数以执行另一个程序。

int execl(const char *path, const char *arg, ...);

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

int execle(const char *path, const char *arg, ..., char * const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execve(const char *path, char *const argv[], char *const envp[]);

这些系统调用中,只有execve()是真正的系统调用,其他函数都是由此函数封装而成的库函数。在内核中,相应的函数为exec.c/do_execve()。Do_execve()函数执行过程如下:

它首先检查当前进程是否能执行参数指定的可执行文件,然后读取该可执行文件,复制进程的参数和进程的环境变量,改变当前进程的相关信息,使得CPU开始执行被加载的程序,如果执行失败,则该函数返回的是错误码。

exit()进程的终结

当一个进程终止时,内核释放这个进程拥有的资源,并且通知该终止进程的父进程。

    通常,进程都是自杀。自杀发生在进程调用exit()系统调用时,无论是显式的准备终止进程还是从任一程序的main函数返回都会调用exit()(例如在C编译器 会放一个exit调用在main函数的后面)。一个进程也可能偶然的终止。这个在进程收到一个信号或者无法操作或忽略的异常时发生。不管进程是怎么终止的,这部分的工作都是由do_exit()来完成.。该函数定义在kernel/exit.c中,主要完成下面几个工作:

1、设置task_struct的标志成员为PF_EXITING标志

2、调用del_timer_sync()来移除内核定时器,在返回前,确保没有任何进入队列的定时器和相关定时器的操作正在运行

3、如果BSD进程计数功能启用,do_exit()调用acct_update_integrals()来取消计数的信息

4、调用exit_mm()释放该进程的mm_strcut。如果没有其他进程正在使用该地址空间,即该地址空间没有被共享,内核将会销毁它

5、调用exit_sem(),如果进程正在队列中等候一个IPC信号,则将它从队列中移除

6、调用exit_files()和exit_fs()分别来减少与文件描述符和文件系统数据关联的对象的使用计数。如果这2个计数任一个达到0,这个对象表示没有进程使用它了,销毁掉它

7、将任务的退出代码(存储在task_struct中的exit_code成员中)设置为exit()使用的代码,或者执行内核机制允许的终止动作。

8、调用exit_notify()发送一个信号给任务的父进程,给任务的子进程找新爹,这新爹可以是线程组中其他的线程或者是Init进程。并设置存储在task_struct结构中的exit_state为EXIT_ZOMBIE

9、do_exit()调用schedule()来切换到一个新的进程。因为进程现在已经不能调度了,此处已经是代码的最后了,任务不会执行了,do_exit()不会返回

      此时,所有与任务有关的对象都"自由"了。任务处于僵死状态(EXIT_ZOMBIE)不能运行。任务占有的内存是任务的内核栈,thread_info结构,task_struct结构。这些只是个它父进程提供些信息,在父进程检索这些信息后或者通知内核内存不再需要时,由进程保留的这块内存将被释放,以便系统使用。

ELF格式

Executable and Linkable Format,即可执行和可链接格式,通常被成为ELF格式,是一种用于执行档、目的档、共享库和核心转储的标准文件格式。ELF文件具有很大的灵活性,它通过文件头组织整个文件的总体结构,通过节区表 (Section Headers Table)和程序头(Program Headers Table或者叫段表)来分别描述可重定位文件和可执行文件。目标文件可分为三种主要类型:

可重定位的目标文件

可执行的目标文件

共享的目标文件

可执行和共享的目标文件静态的描绘了程序,但不管是哪种类型,它们都需要它们的主体,即各种节区。在可重定位文件中,节区 表描述的就是各种节区本身;而在可执行文件中,程序头描述的是由各个节区组成的段(Segment),以便程序运行时动态装载器知道如何对它们进行内存映像,从而方便程序加载和运行。

ELF文件的各部分在文件中分布如下表:



使用readelf工具可以方便的查看elf文件的一些信息。如下所示:







Readelf工具的详细用法可以使用readelf --help查看,如下所示:



 

动态库与ELF格式

在Unix中有两种重要的目标文件格式:a.out 和 elf。这两种格式中都有符号表,其中包括所有的符号(如程序的入口点、变量的地址等)。在elf格式中符号表的内容会比a.out中丰富的多。但是这些符号表可以用strip工具除去。这样的话这个文件就无法让debug程序跟踪了,但是会生成比较小的可执行文件。A.out文件中的符号表可以被完全除去,但是elf文件中的符号表在加载运行时起着重要的作用,所以其中的符号表不能呗完全去除。另外,使用strip去除符号表是不安全的,如果对未连接的目标文件使用strip去掉符号表,则会导致连接器无法连接。

当链接器进行连接时,它根据elf的符号表找到相应的函数入口地址,然后传递给目标文件,最终形成可执行文件,这个过程就叫做重定位。A.out没有重定位功能,在a.out格式中极难实现动态链接技术。ELF的动态链接库是内存位置无关的,而a.out的动态链接库是内存位置有关的,它一定要被加载到规定的内存地址才能工作。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: