【实验二】进程的创建与可执行程序的加载
2013-05-30 14:46
387 查看
嵌一班 王群峰 SG***028
且看代码:
上述代码演示了一个进程创建和执行的过程。
进程的输出结果如下:
由例子可以看出,1.我们可以在应用程序中使用fork()系统调用来把一个正在执行的进程分成两个部分,并且使两个部分都被操作系统执行,即创建一个新的进程;2.我们可以使用execl()系统调用来使当前进程放弃自己的工作,转去执行另外一个可执行程序。
Unix采用了一个独特的方式来实现进程的创建。它把整个进程创建的步骤分为两个部分:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID、PPID和某些资源的统计量(例如:挂起的信号,这个没有必要被继承)。Exec()函数负责读取可执行文件并将其载入地址空间开始运行。
由此,我们展开分析如下节。
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()函数,这样可以避免写时拷贝的额外开销。
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()系统调用时,无论是显式的准备终止进程还是从任一程序的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结构。这些只是个它父进程提供些信息,在父进程检索这些信息后或者通知内核内存不再需要时,由进程保留的这块内存将被释放,以便系统使用。
可重定位的目标文件
可执行的目标文件
共享的目标文件
可执行和共享的目标文件静态的描绘了程序,但不管是哪种类型,它们都需要它们的主体,即各种节区。在可重定位文件中,节区 表描述的就是各种节区本身;而在可执行文件中,程序头描述的是由各个节区组成的段(Segment),以便程序运行时动态装载器知道如何对它们进行内存映像,从而方便程序加载和运行。
ELF文件的各部分在文件中分布如下表:
使用readelf工具可以方便的查看elf文件的一些信息。如下所示:
Readelf工具的详细用法可以使用readelf --help查看,如下所示:
当链接器进行连接时,它根据elf的符号表找到相应的函数入口地址,然后传递给目标文件,最终形成可执行文件,这个过程就叫做重定位。A.out没有重定位功能,在a.out格式中极难实现动态链接技术。ELF的动态链接库是内存位置无关的,而a.out的动态链接库是内存位置有关的,它一定要被加载到规定的内存地址才能工作。
话题引入
先由一个简单的进程创建的例子引入话题。且看代码:
#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的动态链接库是内存位置有关的,它一定要被加载到规定的内存地址才能工作。
相关文章推荐
- 实验二 进程的创建与可执行程序的加载
- 实验二:进程的创建与可执行程序的加载
- 【实验二】进程的创建与可执行程序的加载
- 【实验二】进程的创建与可执行程序的加载
- Linux操作系统实验二:进程的创建与可执行程序的加载
- 实验二(进程的创建与可执行程序的加载)
- 实验二:进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- Linux操作系统分析(二)进程的创建与可执行程序的加载
- 【Linux操作系统分析】进程的创建与可执行程序的加载
- 进程信号Linux操作系统分析(2)- 进程的创建与可执行程序的加载
- 关于fork&exec之进程的创建和可执行程序的加载过程
- 进程的创建与可执行程序的加载
- 【Linux操作系统分析】进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- Linux操作系统分析-(2)进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- Linux操作系统分析-lab2-进程的创建与可执行程序的加载