哈工大-操作系统-HitOSlab-李治军-实验3-进程运行的轨迹跟踪与统计

操作系统实验3:进程运行轨迹的跟踪与统计

实验内容请查看实验指导手册

绪论

开始实验之前,需要弄清楚实验具体要做什么:

  • 参考home/teacher目录下的process.c编写一个能够创建多个并行子进程的程序。
  • 在linux0.11系统的内核中,添加和修改程序,使得我们启动linux0.11并且运行process.c后,系统能够打印出一个监视进程运行的process.log文件

一、实验内容

1.编写process.c文件

process.c代码如下:

#include <stdio.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();
		/*如果fork 失败*/
		if(n_proc[i] < 0 )
		{
			printf("Failed to fork child process %d!\n",i+1);
			return -1;
		}
		else if(n_proc[i] == 0)/*子进程*/
		{
			cpuio_bound(20,2*i,20-2*i); /*每个子进程都占用20s*/
			exit(0); /*执行完cpuio_bound 以后,结束该子进程,下一次循环,该子进程不会fork出新的进程*/
		}
		/*一直都是那一个父进程在fork出子进程*/
	}
	/*打印所有子进程PID*/
	for(i=0;i<10;i++)
		printf("Child PID: %d\n",n_proc[i]);
	/*等待所有子进程完成;
	这里不是很完美,由于wait函数的特点,父进程只要有回收了一个子进程,就会结束自己*/
	wait(&i);  /*Linux 0.11 上 gcc要求必须有一个参数, gcc3.4+则不需要*/ 
	return 0;
}
/* cpuio_bound函数无需修改,直接用 */
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)
	{
		times(&start_time);
		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;
			
		sleep_time=0;
		while (sleep_time < io_time)
		{
			sleep(1);
			sleep_time++;
		}
		last -= sleep_time;
	}
}

process.c的保存路径为~/oslab/hdc/usr/root/
如果你对fork()函数和wait()函数的作用以及工作方式还不是很清楚的话,建议你花点时间,看一下相关文章,这应该会帮助你更好的理解process.c程序的功能作用

2.日志文件

2.1 修改main.c

为了让linux0.11在启动之后,就创建process.log并开始记录,需要将~/oslab/linux-0.11/init/main.c文件做如下修改:

//……
move_to_user_mode();

/***************添加开始***************/
setup((void *) &drive_info);

// 建立文件描述符0和/dev/tty0的关联
(void) open("/dev/tty0",O_RDWR,0);    

//文件描述符1也和/dev/tty0关联
(void) dup(0);        

// 文件描述符2也和/dev/tty0关联
(void) dup(0);        

(void) open("/var/process.log",O_CREAT|O_TRUNC|O_WRONLY,0666);

/***************添加结束***************/

if (!fork()) {        /* we count on this going ok */
    init();
}
//……
2.2 添加fprintk()函数

在内核状态下,write()功能失效,其原理等同于《系统调用》实验中不能在内核状态调用 printf(),只能调用printk()。编写可在内核调用的 write() 的难度较大,所以这里直接给出源码。它主要参考了 printk()sys_write()而写成的:因为和 printk() 的功能近似,建议将此函数放入到 ~/oslab/linux-0.11/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);
/* 如果输出到stdout或stderr,直接调用sys_write即可 */
    if (fd < 3)
    {
        __asm__("push %%fs\n\t"
            "push %%ds\n\t"
            "pop %%fs\n\t"
            "pushl %0\n\t"
        /* 注意对于Windows环境来说,是_logbuf,下同 */
            "pushl $logbuf\n\t" 
            "pushl %1\n\t"
        /* 注意对于Windows环境来说,是_sys_write,下同 */
            "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的描述符都与文件关联。事实上,还存在很多其它情况,这里并没有考虑。*/
    {
    /* 从进程0的文件描述符表中得到文件句柄 */
        if (!(file=task[0]->filp[fd]))    
            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()的使用方式类同与 C 标准库函数fprintf(),唯一的区别是第一个参数是文件描述符,而不是文件指针。例如:

// 向stdout打印正在运行的进程的ID
fprintk(1, "The ID of running process is %ld", current->pid); 

// 向log文件输出跟踪进程运行轨迹(我们要用的就是这个)
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'R', jiffies); 

2.3 寻找状态切换点

我们来理一下思路:

  • 首先,修改了初始化文件main.c,让系统一启动就会创建一个process.log,这个文件的路径在~/oslab/hdc/var/(需要挂载虚拟机才看得到哦);
  • 然后,我们往内核中添加了一个fprintk()函数,使得系统可以向process.log文件进行打印输出。
  • 因此,最后一步,我们需要去寻找系统内核代码的合适位置,插入fprintk(),打印出我们想要的进程状态。

对以下文件进行了修改:
/kernel/fork.c

	/* ... */
	p->start_time = jiffies;
	/*因为更新了启动时间,这里就是一个进程的新建*/
	fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'N', jiffies);
	
	/* ... */
	p->state = TASK_RUNNING; /* do this last, just in case */
	/*上述语句,将一个新建态的进程变为了就绪态的进程,向log文件输出*/
	fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'J', jiffies);

