哈工大操作系统(李治军)实验课,实验四,进程运行轨迹的跟踪与统计

基于Ubuntu22.04下进行实验

一 实验内容:

1. 在Linux 0.11 下时间进程运行轨迹的跟踪,将操作系统启动到系统关机的所有进程运行轨迹都记录在一个
日志文件下
2. 修改进程调度的时间片,观察修改后系统进程运行情况的变化。

二 .编写主程序

我们需要编写一个程序process.c,这个程序可以创建多个进程,并为每个进程分配占用cpu的时间和占用io设备的时间。实验给出了样本程序,我们只需要添加即可。

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

#define HZ	100

void cpuio_bound(int last, int cpu_time, int io_time);

int main(int argc, char * argv[])
{
	//我们添加的部分
	pid_t num[10];
	int i;
	for(i=0;i<10;i++)
	{
		num[i]=fork();
		if(num[i]<0)
		{
			printf("Failed to fork child process %d\n",i+1);
		}
		else if(num[i]==0)
		{
			cpuio_bound(10,i,10-i);
			exit(0);
		}
	}
	for(i=0;i<10;i++)
	{
		printf("Child process PID: %d\n",num[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(&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;
	}
}

—— 其中fork()在创建时会返回两种状态,且会复制父进程的变量;对父进程返回的是子进程的pid,对子进程返回的是0;
—— 所以在process.c中,判断num[ i ]==0,说明此时是子进程,调用cpuio_bound(),在子进程执行完毕后,调用exit(0)来结束该进程。所以一直是进程1在fork()创建子进程,
——(只有进程0、1的文件描述符一定关联着log文件,保证子进程运行状态可以写入log文件中)

三 创建log文件

我们要尽可能早的开始记录进程轨迹,应当在内核启动时就打开log文件。

3.1 内核的入口

内核的入口在 /linux 0.11/init/main.c中的main()

int main()
{
	……
	move_to_user_mode()
	if(!fork())
	{
		init();
	}
}

在这里,0号进程调用fork()创建了1号进程,并调用init进行初始化。
在init中,建立了文件描述符0、1、2,分别是标准输入stdin,标准输出stdout和标准错误stderr。

……
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关联

我将这些代码放到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()) {       
    init();
}
……

文件描述符3与log文件关联,参数含义是:建立只写文件,若文件已经存在则清空已有的内容;0666中的0表示这是个八进制数,6代表二进制110,则当前用户对该文件可读可写不可执行,组内用户和其他用户也是相同的权限。

四 创建写log文件的函数

所有的状态转移都在内核中进行,而内核状态下,我们需要自己编写一个函数来写
log文件,参考printk()和sys_write().
#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" 
            "pushl %1\n\t"
            "call sys_write\n\t" 
            "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;
}

同时我们需要在kernel.h中定义fprintk().请添加图片描述
fprintk使用了不确定参数的函数定义,在使用时,第一个参数写文件描述符,第二个参数写后面输入参数的类型,然后第三第四……参数写要传入的值就可以了。
——例如:

fprintk(1, "The ID of running process is %ld", current->pid); 
//向stdout打印正在运行的进程的ID
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'R', jiffies); 
//向log文件输出

此时我们已经完成了一半了。现在我们需要在系统进行进程状态切换的地方,调用fprintk()来将状态转移的信息记录下来。

五 寻找切换点

进程有新建态(N),就绪态(J),运行态(R),阻塞态(W),退出态(E)
——新建态在fork()新建一个进程时出现,新建到就绪也在fork.c中

——就绪态到运行态依靠schedule()

——运行态到阻塞态,依靠sleep_on()、interrupt_sleep_on()以及sys_pause()

——阻塞到就绪依靠wake_up()

——退出态在exit.c中的do_exit()

下面的c文件都在linunx-0.11/kernel下

5.1 fork.c

在fork中,创建进程主要的函数是copy_process()

 ……
p->start_time = jiffies;
fprintk(3,"%d\t%c\t%d\n",p->pid,'N',jiffies);
//在更新了启动时间后,进程新建完成
//……
p->state = TASK_RUNNING;
fprintk(3,"%d\t%c\t%d\n",p->pid,'J',jiffies)
//切换状态为就绪态
 ……

5.2 sched.c

sleep_on()
在这里插入图片描述
sleep_on将当前进程设置为睡眠态,将等待队列中的第一个进程唤醒

interrupt_sleep_on()
在这里插入图片描述
功能和sleep_on()相似

schedule()

