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

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

实验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。

目录
相关文章
|
30天前
|
消息中间件 存储 算法
【软件设计师备考 专题 】操作系统的内核(中断控制)、进程、线程概念
【软件设计师备考 专题 】操作系统的内核(中断控制)、进程、线程概念
82 0
|
26天前
|
资源调度 监控 算法
深入理解操作系统:进程管理与调度策略
本文旨在探讨操作系统中进程管理的核心概念及其实现机制,特别是进程调度策略对系统性能的影响。通过分析不同类型操作系统的进程调度算法,我们能够了解这些策略如何平衡响应时间、吞吐量和公平性等关键指标。文章首先介绍进程的基本概念和状态转换,随后深入讨论各种调度策略,如先来先服务(FCFS)、短作业优先(SJF)、轮转(RR)以及多级反馈队列(MLQ)。最后,文章将评估现代操作系统在面对多核处理器和虚拟化技术时,进程调度策略的创新趋势。
|
17天前
|
算法 Linux 调度
深入理解操作系统的进程调度策略
【4月更文挑战第8天】本文深入剖析了操作系统中的关键组成部分——进程调度策略。首先,我们定义了进程调度并解释了其在资源分配和系统性能中的作用。接着,探讨了几种经典的调度算法,包括先来先服务(FCFS)、短作业优先(SJF)以及多级反馈队列(MLQ)。通过比较这些算法的优缺点,本文揭示了它们在现实世界操作系统中的应用与局限性。最后,文章指出了未来进程调度策略可能的发展方向,特别是针对多核处理器和云计算环境的适应性。
|
18天前
|
算法 调度 UED
深入理解操作系统中的进程调度策略
【4月更文挑战第7天】 在多任务操作系统中,进程调度策略是决定系统性能和响应速度的关键因素之一。本文将探讨现代操作系统中常用的进程调度算法,包括先来先服务、短作业优先、轮转调度以及多级反馈队列等。通过比较各自的优势与局限性,我们旨在为读者提供一个全面的视角,以理解如何根据不同场景选择合适的调度策略,从而优化系统资源分配和提升用户体验。
|
27天前
|
算法 Unix Linux
深入理解操作系统:进程管理与调度策略
在现代操作系统的核心功能中,进程管理及其调度机制是维护系统稳定与高效运行的基石。本文将深入探讨操作系统中的进程概念、进程状态、以及进程调度策略。我们将从理论和实践两个维度出发,解析不同操作系统如何通过进程管理来优化资源分配,提升系统响应速度,并保证多任务环境下的公平性与效率。特别地,文章还将讨论实时系统中的调度策略,以及它们对于确保关键任务按时完成的重要性。
14 1
|
1月前
|
算法 调度 开发者
深入理解操作系统的进程调度策略
【2月更文挑战第30天】 在现代操作系统中,进程调度策略是其核心组成部分之一,关系到系统资源的合理分配和任务执行的高效性。本文将详细分析几种常见的进程调度算法,包括先来先服务(FCFS)、短作业优先(SJF)、轮转调度(RR)和多级反馈队列(MLFQ),并探讨它们在不同场景下的适用性和优缺点。通过对比分析,旨在帮助读者深入理解进程调度机制,以及在实际系统设计时如何根据需求选择合适的调度策略。
|
1月前
|
算法 大数据 Linux
深入理解操作系统之进程管理的艺术
【2月更文挑战第30天】 在现代计算机系统中,操作系统扮演着指挥官的角色,而进程管理则是其核心职能之一。本文旨在探讨操作系统中进程管理的关键技术和原理,以及它们如何影响系统性能和用户体验。文章将详细解析进程的概念、生命周期、调度算法及进程间的通信机制,并讨论当前操作系统如Linux和Windows在进程管理方面的创新策略。通过深入分析,本文揭示了高效进程管理对于提升操作系统响应速度和资源利用率的重要性。
|
1月前
|
机器学习/深度学习 算法 调度
深入理解操作系统中的进程调度策略
【2月更文挑战第30天】 本文聚焦于操作系统核心功能之一——进程调度,探究其背后的策略与影响性能的关键因素。不同于传统的摘要重述,我们将通过分析进程调度的复杂性、设计目标和不同调度算法的特点,来揭示操作系统如何在多任务环境中确保资源合理分配与系统响应效率。文章将详细讨论几种经典调度策略,并比较它们在不同应用场景下的表现和适用性。
|
15天前
|
监控 Unix Linux
Linux操作系统调优相关工具(四)查看Network运行状态 和系统整体运行状态
Linux操作系统调优相关工具(四)查看Network运行状态 和系统整体运行状态
30 0
|
17天前
|
Linux 编译器 开发者
Linux设备树解析:桥接硬件与操作系统的关键架构
在探索Linux的庞大和复杂世界时🌌,我们经常会遇到许多关键概念和工具🛠️,它们使得Linux成为了一个强大和灵活的操作系统💪。其中,"设备树"(Device Tree)是一个不可或缺的部分🌲,尤其是在嵌入式系统🖥️和多平台硬件支持方面🔌。让我们深入了解Linux设备树是什么,它的起源,以及为什么Linux需要它🌳。
Linux设备树解析:桥接硬件与操作系统的关键架构