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

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

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


实验目的

掌握 Linux 下的多进程编程技术;

通过对进程运行轨迹的跟踪来形象化进程的概念;

在进程运行轨迹跟踪的基础上进行相应的数据统计,从而能对进程调度算法进行实际的量化评价,更进一步加深对调度和调度算法的理解,获得能在实际操作系统上对调度算法进行实验数据对比的直接经验。


实验内容

进程从创建(Linux 下调用 fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态;在运行的过程中如果启动了一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出 CPU;当文件读写完毕以后,操作系统会在将其切换成就绪态,等待进程调度算法来调度该进程执行……


本次实验包括如下内容:


基于模板 process.c 编写多进程的样本程序,实现如下功能: + 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒; + 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;

在 Linux0.11 上实现进程运行轨迹的跟踪。 + 基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。

在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序进行统计。

修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。

/var/process.log 文件的格式必须为:

pid    X    time

其中:


pid 是进程的 ID;

X 可以是 N、J、R、W 和 E 中的任意一个,分别表示进程新建(N)、进入就绪态(J)、进入运行态®、进入阻塞态(W) 和退出(E);

time 表示 X 发生的时间。这个时间不是物理时间,而是系统的滴答时间(tick);

三个字段之间用制表符分隔。例如:

12    N    1056
12    J    1057
4    W    1057
12    R    1057
13    N    1058
13    J    1059
14    N    1059
14    J    1060
15    N    1060
15    J    1061
12    W    1061
15    R    1061
15    J    1076
14    R    1076
14    E    1076
......


编写process.c文件

提示

在 Ubuntu 下,top 命令可以监视即时的进程状态。在 top 中,按 u,再输入你的用户名,可以限定只显示以你的身份运行的进程,更方便观察。按 h 可得到帮助。


在 Ubuntu 下,ps 命令可以显示当时各个进程的状态。ps aux 会显示所有进程;ps aux | grep xxxx 将只显示名为 xxxx 的进程。更详细的用法请问 man。



在 Linux 0.11 下,按 F1 可以即时显示当前所有进程的状态。



文件主要作用

process.c文件主要实现了一个函数:


/*
 * 此函数按照参数占用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,直到总运行时间超过last为止
 * 所有时间的单位为秒
 */
cpuio_bound(int last, int cpu_time, int io_time);


下面是 4 个使用的例子:

// 比如一个进程如果要占用10秒的CPU时间,它可以调用:
cpuio_bound(10, 1, 0);
// 只要cpu_time>0,io_time=0,效果相同
// 以I/O为主要任务:
cpuio_bound(10, 0, 1);
// 只要cpu_time=0,io_time>0,效果相同
// CPU和I/O各1秒钟轮回:
cpuio_bound(10, 1, 1);
// 较多的I/O,较少的CPU:
// I/O时间是CPU时间的9倍
cpuio_bound(10, 1, 9);


修改此模板,用 fork() 建立若干个同时运行的子进程,父进程等待所有子进程退出后才退出,每个子进程按照你的意愿做不同或相同的 cpuio_bound(),从而完成一个个性化的样本程序。


它可以用来检验有关 log 文件的修改是否正确,同时还是数据统计工作的基础。


wait() 系统调用可以让父进程等待子进程的退出。


关键函数解释

fork()

这里摘录某位博主的详解操作系统之 fork() 函数详解 - 简书 (jianshu.com)


fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。


一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

#include <unistd.h>  
#include <stdio.h>   
int main ()   
{   
    pid_t fpid; //fpid表示fork函数返回的值  
    int count=0;  
    fpid=fork();   
    if (fpid < 0)   
        printf("error in fork!");   
    else if (fpid == 0) {  
        printf("i am the child process, my process id is %d/n",getpid());   
        printf("我是爹的儿子/n");//对某些人来说中文看着更直白。  
        count++;  
    }  
    else {  
        printf("i am the parent process, my process id is %d/n",getpid());   
        printf("我是孩子他爹/n");  
        count++;  
    }  
    printf("统计结果是: %d/n",count);  
    return 0;  
}  


运行结果:

i am the child process, my process id is 5574
我是爹的儿子
统计结果是: 1
i am the parent process, my process id is 5573
我是孩子他爹
统计结果是: 1


在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)


为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

1)在父进程中,fork返回新创建子进程的进程ID;

2)在子进程中,fork返回0;

3)如果出现错误,fork返回一个负值;


在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。


引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.

 fork出错可能有两种原因:

 1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。

 2)系统内存不足,这时errno的值被设置为ENOMEM。

 创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

 每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。

 fork执行完毕后,出现两个进程



