实验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操作,定义 HZ=100HZ=100HZ=100​​,内核的标准时间是jiffy,一个jiffy就是一个内部时钟周期,而内部时钟周期是由100HZ的频率所产生中的,也就是一个时钟滴答,间隔时间是10毫秒(ms).计算出来的时间也并非真实时间,而是时钟滴答次数,乘以10ms可以得到真正的时间。

代码实现

下面给出代码:

#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>#define HZ  100void 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,它们分别就是 stdinstdoutstderr

编写fprintk()函数

log 文件将被用来记录进程的状态转移轨迹。所有的状态转移都是在内核进行的。

在内核状态下,write() 功能失效,其原理等同于《系统调用》实验中不能在内核状态调用 printf(),只能调用 printk()。编写可在内核调用的 write() 的难度较大,所以这里直接给出源码。它主要参考了 printk()sys_write() 而写成的:

#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\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;
}

因为和 printk 的功能近似,建议将此函数放入到 kernel/printk.c 中。fprintk() 的使用方式类同与 C 标准库函数 fprintf(),唯一的区别是第一个参数是文件描述符,而不是文件指针。

例如:

// 向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,滴答

jiffieskernel/sched.c 文件中定义为一个全局变量:

long volatile jiffies=0;

它记录了从开机到当前时间的时钟中断发生次数。在 kernel/sched.c 文件中的 sched_init() 函数中,时钟中断处理函数被设置为:

set_intr_gate(0x20,&timer_interrupt);

而在 kernel/system_call.s 文件中将 timer_interrupt 定义为:

timer_interrupt:
!    ……
! 增加jiffies计数值incl jiffies
!    ……

这说明 jiffies 表示从开机时到现在发生的时钟中断次数,这个数也被称为 “滴答数”。

另外,在 kernel/sched.c 中的 sched_init() 中有下面的代码:

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

这三条语句用来设置每次时钟中断的间隔,即为 LATCH,而 LATCH 是定义在文件 kernel/sched.c 中的一个宏:

// 在 kernel/sched.c 中
#define LATCH  (1193180/HZ)// 在 include/linux/sched.h 中
#define HZ 100

再加上 PC 机 8253 定时芯片的输入时钟频率为 1.193180MHz,即 1193180/每秒,LATCH=1193180/100,时钟每跳 11931.8 下产生一次时钟中断,即每 1/100 秒(10ms)产生一次时钟中断,所以 jiffies 实际上记录了从开机以来共经过了多少个 10ms。

注意这里是 HZ=100HZ=100HZ=100 的情况,前文也介绍过。所以时间其实就是近似等于中断次数乘以 1/HZ1/HZ1/HZ​

寻找状态切换点

必须找到所有发生进程状态切换的代码点,并在这些点添加适当的代码,来输出进程状态变化的情况到 log 文件中。

此处要面对的情况比较复杂,需要对 kernel 下的 fork.csched.c 有通盘的了解,而 exit.c 也会涉及到。

例子 1:记录一个进程生命期的开始

第一个例子是看看如何记录一个进程生命期的开始,当然这个事件就是进程的创建函数 fork(),由《系统调用》实验可知,fork() 功能在内核中实现为 sys_fork(),该“函数”在文件 kernel/system_call.s 中实现为:

sys_fork:call find_empty_process
!    ……
! 传递一些参数push %gspushl %esipushl %edipushl %ebppushl %eax
! 调用 copy_process 实现进程创建call copy_processaddl $20,%esp

所以真正实现进程创建的函数是 copy_process(),它在 kernel/fork.c 中定义为:

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

因此要完成进程运行轨迹的记录就要在 copy_process() 中添加输出语句。

这里要输出两种状态,分别是“N(新建)”和“J(就绪)”。

例子 2:记录进入睡眠态的时间

第二个例子是记录进入睡眠态的时间。sleep_on() 和 interruptible_sleep_on() 让当前进程进入睡眠状态,这两个函数在 kernel/sched.c 文件中定义如下:

