操作系统实验03-进程运行轨迹的跟踪与统计 实验内容 步骤 问题回答

  • 基于模板 process.c 编写多进程的样本程序,实现如下功能: + 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒; + 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出。
  • 在 Linux0.11 上实现进程运行轨迹的跟踪。 + 基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。
  • 在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序,也可以使用 python 脚本程序—— stat_log.py(在 /home/teacher/ 目录下) ——进行统计。
  • 修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。
  • 结合自己的体会,谈谈从程序设计者的角度看,单进程编程和多进程编程最大的区别是什么?
  • 你是如何修改时间片的?仅针对样本程序建立的进程,在修改时间片前后,log 文件的统计结果(不包括 Graphic)都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?

步骤

1.修改process.c文件

实验楼在teacher文件夹内提供了process.c文件的模板,另外哈工大git上也有这个文件,在对其进行修改的过程中主要是在main函数内增加一些语句,用 fork() 建立若干个同时运行的子进程,父进程等待所有子进程退出后才退出,每个子进程各自执行 cpuio_bound(),从而实现样本程序。下面贴出process.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!
",i+1);
			return -1;
		}
		/*父进程继续fork*/
	}
	/*打印所有子进程PID*/
	for(i=0;i<10;i++)
		printf("Child PID: %d
",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;
	}
}

2.修改main.c文件

修改main.c的作用是使得操作系统在启动时就打开log文件,main.c文件在init目录下

move_to_user_mode();
	/***************自定义代码块--开始***************/
	setup((void *) &drive_info);
	(void) open("/dev/tty0",O_RDWR,0);    
	(void) dup(0);        
	(void) dup(0);        
	(void) open("/var/process.log",O_CREAT|O_TRUNC|O_WRONLY,0666);
	/***************自定义代码块--结束***************/
	if (!fork()) {		/* we count on this going ok */
		init();
	}

3.修改printk.c文件

系统在内核状态下只能使用printk函数,下面对printk增加了fprintk函数:(文件位置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
	"
            "push %%ds
	"
            "pop %%fs
	"
            "pushl %0
	"
        /* 注意对于Windows环境来说,是_logbuf,下同 */
            "pushl $logbuf
	" 
            "pushl %1
	"
        /* 注意对于Windows环境来说,是_sys_write,下同 */
            "call sys_write
	" 
            "addl $8,%%esp
	"
            "popl %0
	"
            "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
	"
            "push %%ds
	"
            "pop %%fs
	"
            "pushl %0
	"
            "pushl $logbuf
	"
            "pushl %1
	"
            "pushl %2
	"
            "call file_write
	"
            "addl $12,%%esp
	"
            "popl %0
	"
            "pop %%fs"
            ::"r" (count),"r" (file),"r" (inode):"ax","cx","dx");
    }
    return count;
}

4.修改fork.c文件

fork.c文件在kernel目录下,下面做出两处修改:

int copy_process(int nr,……)
{
    struct task_struct *p;
//    ……
// 获得一个 task_struct 结构体空间
    p = (struct task_struct *) get_free_page();  
//    ……
    p->pid = last_pid;
//    ……
// 设置 start_time 为 jiffies
    p->start_time = jiffies; 
      //新增修改
	fprintk(3,"%d	N	%d
",p->pid,jiffies);   
//       ……
/* 设置进程状态为就绪。所有就绪进程的状态都是
   TASK_RUNNING(0),被全局变量 current 指向的
   是正在运行的进程。*/
    p->state = TASK_RUNNING;    
	//新增修改
	fprintk(3,"%d	J	%d
",p->pid,jiffies);
    return last_pid;
}

5.修改sched.c文件

文件位置:kernel/sched.c,下面做出两处修改:

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;
	//    ……
	
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
			(*p)->state==TASK_INTERRUPTIBLE)
				{
					(*p)->state=TASK_RUNNING;
					/*新建修改--可中断睡眠 => 就绪*/
					fprintk(3,"%d	J	%d
",(*p)->pid,jiffies);
				}
	//    ……	
	/*编号为next的进程 运行*/	
	if(current->pid != task[next] ->pid)
	{
		/*新建修改--时间片到时程序 => 就绪*/
		if(current->state == TASK_RUNNING)
			fprintk(3,"%d	J	%d
",current->pid,jiffies);
		fprintk(3,"%d	R	%d
",task[next]->pid,jiffies);
	}
	switch_to(next);
}

1.修改sys_pause函数

int sys_pause(void)
{
	current->state = TASK_INTERRUPTIBLE;
	/*
	*修改--当前进程  运行 => 可中断睡眠
	*/
	if(current->pid != 0)
		fprintk(3,"%d	W	%d
",current->pid,jiffies);
	schedule();
	return 0;
}

2.修改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;
	/*
	*修改--当前进程进程 => 不可中断睡眠
	*/
	fprintk(3,"%d	W	%d
",current->pid,jiffies);
	schedule();
	if (tmp)
	{
		tmp->state=0;
		/*
		*修改--原等待队列 第一个进程 => 唤醒(就绪)
		*/
		fprintk(3,"%d	J	%d
",tmp->pid,jiffies);
	}
}

3.修改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;
	/*
	*修改--唤醒队列中间进程,过程中使用Wait
	*/
	fprintk(3,"%d	W	%d
",current->pid,jiffies);
	schedule();
	if (*p && *p != current) {
		(**p).state=0;
		/*
		*修改--当前进程进程 => 可中断睡眠
		*/
		fprintk(3,"%d	J	%d
",(*p)->pid,jiffies);
		goto repeat;
	}
	*p=NULL;
	if (tmp)
	{
		tmp->state=0;
		/*
		*修改--原等待队列 第一个进程 => 唤醒(就绪)
		*/
		fprintk(3,"%d	J	%d
",tmp->pid,jiffies);
	}

}

4.修改wake_up函数

void wake_up(struct task_struct **p)
{
	if (p && *p) {
		(**p).state=0;
		/*
		*修改--唤醒 最后进入等待序列的 进程
		*/
		fprintk(3,"%d	J	%d
",(*p)->pid,jiffies);
		*p=NULL;
	}
}

6.修改exit.c文件

此文件的位置在kernel目录下,修改了两处位置,如下:

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));
	//    ……
	
	current->state = TASK_ZOMBIE;
	/*
	*修改--退出一个进程
	*/
	fprintk(3,"%d	E	%d
",current->pid,jiffies);
	current->exit_code = code;
	tell_father(current->father);
	schedule();
	return (-1);	/* just to suppress warnings */
}
//    ……
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
	int flag, code;
	struct task_struct ** p;
//    ……
//    ……
	if (flag) {
		if (options & WNOHANG)
			return 0;
		current->state=TASK_INTERRUPTIBLE;
		/*
		*修改--当前进程 => 等待
		*/
		fprintk(3,"%d	W	%d
",current->pid,jiffies);
		schedule();
		if (!(current->signal &= ~(1<<(SIGCHLD-1))))
			goto repeat;
		else
			return -EINTR;
	}
	return -ECHILD;
}

7.make

上述步骤中已经修改了所有的必要文件,直接执行make命令编译内核即可

8.编译运行process.c

将process.c拷贝到linux0.11系统中,这个过程需要挂载一下系统硬盘,挂载拷贝成功之后再卸载硬盘,然后启动模拟器进入系统内编译一下process.c文件,过程命令及截图如下:

sudo ./mount-hdc 
cp ./process.c ./hdc/usr/root/
sudo umonut hdc
./run
gcc -o process process.c

编译process.c的过程如下:

操作系统实验03-进程运行轨迹的跟踪与统计
实验内容
步骤
问题回答

使用./process即可运行目标文件,运行后会生成log文件,生成log文件后将其拷贝到oslab根目录,命令如下:

sudo ./mount-hdc 
cp ./hdc/var/process.log ./
sudo umonut hdc

9.process.log自动化分析

由于默认的python脚本是使用的python2环境,我在Ubuntu上安装的是python3环境,所以对python脚本大概修改了下,直接把print命令更改下,然后有一处的异常处理将逗号更改为as即可,截图如下:

操作系统实验03-进程运行轨迹的跟踪与统计
实验内容
步骤
问题回答

修改了python脚本并确定可以执行之后,使用如下命令执行自动化分析:

./stat_log.py process.log 0 1 2 3 4 5 -g

分析结果如下:

操作系统实验03-进程运行轨迹的跟踪与统计
实验内容
步骤
问题回答

10.修改时间片

通过分析实验楼给出的schedule调度函数可以知道0.11 的调度算法是选取 counter 值最大的就绪进程进行调度。函数代码如下:

while (1) {
    c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS];

// 找到 counter 值最大的就绪态进程
    while (--i) {
        if (!*--p)    continue;
        if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
            c = (*p)->counter, next = i;
    }

// 如果有 counter 值大于 0 的就绪态进程,则退出
    if (c) break;

// 如果没有:
// 所有进程的 counter 值除以 2 衰减后再和 priority 值相加,
// 产生新的时间片
    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
          if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
}

// 切换到 next 进程
switch_to(next);

找到时间片的定义:

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

据此注释可以修改时间片

1.时间片为10

重复process.log自动化分析步骤得出如下结果:

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.41/s

2.时间片为15

重复process.log自动化分析步骤得出如下结果:

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.44/s

3.时间片为20

重复process.log自动化分析步骤得出如下结果:

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

问题回答

问题一

单进程编程较于多进程编程要更简单,利用率低,因为单进程是顺序执行的,而多进程编程是同步执行的,需要复杂且灵活的调度算法,充分利用CPU资源,所以情况要复杂得多。在设计多进程编程时,要考虑资源的分配,时间片的分配等达到系统调度的平衡。要综合考虑所有进程的情况以达到最优的并行执行效果。且多进程编程的功能更为强大,且应用范围较于单进程编程更加广泛。

问题二

  • 将时间片变小,进程调度次数变多,系统会使得该进程等待时间变长。
  • 将时间片增大,进程因中断/睡眠而产生的调度次数也增多,等待时间也会变长。
  • 总结:时间片要设置合理,不能过大或者过小。