/kernel/sched.c

  • sleep_on()函数
void sleep_on(struct task_struct **p)
{
	struct task_struct *tmp;

	if (!p)
		return;
	if (current == &(init_task.task))
		panic("task[0] trying to sleep");
	tmp = *p;
	*p = current;
	current->state = TASK_UNINTERRUPTIBLE;
	/*sleep_on函数,让进程从运行到睡眠,也就是进入到堵塞(W)*/
	fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
	schedule();
	if (tmp)
	{
		tmp->state = 0;
		/*
		*原等待队列 第一个进程 => 唤醒(就绪)
		*/
		fprintk(3,"%d\t%c\t%d\n",tmp->pid,'J',jiffies);
	}
}
  • interruptible_sleep_on()函数
void interruptible_sleep_on(struct task_struct **p)
{
	struct task_struct *tmp;

	if (!p)
		return;
	if (current == &(init_task.task))
		panic("task[0] trying to sleep");
	tmp = *p;
	*p = current;
repeat:
	current->state = TASK_INTERRUPTIBLE;
	fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
	schedule();
	if (*p && *p != current)
	{
		(**p).state = 0;
		/*
		*当前进程进程 => 可中断睡眠 同上
		*/
		fprintk(3,"%d\t%c\t%d\n",(*p)->pid,'J',jiffies);
		goto repeat;
	}
	*p = NULL;
	if (tmp)
	{
		tmp->state = 0;
		/*
		*原等待队列 第一个进程 => 唤醒(就绪)
		*/
		fprintk(3,"%d\t%c\t%d\n",tmp->pid,'J',jiffies);
	}
}
  • 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;
	}
	/*
	编号为next的进程 运行
	很奇怪,这段代码,我在0.11内核中并没有看到,《linux0.11完全注释》上没有,
	但是其他人的代码片中都加上这段代码?
	*/
	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);
	}
	switch_to(next); /*switch_to是一个宏*/
}

  • wake_up()函数
void wake_up(struct task_struct **p)
{
	if (p && *p)
	{
		(**p).state = 0;
		/*睡眠到就绪态*/
		fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'J', jiffies);
		*p = NULL;
	}
}

/kernel/exit.c

  • do_exit()函数
int do_exit(long code)
{
	int i;
	free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
	free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
	for (i=0 ; i<NR_TASKS ; i++)
		if (task[i] && task[i]->father == current->pid) {
			task[i]->father = 1;
			if (task[i]->state == TASK_ZOMBIE)
				/* assumption task[1] is always init */
				(void) send_sig(SIGCHLD, task[1], 1);
		}
	for (i=0 ; i<NR_OPEN ; i++)
		if (current->filp[i])
			sys_close(i);
	iput(current->pwd);
	current->pwd=NULL;
	iput(current->root);
	current->root=NULL;
	iput(current->executable);
	current->executable=NULL;
	if (current->leader && current->tty >= 0)
		tty_table[current->tty].pgrp = 0;
	if (last_task_used_math == current)
		last_task_used_math = NULL;
	if (current->leader)
		kill_session();
	current->state = TASK_ZOMBIE;
	current->exit_code = code;
	/*TASK_ZOMBIE表示进程处于僵死状态,可以理解成进程被退出了吧*/
	fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'E', jiffies);
	tell_father(current->father);
	schedule();
	return (-1);	/* just to suppress warnings */
}
  • sys_waitpid()函数
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;
				/*可以看到,下一句是release释放进程,那么显然这里是进程的退出*/
				fprintk(3, "%ld\t%c\%ld\n", (*p)->pid, 'E', jiffies);
				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;
}

2.4 测试

使用以下命令,重新编译内核:

/*确保卸载虚拟机挂载*/
cd ~/oslab/
sudo umount hdc
/*重新编译内核*/
cd ~/oslab/linux-0.11/
make all

使用以下命令,进入bochs:

cd ~/oslab/
./run

进入bochs后,依次输入以下命令:

gcc -o process process.c
./process
/*等待process执行完成*/
sync
ll /var

我的实验结果如图所示:
在这里插入图片描述

退出bochs,在终端中输入如下命令,在宿主机ubuntu上查看log文件更加方便:

/*将./hdc/var/process.log文件复制到/oslab文件夹中*/
cp ./hdc/var/process.log ~/oslab

在这里插入图片描述

3.统计数据

这个实验的负责团队用python编写一个数据统计程序stat_log.py,它从log文件读入原始数据,然后计算平均周转时间、平均等待时间和吞吐率。