void sleep_on(struct task_struct **p)
{struct task_struct *tmp;
//    ……tmp = *p;
// 仔细阅读,实际上是将 current 插入“等待队列”头部,tmp 是原来的头部*p = current;
// 切换到睡眠态current->state = TASK_UNINTERRUPTIBLE;
// 让出 CPUschedule();
// 唤醒队列中的上一个(tmp)睡眠进程。0 换作 TASK_RUNNING 更好
// 在记录进程被唤醒时一定要考虑到这种情况,实验者一定要注意!!!if (tmp)tmp->state=0;
}
/* TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的区别在于不可中断的睡眠* 只能由wake_up()显式唤醒,再由上面的 schedule()语句后的**   if (tmp) tmp->state=0;** 依次唤醒,所以不可中断的睡眠进程一定是按严格从“队列”(一个依靠* 放在进程内核栈中的指针变量tmp维护的队列)的首部进行唤醒。而对于可* 中断的进程,除了用wake_up唤醒以外,也可以用信号(给进程发送一个信* 号,实际上就是将进程PCB中维护的一个向量的某一位置位,进程需要在合* 适的时候处理这一位。感兴趣的实验者可以阅读有关代码)来唤醒,如在* schedule()中:**  for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)*      if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&*         (*p)->state==TASK_INTERRUPTIBLE)*         (*p)->state=TASK_RUNNING;//唤醒** 就是当进程是可中断睡眠时,如果遇到一些信号就将其唤醒。这样的唤醒会* 出现一个问题,那就是可能会唤醒等待队列中间的某个进程,此时这个链就* 需要进行适当调整。interruptible_sleep_on和sleep_on函数的主要区别就* 在这里。*/
void interruptible_sleep_on(struct task_struct **p)
{struct task_struct *tmp;…tmp=*p;*p=current;
repeat:    current->state = TASK_INTERRUPTIBLE;schedule();
// 如果队列头进程和刚唤醒的进程 current 不是一个,
// 说明从队列中间唤醒了一个进程,需要处理if (*p && *p != current) {// 将队列头唤醒,并通过 goto repeat 让自己再去睡眠(**p).state=0;goto repeat;}*p=NULL;
//作用和 sleep_on 函数中的一样if (tmp)tmp->state=0;
}

总的来说,Linux 0.11 支持四种进程状态的转移:就绪到运行、运行到就绪、运行到睡眠和睡眠到就绪,此外还有新建和退出两种情况。其中就绪与运行间的状态转移是通过 schedule()(它亦是调度算法所在)完成的;运行到睡眠依靠的是 sleep_on()interruptible_sleep_on(),还有进程主动睡觉的系统调用 sys_pause()sys_waitpid();睡眠到就绪的转移依靠的是 wake_up()。所以只要在这些函数的适当位置插入适当的处理语句就能完成进程运行轨迹的全面跟踪了。

修改fork.c文件

fork.c文件在kernel目录下,这里要输出两种状态,分别是“N(新建)”和“J(就绪)”,下面做出两处修改:

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

修改sched.c文件

文件位置:kernel/sched.c

修改schedule函数
//这里仅仅说一下改动了什么
/* check alarm, wake up any interruptible tasks that have got a signal */
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)if (*p) {if ((*p)->alarm && (*p)->alarm < jiffies) {(*p)->signal |= (1<<(SIGALRM-1));(*p)->alarm = 0;}if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&(*p)->state==TASK_INTERRUPTIBLE){(*p)->state=TASK_RUNNING;fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);} }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;
}
//切换到相同的进程不输出
if(current->pid != task[next] ->pid){/*新建修改--时间片到时程序 => 就绪*/if(current->state == TASK_RUNNING)fprintk(3,"%d\tJ\t%d\n",current->pid,jiffies);fprintk(3,"%d\tR\t%d\n",task[next]->pid,jiffies);}
// 切换到 next 进程
switch_to(next);
修改sys_pause函数
int sys_pause(void)
{current->state = TASK_INTERRUPTIBLE;/**修改--当前进程  运行 => 可中断睡眠*/if(current->pid != 0)fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);schedule();return 0;
}
修改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\tW\t%d\n",current->pid,jiffies);schedule();if (tmp){tmp->state=0;/**修改--原等待队列 第一个进程 => 唤醒(就绪)*/fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);}
}
修改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\tW\t%d\n",current->pid,jiffies);schedule();if (*p && *p != current) {(**p).state=0;/**修改--当前进程 => 可中断睡眠*/fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);goto repeat;}*p=NULL;if (tmp){tmp->state=0;/**修改--原等待队列 第一个进程 => 唤醒(就绪)*/fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);}
}
修改wake_up函数
void wake_up(struct task_struct **p)
{if (p && *p) {(**p).state=0;/**修改--唤醒 最后进入等待序列的 进程*/fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);*p=NULL;}
}

