实验四:进程运行轨迹的跟踪与统计
1. 基于模板 process.c
编写多进程的样本程序,实现如下功能: + 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒; + 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;
myprocess.c代码如下:
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
#define HZ 100
void cpuio_bound(int last, int cpu_time, int io_time);
int main(int argc, char * argv[])
{
pid_t n_proc[10]; /*10个子进程 PID*/
int i;
for(i=0;i<10;i++)
{
n_proc[i] = fork();
/*子进程*/
if(n_proc[i] == 0)
{
cpuio_bound(20,2*i,20-2*i); /*每个
子进程都占用20s*/
return 0; /*执行完cpuio_bound 以后,结束该子进程*/
}
/*fork 失败*/
else if(n_proc[i] < 0 )
{
printf("Failed to fork child process %d!\n",i+1);
return -1;
}
/*父进程继续fork*/
}
/*打印所有子进程PID*/
for(i=0;i<10;i++)
printf("Child PID: %d\n",n_proc[i]);
/*等待所有子进程完成*/
wait(&i);
return 0;
}
/*
* 此函数按照参数占用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
* 所有时间的单位为秒
*/
void cpuio_bound(int last, int cpu_time, int io_time)
{
struct tms start_time, current_time;
clock_t utime, stime;
int sleep_time;
while (last > 0)
{
/* CPU Burst */
times(&start_time);
/* 其实只有t.tms_utime才是真正的CPU时间。但我们是在模拟一个
* 只在用户状态运行的CPU大户,就像“for(;;);”。所以把t.tms_stime
* 加上很合理。*/
do
{
times(¤t_time);
utime = current_time.tms_utime - start_time.tms_utime;
stime = current_time.tms_stime - start_time.tms_stime;
} while ( ( (utime + stime) / HZ ) < cpu_time ); //除以HZ相当于乘以时钟滴答的时间间隔
last -= cpu_time;
if (last <= 0 )
break;
/* IO Burst */
/* 用sleep(1)模拟1秒钟的I/O操作 */
sleep_time=0;
while (sleep_time < io_time)
{
sleep(1);
sleep_time++;
}
last -= sleep_time;
}
}
struct tms 结构体在内核中的定义如下:
//描述进程及其子进程使用的 CPU 时间的结构
struct tms {
__kernel_clock_t tms_utime; //用户程序 CPU 时间
__kernel_clock_t tms_stime; //系统调用所耗费的 CPU 时间
__kernel_clock_t tms_cutime; //已死掉子进程的 CPU 时间
__kernel_clock_t tms_cstime; //已死掉子进程所耗费的系统调用 CPU 时间
};
clock_t 是一个长整型数
定义 HZ=100,内核的标准时间是jiffy,一个jiffy就是一个内部时钟周期,而内部时钟周期是由100HZ的频率所产生中的,也就是一个时钟滴答,间隔时间是10毫秒(ms).计算出来的时间也并非真实时间,而是时钟滴答次数,乘以10ms可以得到真正的时间。
运行结果:
主进程描述:主进程会依次打印10个孩子进程的pid,然后陷入阻塞,待20s后,找到一个变成僵尸的子进程后,接着往下运行至结束。
2. 在 Linux0.11 上实现进程运行轨迹的跟踪。 + 基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。
**代码修改参考:**https://blog.csdn.net/leoabcd12/article/details/122268321
实验思路:
操作系统启动后先要打开 /var/process.log,然后在每个进程发生状态切换的时候向 log 文件内写入一条记录,其过程和用户态的应用程序没什么两样,为了能尽早开始记录,应当在内核启动时就打开 log 文件。
Linux 0.11 支持四种进程状态的转移:就绪到运行、运行到就绪、运行到睡眠和睡眠到就绪,此外还有新建和退出两种情况。其中就绪与运行间的状态转移是通过 schedule()(它亦是调度算法所在)完成的;运行到睡眠依靠的是 sleep_on() 和 interruptible_sleep_on(),还有进程主动睡觉的系统调用 sys_pause() 和 sys_waitpid();睡眠到就绪的转移依靠的是 wake_up()。所以只要在这些函数的适当位置插入适当的处理语句就能完成进程运行轨迹的全面跟踪了。
**碰到问题:**重新编译后,编译运行process.c,可谓是步步出错,然后在徐东的帮助下成功解决。
**问题原因:**以为这个在线环境已经是0.11版本了,其实不然:
这也就意味这我从第一步开始就都是错的,错在选错目录下的文件,正确做法是先解压/home/shiyanlou/oslab目录下的压缩包,也就是0.11版本,然后在那个oslab文件夹里进行上述操作:
运行结果即所有进程的运行轨迹:
第一列:进程的 ID
第二列:N、J、R、W 和 E 中的任意一个,分别表示进程新建(N)、进入就绪态(J)、进入运行态®、进入阻塞态(W) 和退出(E)
第三列:表示第二列发生的时间。这个时间不是物理时间,而是系统的滴答时间(tick);
3. 在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序,也可以使用 python 脚本程序—— stat_log.py(在 /home/teacher/ 目录下) ——进行统计。
1.将stat_log.py(在 /home/teacher/ 目录下)拷贝到和process.log同目录下:
2.执行指令 sudo chmod +x ./stat_log.py 对 stat_log.py 加上权限:
3.执行指令 ./stat_log.py ./process.log 0 1 2 3 4 5 查看结果:
Process:进程号
Turnaround:作业从提交到完成所用的总时间
Waiting:等待时间
CPU Burst:CPU触发次数
I/O Burst:I/O触发次数
Average:平均周转时间和平均等待时间
Throughout:每秒的吞吐量。
吞吐量是指对网络、设备、端口、虚电路或其他设施,单位时间内成功地传送数据的数量(以比特、字节、分组等测量)。
4. 修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。
时间片的初始值是进程0的priority,是在linux-0.11/include/linux/sched.h的宏 INIT_TASK 中定义的,如下:我们只需要修改宏中的第三个值即可,该值即时间片的初始值。
#define INIT_TASK \
{ 0,15,15,
// 上述三个值分别对应 state、counter 和 priority;
修改完后再次编译make all,进入模拟器后编译运行测试文件process.c,然后运行统计脚本stat_log.py查看结果,与之前的结果进行对比。
将时间片的初始值修改为5:
查看proces.log(如下图,这里仅能展示一部分),可以很明显的看出进程状态改变的频率比之前更加的频繁了
这里统计下进程4,12,13,14,15的数据(如下图),相比于时间片的初始值为15,可以很明显的看出每个进程触发CPU和I/O变得更加频繁,平均周转时间和平均等待时间也变得长了,每秒的吞吐量也变得少了(CPU的时间主要都用到了进程切换上)
将第三个值修改为30:
查看proces.log(如下图),可以很明显的看出进程状态的改变没那么的频繁了
这里统计下进程0,1,2,3的数据(如下图),相比于时间片的初始值为5,可以很明显的看出每个进程触发CPU和I/O次数变少,平均周转时间和平均等待时间也变短了,每秒的吞吐量也变得多了
此处总结借用http://t.csdn.cn/T2fet中的一段话:
进程之间的切换是需要时间的,如果时间片设定得太小的话,就会发生频繁的进程切换,因此会浪费大量时间在进程切换上,影响效率;如果时间片设定得足够大的话,就不会浪费时间在进程切换上,利用率会更高,但是用户交互性会受到影响。举一个很直观的例子:我在银行排队办业务,假设我要办的业务很简单只需要占用1分钟,如果每个人的时间片是30分钟,而我前面的每个人都要用满这30分钟,那我就要等上好几个小时!如果每个人的时间片是2分钟的话,我只需要等十几分钟就可以办理我的业务了,前面没办完的会在我之后轮流地继续去办。所以时间片不能过大或过小,要兼顾CPU利用率和用户交互性。
因此选择一个合适的时间片初始值非常重要!
**启发:**根据不同的情景,可以通过修改时间片初始值,观察平均周转时间、平均等待时间和每秒吞吐量的变化,以寻求最佳时间片初始值
实验五:基于内核栈切换的进程切换
实验内容:
现在的 Linux 0.11 采用 TSS(后面会有详细论述)和一条指令就能完成任务切换,虽然简单,但这指令的执行时间却很长,在实现任务切换时大概需要 200 多个时钟周期。
而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。
本次实践项目就是将 Linux 0.11 中采用的 TSS 切换部分去掉,取而代之的是基于堆栈的切换程序。具体的说,就是将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的代码。
在现在的 Linux 0.11 中,真正完成进程切换是依靠任务状态段(Task State Segment,简称 TSS)的切换来完成的。具体的说,在设计“Intel 架构”(即 x86 系统结构)时,每个任务(进程或线程)都对应一个独立的 TSS,TSS 就是内存中的一个结构体,里面包含了几乎所有的 CPU 寄存器的映像。有一个任务寄存器(Task Register,简称 TR)指向当前进程对应的 TSS 结构体,所谓的 TSS 切换就将 CPU 中几乎所有的寄存器都复制到 TR 指向的那个 TSS 结构体中保存起来,同时找到一个目标 TSS,即要切换到的下一个进程对应的 TSS,将其中存放的寄存器映像“扣在”CPU 上,就完成了执行现场的切换。
实验思路:
要实现基于内核栈的任务切换,主要完成如下三件工作:
(1)重写 switch_to
在新的 switch_to 中将用到当前进程的 PCB、目标进程的 PCB、当前进程的内核栈、目标进程的内核栈等信息。由于 Linux 0.11 进程的内核栈和该进程的 PCB 在同一页内存上(一块 4KB 大小的内存),其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址;另外,由于当前进程的 PCB 是用一个全局变量 current 指向的,所以只要告诉新 switch_to()函数一个指向目标进程 PCB 的指针就可以了。同时还要将 next 也传递进去,虽然 TSS(next)不再需要了,但是 LDT(next)仍然是需要的,也就是说,现在每个进程不用有自己的 TSS 了,因为已经不采用 TSS 进程切换了,但是每个进程需要有自己的 LDT,地址分离地址还是必须要有的,而进程切换必然要涉及到 LDT 的切换。
这个函数依次主要完成如下功能:由于是 C 语言调用汇编,所以需要首先在汇编中处理栈帧,即处理 ebp 寄存器;接下来要取出表示下一个进程 PCB 的参数,并和 current 做一个比较,如果等于 current,则什么也不用做;如果不等于 current,就开始进程切换,依次完成 PCB 的切换、TSS 中的内核栈指针的重写、内核栈的切换、LDT 的切换以及 PC 指针(即 CS:EIP)的切换。
(2)将重写的 switch_to 和 schedule() 函数接在一起
(3)修改现在的 fork()
现在需要将新建进程的用户栈、用户程序地址和其内核栈关联在一起,因为TSS没有做这样的关联,fork()要求让父子进程共享用户代码、用户数据和用户堆栈,虽然现在是使用内核栈完成任务的切换(基于堆栈的进程切换),但是fork()的基本含义不应该发生变化。
**代码的修改参考:**https://blog.csdn.net/leoabcd12/article/details/122268321
运行结果: