哈工大操作系统实验--实验四:进程运行轨迹的跟踪与统计,实验记录及实验报告

实验四:进程运行轨迹的跟踪与统计

记录一些学习哈工大操作系统实验的学习笔记和心得


实验内容

  • 基于模板 process.c 编写多进程的样本程序,实现如下功能:
    • 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒;
    • 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;
  • 在 Linux0.11 上实现进程运行轨迹的跟踪
    • 基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中
  • 在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量
  • 修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异

4.1 编写样本程序

从实验材料中我们可以发现,process.c模板程序已经提供了cupio_bound函数来模拟各个子程序的运行时间,包括CPU时间、IO时间等

因此,需要实现的就是在process.c中的main函数使用forkwait系统调用来创建多个子程序,然后每个子程序执行各自的cpuio_bound函数来模拟实际情况,父进程等待所有子进程之后再向stdout来输出所有的子进程id.

这里给出两版代码,一版是广义情况下调用库函数的代码,另外一版是调用系统调用的一版

process.c :

#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define HZ 100

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

/*
    + 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒;
    + 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;
    fork();
    wait();
*/
int main(int argc, char *argv[])
{
    /*

    */
    int pid_list[5];                // pid list
    int pid_index = 0;              // pid index/number
    int fork_numer = atoi(argv[1]); // 参数1, 参数0为./process
    srand(time(NULL));              // init time to get random.
    if (argc < 2)
    {
        printf("argment count is to less!!!\n");
        return 0;
    }

    for (int i = 0; i < fork_numer; i++)
    {
        int pid = fork();
        if (pid == -1)
            printf("create fork errr!\n");
        if (!pid)
        {
            // int total_time = rand() % 10 + 1;
            int total_time = getpid() % 29 + 1; // time <= 30
            printf("I'am %d children fork. total time is %d\n", getpid(), total_time);
            cpuio_bound(total_time, i, total_time - i); // total_time cpu_time io_time
            exit(EXIT_SUCCESS);
        }
        else
        {
            pid_list[pid_index++] = pid;
        }
    }

    /*
        wait all children fork exit;
        wait() 只会等待任意一个子进程退出,因此需要for循环来等待子进程结束
    */
    for (int i = 0; i < fork_numer; i++)
    {
        int pid = wait(NULL);
        printf("I'm father process and I'm end wait, children_pid = %d\n", pid);
    }
    // output all children fork pid;
    for (int i = 0; i < pid_index; i++)
        printf("process %d pid is %d.\n", i, pid_list[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;
    }
}

process2.c

#include <stdio.h>
#include <unistd.h>  
int main(int argc, char * argv[])
{
    int id = fork();
    if(!id) {
        printf("id == %d\n", id);
        printf("I am child process, my id is [%d] and my parent process is [%d].\n", getpid(), getppid());
    }
	return 0;
}

log 文件

观察init/main.c代码可以发现,main函数在执行完所有的初始化后使用fork()来创建进程1,并在进程1中执行init函数,而原本的父进程0则是不断执行pause()系统调用

init函数中,进程首先会初始化文件描述符,并分别将0,1,2绑定至stdin stdout stderror,因此我们可以在这里将文件描述符3绑定至我们的log文件

void init(void)
{
	int pid, i;
setup((void *)&drive_info);
	(void)open("/dev/tty0", O_RDWR, 0);									// stdin  0
	(void)dup(0);														// stdout  1
	(void)dup(0);														// stderror  2
	(void)open("/var/process.log", O_CREAT | O_TRUNC | O_WRONLY, 0666); // log  3
  // ....
}

接下来为了让log文件开启的时间提前,记录所有进程的状态,我们将上述代码移动至内核开启代码后,即move_to_user之后

  move_to_user_mode();
	// 加载文件系统
	setup((void *)&drive_info);
	(void)open("/dev/tty0", O_RDWR, 0);									// stdin  0
	(void)dup(0);														// stdout  1
	(void)dup(0);														// stderror  2
	(void)open("/var/process.log", O_CREAT | O_TRUNC | O_WRONLY, 0666); // log  3
	if (!fork())
	{ /* we count on this going ok */
		init();
	}

至此,log文件就已经开启成功

写log文件

实验楼环境为我们提供了写log文件的函数,直接将它复制到kernel/printk.c中即可,以后我们使用fprink()函数并加上对应的文件描述符即可实现对log文件日志的写入

#include <stdarg.h>
#include <stddef.h>

#include <linux/kernel.h>

#include "linux/sched.h"
#include "sys/stat.h"

static char buf[1024];
static char logbuf[1024];

extern int
vsprintf(char *buf, const char *fmt, va_list args);

int printk(const char *fmt, ...)
{
	va_list args;
	int i;

	va_start(args, fmt);
	i = vsprintf(buf, fmt, args);
	va_end(args);
	__asm__("push %%fs\n\t"
			"push %%ds\n\t"
			"pop %%fs\n\t"
			"pushl %0\n\t"
			"pushl $buf\n\t"
			"pushl $0\n\t"
			"call tty_write\n\t"
			"addl $8,%%esp\n\t"
			"popl %0\n\t"
			"pop %%fs" ::"r"(i) : "ax", "cx", "dx");
	return i;
}

// write()实现
// 写入Log
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;
}
/*
// 向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);
*/

