姓名:胡湜 学号:SA12226279
一、实验内容
1、
编程实验fork(创建一个进程实体)->exec(将ELF可执行文件内容加载到进程实体)->执行。
2、
分析fork和exec系统调用在内核中的执行过程。
3、
注意task_struct进程控制块,ELF文件格式与进程地址空间的联系,注意exec系统调用返回到用户态时EIP指向
位置。
4、
动态链接库在ELF文件格式中与进程地址空间中的表现形式。
5、
总结以上实验和分析所得(300-500字)。
二、实验知识储备
1)进程的执行与PCB的结构
进程即程序的一次执行。从组成上看,进程可划分为三个部分:PCB、CODE与DATA。从动态执行的角度来看,进程可视为程序的指令在OS根据PCB进行调度而分配的若干时间片内对程序中数据的操作过程。
PCB是OS进程管理的依据和对象。为了调度,其中必须存有标识、状态、调度方法以进程管理的依据和对象。为了调度,其中必须存有标识、状态、调度方法以及进程的上下文等信息;而每个进程运行在各自不同的虚拟地址空间,其中就必须有虚实地址映射机制;为了控制,其中便存有进程链信息以及时钟定时器等;为了通讯,其中又必然有信号、信号量等机制。OS便是根据这些信息来控制的管理每个进程的创建、调度与切换以及消亡。
在LINUX中,结构task_struct即是PCB。
task_struct结构的组成主要可分为如下几个部分:
进程运行状态信息。一个Linux进程的运行、等待、停止以及僵死状态。
用户标识信息。执行该进程的用户的信息。
标识号。pid用以唯一标识一个进程,进程还有一个id用以标识它在进程数组中的索引。每个进程还有组号、会话号,这些标识用以判断一个进程是否有足够的优先权来访问外设等。
调度信息。调度策略、优先级等。
信号处理信息。信号挂起标志,信号阻塞掩码,信号处理例程等。
进程内部状态标志。调试跟踪标识,创建方法标识,用户id改变标识,最后一次系统调用时的错误码等等。这些标志一般与具体CPU有关。
进程链信息。Linux中有一个task数组,其长度为允许进程的最大个数,一般为512。
struct task_struct * task[NR_TASKS] =
{&init_task, };
对于每一个进程,有两个指针用以形成一个全体进程的循环双向链表(进程0为根),还有两个指针用以形成一个可运行进程的循环双向链表。还有一些指针用以指向父进程、子进程、兄弟进程等。
等待队列。用于wait4()函数等待子进程的返回。
时间与定时器。保存进程的建立时间,以及在其生命周期中所花费的CPU时间。应用程序还可以建立定时器,在定时器到期时,根据不同定时器类型发送相应的信号。
打开的文件以及文件系统信息。系统需要跟踪进程所打开的文件,以便在适当的时候关闭文件以及判断对文件操作的正确性。子进程从父进程处继承了父进程打开的所有文件的标识符。另外,进程还有指向VFS索引节点的指针,分别是进程的主目录以及当前目录。
内存管理信息。进程分别运行在各自的地址空间中,需要将虚实地址一一映射对应起来。
进程间通讯信息。信号量等。
上下文信息tss。在进程的运行过程中,必然伴随着系统状态的改变,这些状态将影响进程的执行。当调度程序选择了一个新进程以运行时,旧进程从运行状态切换为暂停状态,它的运行环境如寄存器堆栈等,必须保存在上下文中,以便下次恢复运行。显然,上下文信息与CPU类型紧密相连。
2)ELF文件格式简介
ELF文件格式:
ELF = Executable and Linkable
Format,可执行连接格式,是UNIX系统实验室()作为二进制接口(Application Binary
Interface,)而开发和发布的,也是Linux的主要可执行文件格式。
Executable and linking
format(ELF)文件是x86 Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
(1)适于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
(2)适于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。
(3)共享目标文件(shared object
file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
三、实验过程
1、
进程的创建:fork()
函数原型:
pid_t fork( void);
(pid_t 是一个,其实质是int 被定义在#include中)
返回值: 若成功调用一次则返回两个值,子进程返回0,返回子进程ID;否则,出错返回-1
函数执行过程:
1.通过查找pidmap_array位图,为子进程分配新的PID;
2.检查父进程的ptrace字段(current->ptrace),如果它的值不是0,说明有另一个进程正在跟踪父进程;fork()检查编译器程序自己是否想跟踪子进程。在这种情况下,如果子进程不是内核线程那么fork()函数设置CLONE_PTRACE标志;
3.调用copy_process()复制进程描述符,若所有必需的资源都是可用的,该函数返回刚创建的task_struct描述符地址。
4.如果设置了CLONE_PTRACE标志获知必须跟踪子进程,那么子进程的状态被设置成TASK_STOPPED,并未子进程增加挂起的SIGSTOP信号。在另外一个进程把子进程的状态恢复成TASK_RUNNING之前的状态,子进程则保持TASK_STOPPED,等待被唤醒;
5.如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task()函数执行以下操作:
执行结果:
函数结果分析,fork函数执行时由于在复制时复制了的,代码和数据都分成2份。在子进程中为count重新分配内存空间,故不会对结果有影响,所以父进程中的count是13,子进程中的是31.
2、
执行结果:
实验分析:实验中结果可看出此函数共生成20个进程,同时killall 命令能够终止进程的运行。
3、killall-fork
函数原型:
格式:killall
杀死指定名字的进程。实际上是向名字为的所有进程发送SIGTERM信号,如果这些进程没有捕获这个信号,那么这些进程就会直接被干掉了。
格式:killall
-
格式:killall
-
4、execl-code
函数分析:exec函数族,顾名思义,就是一簇函数,他把当前替换成新的,而且该程序通常开始执行!
用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其
main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。有六种不同的exec函数可供使用,它们常常被统称为exec函数。这些exec函数都是UNIX进程控制。用fork可以创建新进程,用exec可以执行新的程序。exit函数和两个wait函数处理终止和等待终止。这些是我们需要的基本的进程控制。
说是exec系统调用,实际上在Linux中,并不存在一个exec()的函数形式,exec指的是一组函数,一共有6个,分别是:
#include
extern char **environ;
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是真正意义上的,其它都是在此基础上经过包装的
执行结果:
函数结果分析:通过调用execvp函数,将命令“ls -l”执行。
三、实验总结
fork 上采用
Copy-On-Write,会省去一次不必要的进程空间复制,诸如copy on write以及子进程先运行会让fork
变得更聪明
装载的方式有两种:覆盖载入和页映射。
覆盖载入需要程序员手动将程序分割成若干块,然后编写一个模块管理代码,管理这些模块以决定什么时候载入内存,什么时候应该被替换出内存。
进程建立过程,首先是创建虚拟地址空间,实际上只是分配一个页目录就可以了,不需要设置页映射关系。然后,读取可执行文件头,建立虚拟空间与可执行文件的映射关系,这种映射关系只是保存在操作系统中的一个数据结构(可执行文件与执行文件进程的虚拟空间的映射关系)。最后,将CPU指令寄存器设置成可执行文件入口,启动运行。在实验中,实际例子do_exece()分析了ELF装载的大致过程,中间实现了动态链接。