有人说两个进程的内容完全一样啊,怎么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量啊。

 执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。

 还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。


struct tms 结构体

struct tms 结构体定义在 <sys/times.h> 头文件里,具体定义如下:

引用
/* Structure describing CPU time used by a process and its children.  */ 
struct tms 
  { 
    clock_t tms_utime ;          /* User CPU time.  用户程序 CPU 时间*/ 
    clock_t tms_stime ;          /* System CPU time. 系统调用所耗费的 CPU 时间 */ 
    clock_t tms_cutime ;         /* User CPU time of dead children. 已死掉子进程的 CPU 时间*/ 
    clock_t tms_cstime ;         /* System CPU time of dead children.  已死掉子进程所耗费的系统调用 CPU 时间*/ 
  };


用户CPU时间和系统CPU时间之和为CPU时间,即命令占用CPU执行的时间总和。实际时间要大于CPU时间,因为Linux是多任务操作系统,往往在执行一条命令时,系统还要处理其他任务。另一个需要注意的问题是即使每次执行相同的命令,所花费的时间也不一定相同,因为其花费的时间与系统运行相关。


数据类型 clock_t

关于该数据类型的定义如下:

#ifndef _CLOCK_T_DEFINED
typedef long clock_t;
#define _CLOCK_T_DEFINED
#endif


clock_t 是一个长整型数。


在 time.h 文件中,还定义了一个常量 CLOCKS_PER_SEC ,它用来表示一秒钟会有多少个时钟计时单元,其定义如下:

#define CLOCKS_PER_SEC ((clock_t)1000)

下文就模拟cpu操作,定义 H Z = 100 HZ=100HZ=100,内核的标准时间是jiffy,一个jiffy就是一个内部时钟周期,而内部时钟周期是由100HZ的频率所产生中的,也就是一个时钟滴答,间隔时间是10毫秒(ms).计算出来的时间也并非真实时间,而是时钟滴答次数,乘以10ms可以得到真正的时间。


代码实现

下面给出代码:

#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!\n",i+1);
    return -1;
  }
  /*父进程继续fork*/
  }
  /*打印所有子进程PID*/
  for(i=0;i<10;i++)
  printf("Child PID: %d\n",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;
  }
}



答疑

❓ 为什么说它可以用来检验有关 log 文件的修改是否正确,同时还是数据统计工作的基础?


每个子进程都通过 cpuio_bound 函数实现了占用CPU和I/O时间的操作,并且可以精确的知道每个操作的时间。所以下面的 log 文件(日志文件)正确与否可以借此推算。


尽早打开log文件

操作系统启动后先要打开 /var/process.log,然后在每个进程发生状态切换的时候向 log 文件内写入一条记录,其过程和用户态的应用程序没什么两样。然而,因为内核状态的存在,使过程中的很多细节变得完全不一样。


为了能尽早开始记录,应当在内核启动时就打开 log 文件。内核的入口是 init/main.c 中的 main(),其中一段代码是:

//……
move_to_user_mode();
if (!fork()) {        /* we count on this going ok */
    init();
}
//……


这段代码在进程 0 中运行,先切换到用户模式,然后全系统第一次调用 fork() 建立进程 1。进程 1 调用 init()。


在 init()中:

// ……
//加载文件系统
setup((void *) &drive_info);
// 打开/dev/tty0,建立文件描述符0和/dev/tty0的关联
(void) open("/dev/tty0",O_RDWR,0);
// 让文件描述符1也和/dev/tty0关联
(void) dup(0);
// 让文件描述符2也和/dev/tty0关联
(void) dup(0);
// ……


这段代码建立了文件描述符 0、1 和 2,它们分别就是 stdin、stdout 和 stderr。这三者的值是系统标准(Windows 也是如此),不可改变。


可以把 log 文件的描述符关联到 3。文件系统初始化,描述符 0、1 和 2 关联之后,才能打开 log 文件,开始记录进程的运行轨迹。


为了能尽早访问 log 文件,我们要让上述工作在进程 0 中就完成。所以把这一段代码从 init() 移动到 main() 中,放在 move_to_user_mode() 之后(不能再靠前了),同时加上打开 log 文件的代码。


修改后的 main() 如下:

//……
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();
}
//……


打开 log 文件的参数的含义是建立只写文件,如果文件已存在则清空已有内容。文件的权限是所有人可读可写。


这样,文件描述符 0、1、2 和 3 就在进程 0 中建立了。根据 fork() 的原理,进程 1 会继承这些文件描述符,所以 init() 中就不必再 open() 它们。此后所有新建的进程都是进程 1 的子孙,也会继承它们。但实际上,init() 的后续代码和 /bin/sh 都会重新初始化它们。所以只有进程 0 和进程 1 的文件描述符肯定关联着 log 文件,这一点在接下来的写 log 中很重要。


