附1: 创建进程(开设Linux基础课程班级选作)
【实验目的】
1)通过运行程序,理解进程的创建,掌握程序与进程的区别和联系。 2)理解并掌握fork系统调用的用法。 3)理解并掌握exec系统调用的用法。
【条件要求】
1)认真阅读和掌握本实践的指导材料。 2)上机操作。
【预备知识】 一、程序与进程的定义
程序是为了完成某项任务而编排的语句序列,它告诉计算机如何执行,因此程序是需要运行的。程序运行过程中,需要占有计算机的各种资源。
进程是具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。进程实体由PCB、用户程序段、用户数据段和栈组成。这里的PCB和栈是由操作系统为用户程序添加的信息。见图6-1。
PCB程序段数据段栈 图6-1 进程示意图
二、进程和程序的区别与联系
程序是静态概念,本身可以作为一种软件资源保存;而进程是程序的一次执行过程,是动态概念,它有一定的生命期,是动态地产生和消亡的。
进程是一个能独立运行的单位,能与其他进程并发执行,进程是作为自愿申请和调度单
位存在的;而通常的程序不能作为一个独立运行的单位。
程序与进程并无一一对应关系,一方面一个程序可由多个进程共用;另一方面一个进程只能对应一个程序。进程和程序的关系犹如演出和剧本的关系。
三、fork系统调用
一个进程调用了fork以后,系统会创建一个子进程。这个子进程和父进程的不同之处在于进程ID和父进程ID,其他都一样。
当一个程序中调用fork函数后,内核会完成如下工作: ● 内核系统分配新的内存块和内核数据结构; ● 复制原来的进程到新的进程; ● 向运行进程集添加新的进程; ● 将进程返回给两个进程。
设原来的进程为父进程,调用fork生成的新进程为子进程,则子进程会执行父进程中fork函数后的代码。
fork系统调用使用格式: ● 头文件:
#include
● 函数原形为: pid_t fork(void);
● 返回值为“pid_t”。
对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。这样,对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。如果调用不成功,则返回“-1”。
四、exec族函数
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。换言之,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
exec系统调用有6种不同的使用格式,但在核心中只对应一个调用入口。它们有不同的调用格式和调用参数。这六种调用格式分别为: #include
1)int execl (const char *path, const char *arg0, ..., const char*argn, (char *)0); 2)int execv (const char *path, char *const *argv);
3)int execle (const char *path, const char *arg0, ..., const char*argn,(char *0), const char *envp[]);
4)int execve (const char *path, char *const *argv, char *const *envp); 5)int execlp (const char *file, const char *arg0, ..., const char*argn, (char *)0); 6)int execvp (const char *file, char *const *argv);
参数“path”指出一个可执行目标文件的路径名,参数“file”指出可执行目标文件的文件名。“arg0”作为约定同“path”一样指出目标文件的路径名,参数“arg1”到“argn”分别是该目标文件执行时所带的命令行参数。参数“argv”是一个字符串指针数组,由它指出该目标程序使用的命令行参数表,按约定第一个字符指针指向与“path”或“file”相同的字符串。最后一个指针指向一个空字符串,其余的指向该程序执行时所带的命令行参数。参数“envp”同“argv”一样也是一个字符指针数组,由它指出该目标程序执行时的进程环境,它也以一个空指针结束。
exec的6种格式在以下三点上有所不同:
1)“path”是一个目标文件的完整路径名,而“file”是目标文件名,它可以通过环境变量“PATH”来搜索。
2)由“path”或“file”指定的目标文件的命令行参数,是完整的参数列表或是通过一指针数组“argv”给出的。
3)环境变量是系统自动传递或者通过“envp”给出的。 表6-1说明了exec系统调用的6种不同格式对以上三点的支持。
表6-1 exec系统调用的6种不同格式
系统调用 execl execv execle execve execlp execvp 参数形式 全部列表 指针数组 全部列表 指针数组 全部列表 指针数组 环境传送 自动 自动 不自动 不自动 自动 自动 否 否 否 否 是 是 路径搜索 与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段、数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样。看上去还是旧的“躯壳”,却已经注入了新的“灵魂”。只有调用失败了,它们才会返回一个“-1”,从原程序的调用点接着往下执行。
现在我们应该明白Linux如何执行新程序:,每当有进程认为自己不能为系统和用户做出任何“贡献”了,它就可以发挥最后一点“余热”,调用任何一个exec,让自己以新的面貌“重生”;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化。我们已经知道,
fork可以将调用进程的所有内容原封不动地拷贝到新产生的子进程中,这些拷贝的动作很消耗时间,而如果fork完之后马上就调用exec,这些辛辛苦苦拷来的东西又会被立刻抹掉。这看起来非常不划算,于是人们设计了一种名为“写时拷贝(copy-on-write)”的技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制。这样,如果下一条语句是“exec”,它就不会作无用功了,因而也就提高了效率。
五、wait系统调用
wait系统调用可以完成父进程和子进程的同步。进程一旦调用了wait,就立即“阻塞”自己,由wait自动分析当前进程的某个子进程是否已经退出,如果让它找到了这样一个子进程,wait就会收集这个子进程的信息,并把它彻底销毁,然后返回;如果没有找到这样一个子进程,wait就会一直“阻塞“在这里,直到有一个出现为止。
wait系统调用使用格式为:
#include
参数“status“用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。如果我们对这个子进程是如何“死掉”的毫不在意,只想把这个“僵尸进程”消灭掉(事实上绝大多数情况下,我们都会这样想),就可以设定这个参数为“NULL”,就像这样: pid = wait(NULL);
【实验内容】
1)创建目录“ex6”,然后进入该目录。 # mkdir ex6 # cd ex6
2)键入“vi”,编辑一个新文件“exam6a.c”。 # vi exam6a.c
其内容如下: #include \
#include
char *parameter[3]; parameter[0] = \ parameter[1] = \ parameter[2] = 0 ;
printf(\printf(\ execvp( \printf(\) bye\\n\printf(\}
3)保存退出“exam6a.c”。 4)在Linux下编译“exam6a.c”。 # gcc -o exam6a exam6a.c
5)执行“exam6a”。 ./exam6a
6)仔细观察输出结果,查看下面两行是否输出: * * *ls is done. bye * * * BBBBBBBBBB*****
如果没有,试说明原因。
7)复制“exam6a.c”为“exam6b.c”,并用Vi编辑“exam6b.c”。 # cp exam6a.c exam6b.c # vi exam6b.c
其内容如下: #include \
#include
char *parameter[3]; parameter[0] = \ parameter[1] = \
parameter[2] = 0 ;
printf(\printf(\/*execvp( \fork();
printf(\printf(\}