实验3-进程运行轨迹的跟踪与统计

进程运行轨迹的跟踪与统计

实验内容

进程从创建(Linux下调用fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换, 如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态; 在运行的过程中如果启动了一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出CPU; 当文件读写完毕以后,操作系统会在将其切换成就绪态,等待进程调度算法来调度该进程执行……

本次实验包括如下内容:

  1. 基于模板“process.c”编写多进程的样本程序,实现如下功能:
    1. 所有子进程都并行运行,每个子进程的实际运行时间一般不超过30秒;
    2. 父进程向标准输出打印所有子进程的id,并在所有子进程都退出后才退出;
  2. 在Linux 0.11上实现进程运行轨迹的跟踪。基本任务是在内核中维护一个日志文件/var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一log文件中。

/var/process.log文件的格式必须为:

pid    X    time

其中:

  • pid是进程的ID;
  • X可以是N,J,R,W和E中的任意一个,分别表示进程新建(N)、进入就绪态(J)、进入运行态®、进入阻塞态(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
……

实验过程

编写样本程序

所谓样本程序,就是一个生成各种进程的程序。我们的对0.11的修改把系统对它们的调度情况都记录到log文件中。在修改调度算法或调度参数后再运行完全一样的样本程序,可以检验调度算法的优劣。理论上,此程序可以在任何Unix/Linux上运行,所以建议在Ubuntu上调试通过后,再拷贝到0.11下运行。

process.c是样本程序的模板。它主要实现了一个函数:

/*
 * 此函数按照参数占用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(),从而完成一个个性化的样本程序。

修改后的process.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
#include <sys/types.h>

#define HZ	100

void cpuio_bound(int last, int cpu_time, int io_time);
/*
1.  所有子进程都并行运行,每个子进程的实际运行时间一般不超过30秒;
2.  父进程向标准输出打印所有子进程的id,并在所有子进程都退出后才退出;
*/
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)
		{
			printf("Fork the Child Process : %d\n",i + 1);
			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*/
	printf("Print the pid of Child Process\n");
	for(i=0;i<10;i++)
		printf("Child PID: %d\n",n_proc[i]);
	/*等待所有子进程完成*/
	wait(&i);  /*Linux 0.11 上 gcc要求必须有一个参数, gcc3.4+则不需要*/ 
	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(&current_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 );
		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;
		printf("the proces last time : %d\n",last);
	}
}

log文件

在操作系统启动后要先打开/var/process.log,然后在每个进程发生状态切换的时候向log文件内写入一条记录,其过程和用户态的应用程序没什么两样。然而,因为内核状态的存在,使过程中的很多细节变得完全不一样。

打开log文件

init/main.c中,将函数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:stdin、1“stdout、2:stderr与中断之间的联系,内核随后利用这些描述符在终端上显示一些系统信息。为了用文件log追踪从开机到结束所有进程的轨迹,我们需要建立log文件与描述符3之间的联系,为了尽早访问log文件,我们需要将修改后的该段代码移动到函数main()中的语句move_to_user_mode();之后

函数main()中的部分代码

……
move_to_user_mode();
if (!fork()) {        /* we count on this going ok */
    init();
}
……

这段代码在进程0中运行,先切换到用户模式,全系统第一次调用fork()创建进程1,进程1调用函数init(),进行初始化

修改后的代码为:

……
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文件将被用来记录进程的状态转移轨迹,而所有的状态转移都是在内核中进行的,在内核状态下,write()功能失效,我们需要编写一个写函数,该函数可以在内核中往log文件中写入,由于难度较大,这里直接给出源码,放在kernel/printk.c

#include <linux/sched.h>
#include <sys/stat.h>

static char logbuf[1024];
int fprintk(int fd, const char *fmt, ...)
{
    va_list args;
    int count;
    struct file * file;
    struct m_inode * inode;

    va_start(args, fmt);
    count=vsprintf(logbuf, fmt, args);
    va_end(args);

    if (fd < 3)    /* 如果输出到stdout或stderr,直接调用sys_write即可 */
    {
        __asm__("push %%fs\n\t"
            "push %%ds\n\t"
            "pop %%fs\n\t"
            "pushl %0\n\t"
            "pushl $logbuf\n\t" /* 注意对于Windows环境来说,是_logbuf,下同 */
            "pushl %1\n\t"
            "call sys_write\n\t" /* 注意对于Windows环境来说,是_sys_write,下同 */
            "addl $8,%%esp\n\t"
            "popl %0\n\t"
            "pop %%fs"
            ::"r" (count),"r" (fd):"ax","cx","dx");
    }
    else    /* 假定>=3的描述符都与文件关联。事实上,还存在很多其它情况,这里并没有考虑。*/
    {
        if (!(file=task[0]->filp[fd]))    /* 从进程0的文件描述符表中得到文件句柄 */
            return 0;
        inode=file->f_inode;

        __asm__("push %%fs\n\t"
            "push %%ds\n\t"
            "pop %%fs\n\t"
            "pushl %0\n\t"
            "pushl $logbuf\n\t"
            "pushl %1\n\t"
            "pushl %2\n\t"
            "call file_write\n\t"
            "addl $12,%%esp\n\t"
            "popl %0\n\t"
            "pop %%fs"
            ::"r" (count),"r" (file),"r" (inode):"ax","cx","dx");
    }
    return count;
}

写函数fprintk()的使用格式:

fprintk(文件描述符,"写入内容",写入内容的格式化参数)
文件描述符:
    0 -> 代表stdin
    1 -> 代表stdout
    2 -> 代表stderr
    3 -> 代表log

例如

fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'R', jiffies); //向log文件写入一个进程的状态

跟踪进程运行轨迹

jiffies,滴答

jiffieskernel/sched.c文件中定义为一个全局变量:

long volatile jiffies=0;

记录了从开机到当前时间的时钟中断发生次数,这个数也称为”滴答数“,一个滴答数的实际时间为10ms

寻找状态切换点

要记录进程的状态轨迹,就要知道进程在哪段代码中会发生状态变化,所以只要找到进程状态发生变化的代码段,在其后使用fprintk()将状态写入log文件即可。由于操作系统的多文件多目录的复杂性,要找到这些代码点也是相当麻烦

在Linux 0.11中,进程的状态有:新建(N)、就绪(J)、运行®、阻塞(W)、退出(E),状态变化有:就绪 -> 运行、运行 -> 就绪、运行 -> 阻塞、阻塞 -> 就绪、新建、退出

进程状态发生变化的文件集中在:/kernel中的fork.cexit.csched.c

管理log文件

日志文件的管理与代码编写无关,有几个要点要注意:

  1. 每次关闭bochs前都要执行一下“sync”命令,它会刷新cache,确保文件确实写入了磁盘。
  2. 在0.11下,可以用“ls -l /var”或“ll /var”查看process.log是否建立,及它的属性和长度。
  3. 一定要实践《实验环境的搭建与使用》一章中关于文件交换的部分。最终肯定要把process.log文件拷贝到主机环境下处理。
  4. 在0.11下,可以用“vi /var/process.log”或“more /var/process.log”查看整个log文件。不过,还是拷贝到Ubuntu下看,会更舒服。
  5. 在0.11下,可以用“tail -n NUM /var/process.log”查看log文件的最后NUM行。

实验结果

process.log的部分记录:

1	N	48
1	J	48
0	J	48
1	R	48
2	N	48
2	J	49
1	W	49
2	R	49
3	N	63
3	J	64
2	J	64
3	R	64
3	W	68
2	R	68
2	E	73
1	J	73
1	R	73
4	N	74
4	J	74
1	W	74
4	R	74
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值