小结

其实就是为了尽早打开log日志文件开始记录,那必须满足在用户模式且可以进行文件读写,因此最前的位置只能在 move_to_user_mode() 之后(不能再靠前了),并且建立文件描述符 0、1 和 2,它们分别就是 stdin、stdout 和 stderr。

目录
相关文章
|
11天前
|
算法 调度 UED
深入理解操作系统:进程调度与优先级队列
【10月更文挑战第31天】在计算机科学的广阔天地中,操作系统扮演着枢纽的角色,它不仅管理着硬件资源,还为应用程序提供了运行的环境。本文将深入浅出地探讨操作系统的核心概念之一——进程调度,以及如何通过优先级队列来优化资源分配。我们将从基础理论出发,逐步过渡到实际应用,最终以代码示例巩固知识点,旨在为读者揭开操作系统高效管理的神秘面纱。
|
4天前
|
消息中间件 安全 算法
深入理解操作系统:进程管理的艺术
【10月更文挑战第38天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是硬件与软件的桥梁,更是维持计算机运行秩序的守夜人。本文将带你走进操作系统的核心——进程管理,探索它是如何协调和优化资源的使用,确保系统的稳定与高效。我们将从进程的基本概念出发,逐步深入到进程调度、同步与通信,最后探讨进程安全的重要性。通过这篇文章,你将获得对操作系统进程管理的全新认识,为你的计算机科学之旅增添一份深刻的理解。
|
8天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【10月更文挑战第34天】本文旨在探讨操作系统中至关重要的一环——进程管理及其调度策略。我们将从基础概念入手,逐步揭示进程的生命周期、状态转换以及调度算法的核心原理。文章将通过浅显易懂的语言和具体实例,引导读者理解操作系统如何高效地管理和调度进程,保证系统资源的合理分配和利用。无论你是初学者还是有一定经验的开发者,这篇文章都能为你提供新的视角和深入的理解。
26 3
|
10天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
11天前
|
算法 调度 UED
深入理解操作系统的进程调度机制
本文旨在探讨操作系统中至关重要的组成部分之一——进程调度机制。通过详细解析进程调度的概念、目的、类型以及实现方式,本文为读者提供了一个全面了解操作系统如何高效管理进程资源的视角。此外,文章还简要介绍了几种常见的进程调度算法,并分析了它们的优缺点,旨在帮助读者更好地理解操作系统内部的复杂性及其对系统性能的影响。
|
12天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
27 2
|
12天前
|
消息中间件 算法 Linux
深入理解操作系统之进程管理
【10月更文挑战第30天】在数字时代的浪潮中,操作系统作为计算机系统的核心,扮演着至关重要的角色。本文将深入浅出地探讨操作系统中的进程管理机制,从进程的概念入手,逐步解析进程的创建、调度、同步与通信等关键过程,并通过实际代码示例,揭示这些理论在Linux系统中的应用。文章旨在为读者提供一扇窥探操作系统深层工作机制的窗口,同时激发对计算科学深层次理解的兴趣和思考。
|
13天前
|
消息中间件 算法 调度
深入理解操作系统:进程管理与调度策略
【10月更文挑战第29天】本文将带领读者深入探讨操作系统中的核心组件之一——进程,并分析进程管理的重要性。我们将从进程的生命周期入手,逐步揭示进程状态转换、进程调度算法以及优先级调度等关键概念。通过理论讲解与代码演示相结合的方式,本文旨在为读者提供对进程调度机制的全面理解,从而帮助读者更好地掌握操作系统的精髓。
29 1
|
9天前
|
消息中间件 算法 调度
深入理解操作系统:进程管理的艺术
【10月更文挑战第33天】本文旨在揭示操作系统中进程管理的神秘面纱,带领读者从理论到实践,探索进程调度、同步以及通信的精妙之处。通过深入浅出的解释和直观的代码示例,我们将一起踏上这场技术之旅,解锁进程管理的秘密。
15 0
|
11天前
|
算法 Linux 调度
深入理解操作系统之进程调度
【10月更文挑战第31天】在操作系统的心脏跳动中,进程调度扮演着关键角色。本文将深入浅出地探讨进程调度的机制和策略,通过比喻和实例让读者轻松理解这一复杂主题。我们将一起探索不同类型的调度算法,并了解它们如何影响系统性能和用户体验。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇理解操作系统深层工作机制的大门。
22 0

热门文章

最新文章