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

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

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


实验内容

  • 基于模板 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
    评论
实验进程管理   Windows所创建的每个进程都从调用CreateProcess() API函数开始,该函数的任务是在对象管理器子系统内初始化进程对象。每一进程都以调用ExitProcess() 或TerminateProcess() API函数终止。通常应用程序的框架负责调用 ExitProcess() 函数。对于C++ 运行库来说,这一调用发生在应用程序的main() 函数返回之后。 1. 创建进程 CreateProcess() 调用的核心参数是可执行文件运行时的文件名及其命令行。表 2-1详细地列出了每个参数的类型和名称。   表2-1 CreateProcess() 函数的参数 参数名称 使用目的 LPCTSTR lpApplivationName 全部或部分地指明包括可执行代码的EXE文件的文件名 LPCTSTR lpCommandLine 向可执行文件发送的参数 LPSECURIITY_ATTRIBUTES lpProcessAttributes 返回进程句柄的安全属性。主要指明这一句柄是否应该由其他子进程所继承 LPSECURIITY_ATTRIBUTES lpThreadAttributes 返回进程的主线程的句柄的安全属性 BOOL bInheritHandle 一种标志,告诉系统允许新进程继承创建者进程的句柄 DWORD dwCreationFlage 特殊的创建标志 (如CREATE_SUSPENDED) 的位标记 LPVOID lpEnvironment 向新进程发送的一套环境变量;如为null值则发送调用者环境 LPCTSTR lpCurrentDirectory 新进程的启动目录 STARTUPINFO lpStartupInfo STARTUPINFO结构,包括新进程的输入和输出配置的详情 LPPROCESS_INFORMATION lpProcessInformation 调用的结果块;发送新应用程序的进程和主线程的句柄和ID   可以指定第一个参数,即应用程序的名称,其中包括相对于当前进程的当前目录的全路径或者利用搜索方法找到的路径;lpCommandLine参数允许调用者向新应用程序发送数据;接下来的三个参数与进程和它的主线程以及返回的指向该对象的句柄的安全性有关。 然后是标志参数,用以在dwCreationFlags参数中指明系统应该给予新进程什么行为。经常使用的标志是CREATE_SUSPNDED,告诉主线程立刻暂停。当准备好时,应该使用ResumeThread() API来启动进程。另一个常用的标志是CREATE_NEW_CONSOLE,告诉新进程启动自己的控制台窗口,而不是利用父窗口。这一参数还允许设置进程的优先级,用以向系统指明,相对于系统中所有其他的活动进程来说,给此进程多少CPU时间。 接着是CreateProcess() 函数调用所需要的三个通常使用缺省值的参数。第一个参数是lpEnvironment参数,指明为新进程提供的环境;第二个参数是lpCurrentDirectory,可用于向主创进程发送与缺省目录不同的新进程使用的特殊的当前目录;第三个参数是STARTUPINFO数据结构所必需的,用于在必要时指明新应用程序的主窗口的外观。 CreateProcess() 的最后一个参数是用于新进程对象及其主线程的句柄和ID的返回值缓冲区。以PROCESS_INFORMATION结构中返回的句柄调用CloseHandle() API函数是重要的,因为如果不将这些句柄关闭的话,有可能危及主创进程终止之前的任何未释放的资源。 2. 正在运行进程 如果一个进程拥有至少一个执行线程,则为正在系统中运行进程。通常,这种进程使用主线程来指示它的存在。当主线程结束时,调用ExitProcess() API函数,通知系统终止它所拥有的所有正在运行、准备运行或正在挂起的其他线程。当进程正在运行时,可以查看它的许多特性,其中少数特性也允许加以修改。 首先可查看的进程特性是系统进程标识符 (PID) ,可利用GetCurrentProcessId() API函数来查看,与GetCurrentProcess() 相似,对该函数的调用不能失败,但返回的PID在整个系统中都可使用。其他的可显示当前进程信息的API函数还有GetStartupInfo()和GetProcessShutdownParameters() ,可给出进程存活期内的配置详情。 通常,一个进程需要它的运行期环境的信息。例如API函数GetModuleFileName() 和GetCommandLine() ,可以给出用在CreateProcess() 中的参数以启动应用程序。在创建应用程序时可使用的另一个
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

赵英英俊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值