jiffies

jiffies是系统滴答数,它代表了系统从开机到现在为止经过的滴答数,在sched_init()中我们也可以发现时钟处理函数被初始化为time_interuupt,其中每次执行该函数都会让jiffies的值加一

下面这部分代码用于设置每次时钟中断的间隔LATCH

// 设置8253模式
outb_p(0x36, 0x43);
outb_p(LATCH&0xff, 0x40);
outb_p(LATCH>>8, 0x40);

linux0.11环境下的jiffies10ms

寻找状态切换点

为了在合适的地方记录状态的变化,并将其写入日志之中,我们需要考虑一下几种情况

  • 新建 --> 就绪
  • 就绪 --> 运行
  • 运行 --> 就绪
  • 运行 --> 睡眠(可中断,不可中断)
  • 睡眠 --> 就绪

了解到可能存在的状态变化之后,我们只需要在相对应的代码位置进行记录即可,主要修改的函数包括:

  • 就绪到运行:schedule()
  • 运行到睡眠:sleep_on()interruptible_sleep_on()
  • 进程主动睡眠:sys_pause()sys_waitpid()
  • 睡眠到就绪:wake_up()

这里给出一部分参考代码,具体的可以查看dev3分支

/*
  schedule()部分
*/
	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) // 找到counter最大的进程并且是就绪态
			break;
		for (p = &LAST_TASK; p > &FIRST_TASK; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
								(*p)->priority;
	}
	// switch_to 切换进程,但是由于这里是通过汇编进行切换,因此需要提前记录
	if (task[next]->pid != current->pid)
	{
		if (current->state == TASK_RUNNING)
			fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'J', jiffies);
		fprintk(3, "%ld\t%c\t%ld\n", task[next]->pid, 'R', jiffies);
	}
	switch_to(next);

在上面我们需要关注的点是,每次记录之前需要使用if()进行判断,是否状态真的发生了改变,避免重复的记录

其中进程0即父进程在系统无事可做的时候,会不断调用pause()系统调用,以激活调度算法,此时它可以是等待态,也可以是运行态,因为它是唯一一个在CPU上运行的程序

管理log文件

  • 每次退出bochs之前记得使用sync刷新缓存
  • 可以使用mount来挂载文件,将process.log文件拷贝到主机环境,方便处理阅读

数据统计

这里给出进行测试的步骤

  • 将实验楼提供的测试代码拷贝至主机环境
  • 使用挂载命令,将前面的process.c程序上传到linux0.11 root/
  • linux0.11中编译运行process.c程序
  • 使用挂载命令,将linux0.11中的process.log拷贝至主机环境
  • 使用stat_log.pyprocess.log进行测试
chmod +x ./stat_log.py
./stat_log.py process.log 1 2 3 4  # 只统计pid为1 2 3 4的进程
./stat_log.py process.log # 统计所有进程

修改时间片

根据实验指导,我们就可以发现,nice系统调用不会执行,只有scdule.h中的INIT_TASK宏会修改state counter priority,因此我们直接在这里修改即可完成对时间片的修改

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

当就绪进程counter为0的时候,会被更新成初始priority的值

实验报告

  • 结合自己的体会,谈谈从程序设计者的角度看,单进程编程和多进程编程最大的区别是什么?

    • 单进程编程无需考虑系统资源的调用部分,独占CPU即可
    • 多进程编程需要考虑各个进程之间的资源调度、运行优先级、运行时间分配等问题
  • 你是如何修改时间片的?仅针对样本程序建立的进程,在修改时间片前后,log 文件的统计结果(不包括 Graphic)都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?

    • 修改时间片前文已经给出了
    • 作者偷了个懒,没有比较,等待以后补充…
    • 据作者猜测应该没什么变化,因为运行优先级等都是确定过的,只影响了运行时间
  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