修改exit.c文件

当一个进程结束了运行或在半途中终止了运行,那么内核就需要释放该进程所占用的系统资源。这包括进程运行时打开的文件、申请的内存等。

当一个用户程序调用exit()系统调用时,就会执行内核函数do_exit()。该函数会首先释放进程代码段和数据段占用的内存页面,关闭进程打开着的所有文件,对进程使用的当前工作目录、根目录和运行程序的i节点进行同步操作。如果进程有子进程,则让init进程作为其所有子进程的父进程。如果进程是一个会话头进程并且有控制终端,则释放控制终端(如果按照实验的数据,此时就应该打印了),并向属于该会话的所有进程发送挂断信号 SIGHUP,这通常会终止该会话中的所有进程。然后把进程状态置为僵死状态 TASK_ZOMBIE。并向其原父进程发送 SIGCHLD 信号,通知其某个子进程已经终止。最后 do_exit()调用调度函数去执行其他进程。由此可见在进程被终止时,它的任务数据结构仍然保留着。因为其父进程还需要使用其中的信息。

在子进程在执行期间,父进程通常使用wait()waitpid()函数等待其某个子进程终止。当等待的子进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到自己进程中。最终释放已终止子进程任务数据结构所占用的内存页面,并置空子进程在任务数组中占用的指针项。

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\tE\t%d\n",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\tW\t%d\n",current->pid,jiffies);schedule();if (!(current->signal &= ~(1<<(SIGCHLD-1))))goto repeat;elsereturn -EINTR;}return -ECHILD;
}

小结

总的来说,Linux 0.11 支持四种进程状态的转移:就绪到运行、运行到就绪、运行到睡眠和睡眠到就绪,此外还有新建和退出两种情况。其中就绪与运行间的状态转移是通过 schedule()(它亦是调度算法所在)完成的;运行到睡眠依靠的是 sleep_on()interruptible_sleep_on(),还有进程主动睡觉的系统调用 sys_pause()sys_waitpid();睡眠到就绪的转移依靠的是 wake_up()。所以只要在这些函数的适当位置插入适当的处理语句就能完成进程运行轨迹的全面跟踪了。

为了让生成的 log 文件更精准,以下几点请注意:

  • 进程退出的最后一步是通知父进程自己的退出,目的是唤醒正在等待此事件的父进程。从时序上来说,应该是子进程先退出,父进程才醒来。
  • 系统无事可做的时候,进程 0 会不停地调用 sys_pause(),以激活调度算法。此时它的状态可以是等待态,等待有其它可运行的进程;也可以叫运行态,因为它是唯一一个在 CPU 上运行的进程,只不过运行的效果是等待。

编译

重新编译

make all

编译运行process.c

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

// oslab目录下运行
sudo ./mount-hdc
cp ./test3/process.c ./hdc/usr/root/
sudo umonut hdc

进入linux-0.11

gcc -o process process.c
./process
sync

使用./process即可运行目标文件,运行后会生成log文件,生成log文件后一定要记得刷新,然后将其拷贝到oslab/test3目录,命令如下:

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

process.log自动化分析

实验楼stat_log.py下载地址

只要给 stat_log.py 加上执行权限(使用的命令为 chmod +x stat_log.py)就可以直接运行它。

在结果中我们可以看到各个进程的周转时间(Turnaround,指作业从提交到完成所用的总时间)、等待时间等,以及平均周转时间和等待时间。

修改时间片

MOOC哈工大操作系统实验3:进程运行轨迹的跟踪与统计_ZhaoTianhao的博客-CSDN博客