从github上将stat_log.py下载下来,保存在~/oslab/hdc/var/,在ubuntu终端里,依次输入以下命令:

cd ./hdc/var/
chmod +x stat_log.py
python stat_log.py process.log 7 8 9 10 11 -g

如果process.log文件数据内容不正确或者打印格式不正确的话,这个py程序将无法执行。
程序运行结果如下图所示:
在这里插入图片描述

4.修改时间片及测试

linux0.11采用的调度算法是一种综合考虑进程优先级并能动态反馈调整时间片的轮转调度算法。 那么什么是轮转调度算法呢?它为每个进程分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程;如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。

进程之间的切换是需要时间的,如果时间片设定得太小的话,就会发生频繁的进程切换,因此会浪费大量时间在进程切换上,影响效率;如果时间片设定得足够大的话,就不会浪费时间在进程切换上,利用率会更高,但是用户交互性会受到影响

时间片的初始值是进程0的priority,是在linux-0.11/include/linux/sched.h的宏 INIT_TASK中定义的,如下:我们只需要修改宏中的第三个值即可,该值即时间片的初始值。

#define INIT_TASK \
    { 0,15,15, 
// 上述三个值分别对应 state、counter 和 priority;

由于我的process.log数据存在问题,我没有做这个测试实验,下面贴一下别人的实验结果:

原始时间片为15,修改了两次时间片,分别为10和20,结果如下:
原始时间片:15
(Unit: tick)
Process   Turnaround   Waiting   CPU Burst   I/O Burst
      7         2247       142           0        2105
      8         2202      1686         200         315
      9         2246      1991         255           0
     10         2230      1975         255           0
     11         2215      1959         255           0
     12         2199      1944         255           0
     13         2183      1928         255           0
     14         2168      1912         255           0
     15         2152      1897         255           0
     16         2137      1881         255           0
Average:     2197.90   1731.50
Throughout: 0.45/s
**************************************************************
第一次修改时间片:10
(Unit: tick)
Process   Turnaround   Waiting   CPU Burst   I/O Burst
      7         2298        97           0        2200
      8         2319      1687         200         432
      9         2368      2098         270           0
     10         2358      2087         270           0
     11         2347      2076         270           0
     12         2336      2066         270           0
     13         2326      2055         270           0
     14         2315      2044         270           0
     15         2304      2034         270           0
     16         2292      2021         270           0
Average:     2326.30   1826.50
Throughout: 0.42/s
**************************************************************
第二次修改时间片:20
(Unit: tick)
Process   Turnaround   Waiting   CPU Burst   I/O Burst
      7         2587       187           0        2400
      8         2567      1766         200         600
      9         2608      2308         300           0
     10         2585      2285         300           0
     11         2565      2264         300           0
     12         2544      2244         300           0
     13         2523      2223         300           0
     14         2503      2202         300           0
     15         2482      2182         300           0
     16         2461      2161         300           0
Average:     2542.50   1982.20
Throughout: 0.38/s

结论:
时间片变小,进程因时间片到时产生的进程调度次数变多,该进程等待时间越长。
然而随着时间片增大,进程因中断或者睡眠进入的进程调度次数也增多,等待时间随之变长。
故而需要设置合理的时间片,既不能过大,也不能过小。

二、回答问题

  1. 结合自己的体会,谈谈从程序设计者的角度看,单进程编程和多进程编程最大的区别是什么?
    1. 单进程编程是一个进程从上到下顺序进行;多进程编程可以通过并发执行,即多个进程之间交替执行,如某一个进程正在I/O输入输出而不占用CPU时,可以让CPU去执行另外一个进程,这需要采取某种调度算法。
    2. 单进程编程的CPU利用率低,因为单进程在等待I/O时,CPU是空闲的;多进程编程的CPU利用率高,因为当某一进程等待I/O时,CPU会去执行另一个进程,因此CPU的利用率高。
  2. 你是如何修改时间片的?仅针对样本程序建立的进程,在修改时间片前后,log文件的统计结果(不包括Graphic)都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?
    1. 时间片变小,进程因时间片到时产生的进程调度次数变多,该进程等待时间越长。
    2. 然而随着时间片增大,进程因中断或者睡眠进入的进程调度次数也增多,等待时间随之变长。
    3. 故而需要设置合理的时间片,既不能过大,也不能过小。

参考文献:
1.Linux中wait()函数
2.linux中fork()函数详解
3.Linux中的stdout和stderr
4.linux文件描述符限制及使用详解
5.哈工大操作系统实验课——进程运行轨迹的跟踪与统计(lab 4)
6.(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验3-进程运行轨迹的跟踪与统计
7.蓝桥云课-操作系统原理与实践
8.哈工大操作系统实验手册
9.哈工大实验“官方”github仓库
10.MOOC哈工大操作系统实验3:进程运行轨迹的跟踪与统计

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值