void schedule(void)
{
	int i, next, c;
	struct task_struct **p;

	/* check alarm, wake up any interruptible tasks that have got a signal */

	for (p = &LAST_TASK; p > &FIRST_TASK; --p)
		if (*p)
		{
			if ((*p)->alarm && (*p)->alarm < jiffies)
			{
				(*p)->signal |= (1 << (SIGALRM - 1));
				(*p)->alarm = 0;
			}
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
				(*p)->state == TASK_INTERRUPTIBLE)
			{
				(*p)->state = TASK_RUNNING;
				/*从可中断睡眠态转换为就绪态*/
				fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);
			}
		}

	/* this is the scheduler proper: */

	while (1)
	{
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i)
		{
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
		if (c)
			break;
		for (p = &LAST_TASK; p > &FIRST_TASK; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
								(*p)->priority;
	}
	switch_to(next); 
}

在linux 0.11 中,就绪态和运行态在源码中都是TASK_RUNNING,所以我们需要在就绪态到运行态转变的schedule中,添加一段代码来清楚记录这个状态的转变

if(current->pid != task[next] ->pid)
	{
		if(current->state == TASK_RUNNING)
			fprintk(3,"%d\t%c\t%d\n",current->pid,'J',jiffies);
		fprintk(3,"%d\t%c\t%d\n",task[next]->pid,'R',jiffies);
	}
	//在swich_to(next)前添加
这段代码判断下一个要执行的程序是否是当前程序,若不是,则将当前程序设置为就绪态,而将下一个要
执行的程序设置为运行态。

wake_up()请添加图片描述
调用wake_up()主动从阻塞态转变为就绪态。

sys_pause()
在这里插入图片描述
进程0会不断调用sys_pause()来进行调用,所以我们将进程0排除在外。其他进程调用sys_pause()时,记录其状态变化为阻塞态W

5.3 exit.c

do_exit()
在这里插入图片描述
在将进程的状态设置为僵死后,可以视为退出态

sys_waitpif()

int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
	int flag, code;
	struct task_struct ** p;

	verify_area(stat_addr,4);
repeat:
	flag=0;
	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p || *p == current)
			continue;
		if ((*p)->father != current->pid)
			continue;
		if (pid>0) {
			if ((*p)->pid != pid)
				continue;
		} else if (!pid) {
			if ((*p)->pgrp != current->pgrp)
				continue;
		} else if (pid != -1) {
			if ((*p)->pgrp != -pid)
				continue;
		}
		switch ((*p)->state) {
			case TASK_STOPPED:
				if (!(options & WUNTRACED))
					continue;
				put_fs_long(0x7f,stat_addr);
				return (*p)->pid;
			case TASK_ZOMBIE:
				current->cutime += (*p)->utime;
				current->cstime += (*p)->stime;
				flag = (*p)->pid;
				code = (*p)->exit_code;
				/*这一部分代码检索了队列中进程的状态,若为僵死,则设置其释放,我们在do_exit已经
				标注进程为退出态了,所以这里不重复写进log文件了*/
				release(*p);
				put_fs_long(code,stat_addr);
				return flag;
			default:
				flag=1;
				continue;
		}
	}
	if (flag) {
		if (options & WNOHANG)
			return 0;
		current->state=TASK_INTERRUPTIBLE;
		fprintk(3,"%ld\t%c\t%ld\n",current->pid,'W',jiffies);
		//设置当前进程为阻塞态
		schedule();
		if (!(current->signal &= ~(1<<(SIGCHLD-1))))
			goto repeat;
		else
			return -EINTR;
	}
	return -ECHILD;
}

六 测试

6.1 运行系统

在oslab目录下卸载虚拟机挂载后,重新编译内核

cd ~/oslab/linux-0.11
make clean
make all

进入bochs

cd ~/oslab
./run

编译process.c

gcc -o process process.c
//运行
./process
//保存到文件中
sync

process.log 结果如图
在这里插入图片描述

6.2 分析数据

实验提供了一个数据统计程序 stat_log_py,可以对该实验产生的log文件进行数据分析
stat_log_py

该程序使用python2写成,ubuntu 22.04为python3,所以需要下载并使用python2来运行该程序

//将stat_log_py设置为可执行程序
chmod +x stat_log.py
//这里我的python2的版本为python2.7
python2.7 stat_log.py process.log 13 14 15 16

运行结果如图请添加图片描述
若发生报错,一般是log文件记录的数据有问题,可以排查在各个地方加入的fprink()是否完整和正确

七 修改时间片

时间片小->(导致)切换次数多->系统内耗大(切换需要时间)->吞吐量就小

7.1 INIT_TASK

时间片的初始值是进程0的priority,在linux-0.11/include/linux/sched.h中的宏INIT_TASK中定义
请添加图片描述
修改state etc即可,修改时间片需要修改第三个值,默认为15,这里修改为10。

7.2 再次运行程序

结果如下图
请添加图片描述
可以看到进程的等待时间明显变短

实验到此结束

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值