这段没有耐心实现了,摘录了一位博主的解释

linux0.11采用的调度算法是一种综合考虑进程优先级并能动态反馈调整时间片的轮转调度算法。 那么什么是轮转调度算法呢?它为每个进程分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程;如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。那什么是综合考虑进程优先级呢?就是说一个进程在阻塞队列中停留的时间越长,它的优先级就越大,下次就会被分配更大的时间片。
进程之间的切换是需要时间的,如果时间片设定得太小的话,就会发生频繁的进程切换,因此会浪费大量时间在进程切换上,影响效率;如果时间片设定得足够大的话,就不会浪费时间在进程切换上,利用率会更高,但是用户交互性会受到影响,举一个很直观的例子:我在银行排队办业务,假设我要办的业务很简单只需要占用1分钟,如果每个人的时间片是30分钟,而我前面的每个人都要用满这30分钟,那我就要等上好几个小时!如果每个人的时间片是2分钟的话,我只需要等十几分钟就可以办理我的业务了,前面没办完的会在我之后轮流地继续去办。所以时间片不能过大或过小,要兼顾CPU利用率和用户交互性。
时间片的初始值是进程0的priority,是在linux-0.11/include/linux/sched.h的宏 INIT_TASK 中定义的,如下:我们只需要修改宏中的第三个值即可,该值即时间片的初始值。

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

修改完后再次编译make all,进入模拟器后编译运行测试文件process.c,然后运行统计脚本stat_log.py查看结果,与之前的结果进行对比。

问题回答

问题1:单进程编程和多进程编程的区别?

1.执行方式:单进程编程是一个进程从上到下顺序进行;多进程编程可以通过并发执行,即多个进程之间交替执行,如某一个进程正在I/O输入输出而不占用CPU时,可以让CPU去执行另外一个进程,这需要采取某种调度算法。

2.数据是否同步:单进程的数据是同步的,因为单进程只有一个进程,在进程中改变数据的话,是会影响这个进程的;多进程的数据是异步的,因为子进程数据是父进程数据在内存另一个位置的拷贝,因此改变其中一个进程的数据,是不会影响到另一个进程的。

3.CPU利用率:单进程编程的CPU利用率低,因为单进程在等待I/O时,CPU是空闲的;多进程编程的CPU利用率高,因为当某一进程等待I/O时,CPU会去执行另一个进程,因此CPU的利用率高。

4.多进程用途更广泛。

问题2:仅针对样本程序建立的进程,在修改时间片前后,log 文件的统计结果都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?

依次将时间偏设为1,5,10,15,20,25,50,100,150后,经统计分析log文件可以发现:
1)在一定的范围内,平均等待时间,平均完成时间的变化随着时间片的增大而减小。这是因为在时间片小的情况下,cpu将时间耗费在调度切换上,所以平均等待时间增加。
2)超过一定的范围之后,这些参数将不再有明显的变化,这是因为在这种情况下,RR轮转调度就变成了FCFS先来先服务了。随着时间片的修改,吞吐量始终没有明显的变化,这是因为在单位时间内,系统所能完成的进程数量是不会变的。

警示

编译好后进入linux-0.11

直接报了内核错误,这肯定是之前的代码打错了。那么多代码,我怎么知道错误在哪。但是注意以前正常情况下会打印剩余空间。

而先前改代码的时候发现这段打印的代码就在进程1 init() 函数内,所以推断是修改进程0时出现了错误

