进程运行轨迹的跟踪与统计
难度系数:★★★☆☆
实验目的
- 掌握Linux下的多进程编程技术;
- 通过对进程运行轨迹的跟踪来形象化进程的概念;
- 在进程运行轨迹跟踪的基础上进行相应的数据统计,从而能对进程调度算法进行实际的量化评价,更进一步加深对调度和调度算法的理解,获得能在实际操作系统上对调度算法进行实验数据对比的直接经验。
实验内容
进程从创建(Linux下调用fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态;在运行的过程中如果启动了一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出CPU;当文件读写完毕以后,操作系统会在将其切换成就绪态,等待进程调度算法来调度该进程执行……
本次实验包括如下内容:
- 基于模板“process.c”编写多进程的样本程序,实现如下功能:
- 所有子进程都并行运行,每个子进程的实际运行时间一般不超过30秒;
- 父进程向标准输出打印所有子进程的id,并在所有子进程都退出后才退出;
- 在Linux0.11上实现进程运行轨迹的跟踪。基本任务是在内核中维护一个日志文件/var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一log文件中。
- 在修改过的0.11上运行样本程序,通过分析log文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序,也可以使用python脚本程序—— stat_log.py(在/home/teacher/目录下) ——进行统计。
- 修改0.11进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。
/var/process.log文件的格式必须为:
pid X time
其中:
- pid是进程的ID;
- X可以是N,J,R,W和E中的任意一个,分别表示进程新建(N)、进入就绪态(J)、进入运行态(R)、进入阻塞态(W)和退出(E);
- time表示X发生的时间。这个时间不是物理时间,而是系统的滴答时间(tick);
三个字段之间用制表符分隔。
例如:
12 N 1056
12 J 1057
4 W 1057
12 R 1057
13 N 1058
13 J 1059
14 N 1059
14 J 1060
15 N 1060
15 J 1061
12 W 1061
15 R 1061
15 J 1076
14 R 1076
14 E 1076
......
实验报告
完成实验后,在实验报告中回答如下问题:
- 结合自己的体会,谈谈从程序设计者的角度看,单进程编程和多进程编程最大的区别是什么?
- 你是如何修改时间片的?仅针对样本程序建立的进程,在修改时间片前后,log文件的统计结果(不包括Graphic)都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?
评分标准
- process.c,50%
- 日志文件建立成功,5%
- 能向日志文件输出信息,5%
- 5种状态都能输出,10%(每种2%)
- 调度算法修改,10%
- 实验报告,20%
实验提示
process.c的编写涉及到fork()和wait()系统调用,请自行查阅相关文献。0.11内核修改涉及到init/main.c、kernel/fork.c和kernel/sched.c,开始实验前如果能详细阅读《注释》一书的相关部分,会大有裨益。
- 编写样本程序
所谓样本程序,就是一个生成各种进程的程序。我们的对0.11的修改把系统对它们的调度情况都记录到log文件中。在修改调度算法或调度参数后再运行完全一样的样本程序,可以检验调度算法的优劣。理论上,此程序可以在任何Unix/Linux上运行,所以建议在Ubuntu上调试通过后,再拷贝到0.11下运行。
process.c是样本程序的模板(在/home/teacher/目录下)。它主要实现了一个函数:
/*
* 此函数按照参数占用CPU和I/O时间
* last: 函数实际占用CPU和I/O的总时间,不含在就绪队列中的时间,>=0是必须的
* cpu_time: 一次连续占用CPU的时间,>=0是必须的
* io_time: 一次I/O消耗的时间,>=0是必须的
* 如果last > cpu_time + io_time,则往复多次占用CPU和I/O,直到总运行时间超过last为止
* 所有时间的单位为秒
*/
cpuio_bound(int last, int cpu_time, int io_time);
比如一个进程如果要占用10秒的CPU时间,它可以调用:
cpuio_bound(10, 1, 0); // 只要cpu_time>0,io_time=0,效果相同
以I/O为主要任务:
cpuio_bound(10, 0, 1); // 只要cpu_time=0,io_time>0,效果相同
CPU和I/O各1秒钟轮回:
cpuio_bound(10, 1, 1);
较多的I/O,较少的CPU:
cpuio_bound(10, 1, 9); // I/O时间是CPU时间的9倍
修改此模板,用fork()建立若干个同时运行的子进程,父进程等待所有子进程退出后才退出,每个子进程按照你的意愿做不同或相同的cpuio_bound(),从而完成一个个性化的样本程序。它可以用来检验有关log文件的修改是否正确,同时还是数据统计工作的基础。
wait()系统调用可以让父进程等待子进程的退出。
小技巧:
在Ubuntu下,top命令可以监视即时的进程状态。在top中,按u,再输入你的用户名,可以限定只显示以你的身份运行的进程,更方便观察。按h可得到帮助。
在Ubuntu下,ps命令可以显示当时各个进程的状态。“ps aux”会显示所有进程;“ps aux | grep xxxx”将只显示名为xxxx的进程。更详细的用法请问man。
在Linux 0.11下,按F1可以即时显示当前所有进程的状态。
- log文件
操作系统启动后先要打开/var/process.log,然后在每个进程发生状态切换的时候向log文件内写入一条记录,其过程和用户态的应用程序没什么两样。然而,因为内核状态的存在,使过程中的很多细节变得完全不一样。
打开log文件
为了能尽早开始记录,应当在内核启动时就打开log文件。内核的入口是init/main.c中的main()(Windows环境下是start()),其中一段代码是:
……
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}
……
这段代码在进程0中运行,先切换到用户模式,然后全系统第一次调用fork()建立进程1。进程1调用init()。在init()中:
……
setup((void *) &drive_info); //加载文件系统
(void) open("/dev/tty0",O_RDWR,0); //打开/dev/tty0,建立文件描述符0和/dev/tty0的关联
(void) dup(0); //让文件描述符1也和/dev/tty0关联
(void) dup(0); //让文件描述符2也和/dev/tty0关联
……
这段代码建立了文件描述符0、1和2,它们分别就是stdin、stdout和stderr。这三者的值是系统标准(Windows也是如此),不可改变。可以把log文件的描述符关联到3。文件系统初始化,描述符0、1和2关联之后,才能打开log文件,开始记录进程的运行轨迹。为了能尽早访问log文件,我们要让上述工作在进程0中就完成。所以把这一段代码从init()移动到main()中,放在move_to_user_mode()之后(不能再靠前了),同时加上打开log文件的代码。修改后的main()如下:
……
move_to_user_mode();
/***************添加开始***************/
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0); //建立文件描述符0和/dev/tty0的关联
(void) dup(0); //文件描述符1也和/dev/tty0关联
(void) dup(0); //文件描述符2也和/dev/tty0关联
(void) open("/var/process.log",O_CREAT|O_TRUNC|O_WRONLY,0666);
/***************添加结束***************/
if (!fork()) { /* we count on this going ok */
init();
}
……
打开log文件的参数的含义是建立只写文件,如果文件已存在则清空已有内容。文件的权限是所有人可读可写。
这样,文件描述符0、1、2和3就在进程0中建立了。根据fork()的原理,进程1会继承这些文件描述符,所以init()中就不必再open()它们。此后所有新建的进程都是进程1的子孙,也会继承它们。但实际上,init()的后续代码和/bin/sh都会重新初始化它们。所以只有进程0和进程1的文件描述符肯定关联着log文件,这一点在接下来的写log中很重要。
- 写log文件
log文件将被用来记录进程的状态转移轨迹。所有的状态转移都是在内核进行的。在内核状态下,write()功能失效,其原理等同于《系统调用》实验中不能在内核状态调用printf(),只能调用printk()。编写可在内核调用的write()的难度较大,所以这里直接给出源码。它主要参考了printk()和sys_write()而写成的:
#include "linux/sched.h"
#include "sys/stat