三、实验内容与要求 1、熟悉windows的编程接口,使用系统调用编程实现将参数1对应文件1.txt和参数2对应文件2.txt的内容合并到参数3对应文件zong.txt中(上传文件名为学号后5位ex0701.c)。 2、使用windows提供的命令将文件1.txt和文件2.txt的内容合并到文件total.txt中 (请将实现的操作命令写入下题批处理文件的第一行)。 3、主管助理小张经常接收公司员工发来的文件,开始为了节省时间,小张将下载的文件都保存在文件夹xiazai中(文件名如图1所示,下载后直接解压即可),这样不便于后期的统计和分类管理,现在领导要求必须为所有员工(90人)每人单独建立一个文件夹(以员工工号命名10201、10202......10290),然后将他们提交的文件分别剪切到各自对应的文件夹中(如图2所示)。于是小张开始为7名员工建立文件夹,再一个一个的去做……同学们想想有没有一种方法能快速完成所要求的操作呢? 请熟悉windows的命令接口,使用windows提供的常用命令copy、md、del等编写一个批处理文件(上传文件名为学号后5位ex0703.bat),实现所要求的功能: 1、启动linux系统或通过windows telnet到linux。 2、用huas用户名和密码123456登入系统中。 3、打开一终端窗口(在linux桌面上单击右键,选择从终端打开)。然后在其中输入以下命令实验。 4、熟悉常用操作命令. 5、编辑如下源代码(实验教材P86 1.进程的创建)并保存 二、实验目的 (1)加深对进程概念的理解,明确进程和程序的区别。 (2)分析进程竞争资源现象,学习解决进程互斥的方法。 (3了解Linux系统中进程通信的基本原理。 三、实验内容与要求 (1)任务一:编写一段程序,使其实现进程的软中断通信。 要求:使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕捉键盘上来的中断信号(即按DEL键);当捕捉到中断信号后,父进程用系统调用Kill()向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止: Child Processll is Killed by Parent! Child Processl2 is Killed by Parent! 父进程等待两个子进程终止后,输出如下的信息后终止 Parent Process is Killed! (2)任务二:在上面的程序中增加语句signal (SIGNAL, SIG-IGN)和signal (SIGQUIT, SIG-IGN),观察执行结果,并分析原因。 (3)任务三:进程的管道通信 编制一段程序,实现进程的管道通信。 使用系统调用pipe()建立一条管道线;两个子进程P1和P2分别向管道中写一句话: Child 1 is sending a message! Child 2 is sending a message! 而父进程则从管道中读出来自于两个子进程的信息,显示在屏幕上。 要求父进程先接收子进程P1发来的消息,然后再接收子进程P2发来的消息。 二、实验目的 自行编制模拟程序,通过形象化的状态显示,加深理解进程的概念、进程之间的状态转换及其所带来的PCB内容 、组织的变化,理解进程与其PCB间的一一对应关系。 三、实验内容与要求 1)设计并实现一个模拟进程状态转换及其相应PCB内容、组织结构变化的程序。 2)独立编写、调试程序。进程的数目、进程的状态模型(三状态、五状态、七状态或其它)以及PCB的组织形式可自行选择。 3)合理设计与进程PCB相对应的数据结构。PCB的内容要涵盖进程的基本信息、控制信息、资源需求及现场信息。 4)设计出可视性较好的界面,应能反映出进程状态的变化引起的对应PCB内容、组织结构的变化。 二、实验目的 存储管理的主要功能之一是合理地分配空间。请求页式管理是一种常用的虚拟存储管理技术。本实验的目的是通过请求页式管理中页面置换算法模拟设计,了解虚拟存储技术的特点,掌握请求页式存储管理的页面置换算法。 三、实验内容与要求 通过计算不同算法的命中率比较算法的优劣。同时也考虑了用户内存容量对命中率的影响。页面失效次数为每次访问相应指令时,该指令所对应的页不在内存中的次数。 计算并输出下属算法在不同内存容量下的命中率。  先进先出的算法(FIFO); 最近最少使用算法(LRU) 二、实验目的 死锁会引起计算机工作僵死,因此操作系统中必须防止。本实验的目的在于使用高级语言编写和调试一个系统动态分配资源的简单模拟程序,了解死锁产生的条件和原因,并采用银行家算法有效地防止死锁的发生,以加深对课堂上所讲授的知识的理解。 三、实验内容与要求 设计有n个进程共享m个系统资源的系统,进程可动态的申请和释放资源,系统按各进程的申请动态的分配资源。 系统能显示各个进程申请和释放资源,以及系统动态分配资源的过程,便于用户观察和分析。 四、算法描述(含数据结构定义)或流程图 (一) 数据结构 1. 可利用资源向量Available ,它是一个含有m个元素的数组,其中的每一个元素代表一类可利用的资源的数目,其初始值是系统中所配置的该类全部可用资源数目。其数值随该类资源的分配和回收而动态地改变。如果Available(j)=k,标是系统中现有Rj类资源k个。 2. 最大需求矩阵Max,这是一个n×m的矩阵,它定义了系统中n个进程中的每一个进程对m类资源的最大需求。如果Max(i,j)=k,表示进程i需要Rj类资源的最大数目为k。 3. 分配矩阵Allocation,这是一个n×m的矩阵,它定义了系统中的每类资源当前一分配到每一个进程的资源数。如果Allocation(i,j)=k,表示进程i当前已经分到Rj类资源的数目为k。Allocation i表示进程i的分配向量,有矩阵Allocation的第i行构成。 4. 需求矩阵Need,这是一个n×m的矩阵,用以表示每个进程还需要的各类资源的数目。如果Need(i,j)=k,表示进程i还需要Rj类资源k个,才能完成其任务。Need i表示进程i的需求向量,由矩阵Need的第i行构成。 上述三个矩阵间存在关系:Need(i,j)=Max(i,j)-Allocation(i,j)。 (二) 银行家算法 Request i 是进程Pi 的请求向量。Request i (j)=k表示进程Pi请求分配Rj类资源k个。当Pi发出资源请求后,系统按下述步骤进行检查: 1. 如果Request i ≤Need,则转向步骤2;否则,认为出错,因为它所请求的资源数已超过它当前的最大需求量。 2. 如果Request i ≤Available,则转向步骤3;否则,表示系统中尚无足够的资源满足Pi的申请,Pi必须等待。 3. 系统试探性地把资源分配给进程Pi,并修改下面数据结构中的数值: 二、实验目的 磁盘是高速、大容量、旋转型、可直接存取的存储设备。它作为计算机系统的辅助存储器,担负着繁重的输入输出工作,在现代计算机系统中往往同时会有若干个要求访问磁盘的输入输出要求。系统可采用一种策略,尽可能按最佳次序执行访问磁盘的请求。由于磁盘访问时间主要受寻道时间T的影响,为此需要采用合适的寻道算法,以降低寻道时间。本实验要求模拟设计一个磁盘调度程序,观察调度程序的动态运行过程。通过实验来理解和掌握磁盘调度的职能。 三、实验内容与要求 分别模拟如下磁盘调度算法,对磁盘进行移臂操作:  先来先服务算法  最短寻道优先算法 1. 假设磁盘只有一个盘面,并且磁盘是可移动头磁盘。 2. 磁盘是可供多个进程共享的存储设备,但一个磁盘每个时刻只能为一个进程服务。当有进程在访问某个磁盘时,其它想访问该磁盘的进程必须等待,直到磁盘一次工作结束。当有多个进程提出输入输出请求而处于等待状态时,可用磁盘调度算法从若干个等待访问者中选择一个进程,让它访问磁盘。为此设置“驱动调度”进程。 3. 由于磁盘与处理器是并行工作的,所以当磁盘在为一个进程服务时,占有处理器的其它进程可以提出使用磁盘(这里我们只要求访问磁道),即动态申请访问磁道,为此设置“接受请求”进程。 4. 为了模拟以上两个进程的执行,可以考虑使用随机数来确定二者的允许顺序,参考程序流程图。 5. “接受请求”进程建立一张“进程请求I/O”表,指出等待访问磁盘的进程要求访问的磁道,表的格式如下: 进程名 要求访问的磁道号 6. 磁盘调度的功能是查“请求I/O”表,当有等待访问的进程时,按磁盘调度算法从中选择一个等待访问的进程,按其指定的要求访问磁道。流程图中的“初始化”工作包括:初始化“请求I/O”表,设置当前移臂方向;当前磁道号。并且假设程序运行前“请求I/O”表中已有若干进程(4~8个)申请访问相应磁道。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赵英英俊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值