好家伙,顺序反了。之前说了,文件系统初始化,描述符 0、1 和 2 关联之后,才能打开 log 文件。这里却直接先打开 log 文件了。

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

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

    进程运行轨迹的跟踪与统计 难度系数:★★★☆☆ 实验目的 掌握Linux下的多进程编程技术: 通过对进程运行轨迹的跟踪来形象化进程的概念: 在进程运行轨迹跟踪的基础上进行相应的数据统计,从而能对进程调 ...

  2. 操作系统实验3:进程运行轨迹的跟踪与统计

    参考 哈工大操作系统实验 B站UP主的视频讲解 Linux内核完全注释:基于0.11内核(修正版V3.0) https://www.cnblogs.com/wanghuizhao/p/16644919 ...

  3. Linux 0.11进程运行轨迹的跟踪,进程运行轨迹的跟踪与统计

    1,到内核init/main.c下修改信息:(cd/oslab/oslab/linux-0.11/init) 2,向kernel/printk.c中添加打印日志的功能 注意,是在源文件下增加代码: 3 ...

  4. 【操作系统】实验楼实验四——进程运行的轨迹跟踪与设计

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

  5. 操作系统实验一到实验九合集(哈工大李治军)

    操作系统实验 作者寄语 操作系统实验的学习是一个循序渐进的过程,初次看linux-0.11中的代码,看着满屏的汇编语言,确实头疼.但通过学习赵炯博士的Linux内核0.11完全注释,结合着王爽老师的汇 ...

  6. 操作系统实验四——使用命名管道实现进程通信

    操作系统实验四--使用命名管道实现进程通信 一. 实验目的 (1)了解windows系统环境下的进程通讯机制. (2)熟悉Windows系统提供的进程通信API. 二. 实验准备 相关API函数介绍 ...

  7. 操作系统实验四-LRU算法的模拟

    操作系统实验四:页式虚拟存储管理的模拟 一.实验目的: 掌握存储管理的基本原理.地址变换过程:用软件实现地址转换过程:用一种常用的页面置换算法来处理缺页中断并研究其命中率. 二.实验题目: 1.模拟请 ...

  8. C语言 操作系统实验 四种调度(最高响应比优先算法 HRN)

    注: 本文是四个调度算法的第一篇算法. 本文是根据CSDN上某一FCFS调度算法魔改来的,所以FCFS的算法不会发到网站. 我是个菜鸡,发文是为了纪念自己完成了代码,以及累计自己的经验. 如有知识错误 ...

  9. 广州大学2020操作系统实验四:文件系统

    相关资料 广州大学2020操作系统实验一:进程管理与进程通信 广州大学2020操作系统实验二:银行家算法 广州大学2020操作系统实验三:内存管理 广州大学2020操作系统实验四:文件系统 广州大学2 ...

最新文章

  1. 现代hy-9600音响_从音响工程师到软件工程师-为什么我要学习编码
  2. 听听阿里老哥对算法工程师技术学习路线的建议
  3. 铁钉的blog地址 http://nails.blog.51cto.com
  4. [Leedcode][JAVA][第445题][链表][栈]
  5. 深度学习中防止过拟合的方法
  6. A - 数据结构实验之栈与队列一:进制转换
  7. vb6如何判断文件是否存在_使用boost.filesystem检查文件是否存在的正确姿势
  8. mac PHP 环境搭建
  9. c++程序调用python代码_使用C++调用Python代码的方法详解
  10. 【Android病毒分析报告】- 手机支付毒王“银行悍匪”的前世今生
  11. 计算机vb代码电阻,利用VB程序编写色环电阻阻值计算器
  12. mysql误删除数据恢复_mysql误删除数据恢复
  13. html表格圣杯布局页面,Css圣杯布局
  14. java上传文件夹文件
  15. 官方正式发布 Java 16
  16. ArcGIS三维资源收集帖
  17. 【PyTorch教程】P27、28、29 完整的模型套路
  18. mysql stop failed_Mysql报错:Failed to stop mysqld.service: Unit mysqld.service not loaded.
  19. webstorm2020背景和字体_WebStorm改变字体大小以及更换背景颜色
  20. vue移动端项目经验

热门文章

  1. 不同应用选择荧光染料 -CY7 ALK脂溶性Sulfo-Cyanine7 alkyne 结构式应用
  2. java word模版填充_Java 数据填充到word模板中
  3. AirServer 7.3.0中文版手机设备无线传送电脑屏幕工具
  4. 浅谈 Web 网站架构演变过程
  5. MySQL基础期末考试试题
  6. 【商业信息】PNP ID注册名单 2019-05-21
  7. android手机使用otg usb手柄
  8. 如何避免成为背锅侠?
  9. CISCO CDP邻居发现协议
  10. 【工具使用】怎么设置SSH隧道(Port Forwarding)