一、实验目的与要求:
通过进程的创建、撤销和运行加深对进程概念和进程并发执行的理解,明确进程与程序之间的区别。
二、方法、步骤:
- 掌握在Linux中编译运行的方法。
- 阅读例程,理解函数fork()、execl()、exit()、getpid()和waitpid()的功能和用法
- 运行例程,分析例程中关键代码的功能,给出运行结果并对运行结果进行分析说明。
- 模仿例程,编写一段程序实现以下功能:
- 使用系统调用fork()创建两个子进程
- 各个子进程显示和输出一些提示信息和自己的进程标识符。
- 父进程显示自己的进程ID和一些提示信息,然后调用waitpid()等待多个子进程结束,并在子进程结束后显示输出提示信息表示程序结束。
- 创建多个(3个以上)进程并发运行,控制好各个子进程输出自己的进程标识符和一些提示信息,对程序运行结果进行分析说明。观察各个子进程并发执行的顺序,输出结果是否与设想中的顺序不同,并分析原因。
- 实现完美二叉树形式的进程树
三.实验过程及内容:
1.掌握在Linux中编译运行的方法
- 编写源代码:使用文本编辑器或者IDE编写C或C++源代码文件,例如hello.c或hello.cpp
- 保存源代码:将源代码文件保存在工作目录
- 打开终端:打开Linux终端
- 切换到源代码所在目录:使用cd命令,例如cd /path/codes
- 编译源代码:使用GCC编译器编译源代码
例如:gcc -o output_filename input_filename.c
运行可执行文件:编译成功后,可以运行生成的可执行文件。
例如:./output_filename
终端中将显示程序的输出。
2. 阅读例程,理解函数 fork()、execl()、exit()、getpid()和 waitpid()的功能和用法
2.1 fork 函数
功能:fork()创建一个新的进程,新进程是调用进程的副本。
在父进程中,fork()返回新创建子进程的进程 ID;在子进程中,fork()返回 0。如果出现错误,fork()返回-1。
用法:
图1.fork函数用法示例
2.2 execl函数
功能:用于在当前进程中执行另一个程序,通常与fork()结合使用。execl()的参数包括要执行的程序的路径以及传递给该程序的参数,最后一个参数必须是NULL。
用法:execl("/bin/ls", "ls", "-l", NULL);
2.3 exit函数
功能:用于终止当前进程的执行,并返回一个状态码。这个状态码可以被父进程通过wait()或waitpid()函数获取。
用法:exit(0); // 正常退出
exit(1); // 异常退出
2.4 getpid函数
功能:获取当前进程的进程ID。
用法:pid_t pid = getpid();
2.5 waitpid函数
功能:等待特定子进程的结束,阻塞父进程直到子进程结束或出错。它可以指定要等待的子进程ID,也可以使用-1表示等待任意子进程。
用法:int status;
pid_t child_pid = waitpid(-1, &status, 0);
3.运行例程,分析例程中关键代码的功能,给出运行结果并对运行结果进行分析说明。
3.1 例程1
代码分析:简单的输出函数,终端显示Hello World!!
运行结果:
图2. 例程1运行结果
结果分析:正常输出,说明编译运行环境正常
3.2 例程2
代码分析:
(1)自定义tpirntf函数输出hh:mm:ss格式的当前时间和当前进程pid
(2) 父进程首先输出自己的进程ID(PID),然后调用fork()创建子进程。
子进程在创建后会输出自己的进程ID,并且循环3次输出自己的ID和循环次数,每次间隔1秒。
父进程在创建子进程后会输出一条消息表示fork了一个子进程,并等待子进程退出。
父进程调用waitpid()等待子进程退出,然后输出子进程退出的消息和自己退出的消息。
运行结果:
图3. 例程2运行结果
结果分析:父进程创建一个子进程,父子进程各自输出一些信息
3.3 例程3
代码分析:
父进程调用fork()创建子进程,子进程在创建后等待5秒(使用sleep(5)),然后输出一条信息表示子进程正在运行,并调用execl("/bin/ps", "-a", NULL);来执行ps -a命令查看进程列表。
父进程在创建子进程后输出一条信息表示父进程正在运行,并等待1秒。随后输出一条信息表示fork了一个子进程,并调用waitpid()等待子进程退出。
子进程执行execl()后,会替换自己的进程映像为ps -a命令,因此之后的代码不会被执行。
运行结果:
图3. 例程3运行结果
结果分析:execl函数是用于执行指定路径下的可执行文件的一个函数。它的作用是将当前进程替换为一个新的进程,并执行指定的可执行文件。在替换之后,原进程的代码、数据和堆栈等都会被新进程完全取代,原进程的执行流程也就结束了。新进程继承了原进程的PID,因此在正常情况下,execl函数调用后的代码是不会再执行的,除非调用失败才会继续执行后续的代码。
You should never see this because the child is already gone.这条消息并没有出现,因为子进程已经被ps -a替换了。
4、 模仿例程,编写一段程序实现以下功能:
a) 使用系统调用fork()创建两个子进程
b) 各个子进程显示和输出一些提示信息和自己的进程标识符。
c) 父进程显示自己的进程ID和一些提示信息,然后调用waitpid()等待多个子进程结束,并在子进程结束后显示输出提示信息表示程序结束。
图4. 生成2个子进程的代码
运行结果:
图5. 生成2个子进程的结果
5、 创建多个(3个以上)进程并发运行,控制好各个子进程输出自己的进程标识符和一些提示信息,对程序运行结果进行分析说明。观察各个子进程并发执行的顺序,输出结果是否与设想中的顺序不同,并分析原因。
图6. 创建4个进程的代码
代码思路:使用信号量,控制每次最多只有1个子进程在运行,实现并发控制
设想:输出顺序为子进程1,2,3,4,父进程
实际结果:子进程执行顺序并不总是按照1,2,3,4
图7. 创建4个进程的运行结果
分析:这是由于操作系统调度算法的不确定性所导致的。操作系统可能会根据进程的优先级、运行时间、等待时间等因素来决定进程的执行顺序。因此,每个子进程执行的顺序并不是确定的。
6. 完美二叉树形式的进程树
思路:每个父进程内2次调用fork()创建子进程,然后在每个进程中根据当前的层数计算出当前进程的编号(numb),输出进程编号,pid, ppid
图8.完美二叉树进程树代码
运行结果:以3层的二叉树为例。不使用pstree,是因为父进程使用waitpid来等待每个子进程的结束,找不到使用pstree合适的位置。
图9. 完美二叉树进程树运行结果
图10. 二叉树中各个进程的PID