哈工大OS实验四——进程运行轨迹的跟踪与统计

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

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

process.c解读

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


使用last来判断退出,当last时间为小于0表示时间到达了。

记录一个开始时间,

消耗时间来假设使用了一个cpu时间,减去一个cpu时间,表示调用

使用sleep函数来模拟一秒,直到大于等于一个io时间退出,减去一个io时间,表示调用

调用完仍然last仍然没有小于0,继续调用。

proccess.c修改:

#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 p1,p2,p3,p4;
	
	p1=fork();
	if(p1<0){
		printf("file to fork");
	}else if(p1==0){
		printf("ppid:%4d pid:%4d\n",getppid(),getpid());
		cpuio_bound(10,1,0);
	}else{
		printf("ppid:%4d pid:%4d fpid:%4d\n",getppid(),getpid(),p1);
	}


	p2=fork();
	if(p2<0){
		printf("file to fork");
	}else if(p2==0){
		printf("ppid:%4d pid:%4d\n",getppid(),getpid());
		cpuio_bound(10,0,1);
	}else{
		printf("ppid:%4d pid:%4d fpid:%4d\n",getppid(),getpid(),p2);
	}

	p3=fork();
	if(p3<0){
		printf("file to fork");
	}else if(p3==0){
		printf("ppid:%4d pid:%4d\n",getppid(),getpid());
		cpuio_bound(10,1,1);
	}else{
		printf("ppid:%4d pid:%4d fpid:%4d\n",getppid(),getpid(),p3);
	}

	p4=fork();
	if(p4<0){
		printf("file to fork");
	}else if(p4==0){
		printf("ppid:%4d pid:%4d\n",getppid(),getpid());
		cpuio_bound(10,1,9);
	}else{
		printf("ppid:%4d pid:%4d fpid:%4d\n",getppid(),getpid(),p4);
	}
	wait(NULL);
	wait(NULL);
	wait(NULL);
	wait(NULL);	
	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;
	}
}

哈工大OS实验四——进程运行轨迹的跟踪与统计
执行得到结果
由此可见,首先实现的是281进程,创建了282,283,284,285四个进程,

282创建了288,289,290三个进程

283创建了286,287两个进程

284创建了291

288创建了292,293

292创建了296

289创建了295

自定义文件描述符

将log文件作为一个进程默认打开的文件,当需要时可以进行输入和输出,可以看成是一个基本输入输出文件,这里需要补充的知识。也就是说将log文件作为和屏幕一样的设备。

我们可以看源码的main函数,其中对于基本输入输出的定义。在init/main.c中
哈工大OS实验四——进程运行轨迹的跟踪与统计
这里当进行完了所有的设置的初始化之后,先进行了一个fork(),进行了分支,将子进程去调用init()函数初始化,父进程被阻断。再看init函数里面
哈工大OS实验四——进程运行轨迹的跟踪与统计
前面的setup函数用于加载文件系统,
open函数用于打开终端,作为这个进程的文件描述符,dup用于复制文件描述符,因为在之前这个进程没有打开任何的文件,所以open的文件描述序号为0,然后复制了两次文件描述符0,作为文件描述符1和2,这三个文件描述符就是标准输入,标准输出和标准错误输出,同时因为fork函数作为创建进程的函数,子进程会继承父进程的文件描述符,所以子进程默认保护文件描述符0,1,2

我们可以将这三行放在fork函数之前,这样每一个进程都会被监视。
哈工大OS实验四——进程运行轨迹的跟踪与统计同时再打开一个文件,这样之后的每一个进程开始创建都有文件描述符3,指向的是/var/process.log文件。
这个我们需要知道的是,这个创建过程我们是在内核完成的,那么在内核中我们肯定不能使用用户态的write函数,所以需要一个在内核中写文件的函数,这里直接给出fprintk(int fd,const char *fmt,……)函数,给出文件描述符,用法和fprintf一样。当进程状态发生了切换,我们只需要进行fprintk将其输出到/var/process.log中。

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

将这个函数写入kernel/printk.c中

时钟中断的实现

kernal中的sched.c是进行时钟中断的一个文件,其中的sched.c被init/main.c调用,用于对时钟的初始化。
其中定义了一个

long volatile jiffies=0;//这个记录了计算机从开机到当前发生了时钟中断次数

下面的timer_interrupt函数主要是当一个进程到了时钟中断,会调用一个do_timer函数,这个do_timer再调用shchedule来进行就绪状态的进程的选择执行,最后再从0环退出到3环。

timer_interrupt:
!    ……
! 增加jiffies计数值
    incl jiffies
!    ……
	call do_timer		# 'do_timer(long CPL)' does everything from
	addl $4,%esp		# task switching to accounting ...
	jmp ret_from_sys_call

再之后,在sched_init()中还定义了

// 设置8253模式
outb_p(0x36, 0x43);
outb_p(LATCH&0xff, 0x40);
outb_p(LATCH>>8, 0x40);
// 在 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。
时钟每秒跳动1193180次,那么LATCH=1193180/100也就是说,每跳动11931.8次就进行一次jiffies的增加,表示的是进行了1/100秒。

寻找状态切换点:

要显示进程状态要修改的文件主要是

fork.c 其中包含产生进程的代码

sched.c 其中包含调度相关代码 只需要修改.state进行修改的地方 但是需要注意的是就绪状态和运行状态是使用一个标志0来表示的,这就需要我们对schedule()函数的代码做详细的分析,得出在什么时候需要将一个进程的状态置为运行,什么时候是就绪

exit.c主要是进程退出的代码。只需要.state进行了修改的地方进行输出,且当我们需要在schedule()函数中添加

fork()

一个进程的从fork()开始,所以我们可以看fork()函数的源代码,fork函数的源代码在sys_fork()中,在kernel/system_call.s中实现

.align 2
sys_fork:
	call find_empty_process
	testl %eax,%ea
	
	js 1f
	push %gs
	pushl %esi
	pushl %edi
	pushl %ebp
	pushl %eax
	call copy_process
	addl $20,%esp
1:	ret

其中的copy_process进行的就是一个新的进程的创建工作,在kernel/fork.c中实现,代码为

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx,
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;

	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	//*********************************new code
	fprintk(3,"%d\tN\t%d\n",p->pid,jiffies);	
	//*********************************
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)
		if ((f=p->filp[i]))
			f->f_count++;
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case  设置进程的状态为就绪态*/
	//********************************new code
	fprintk(3,"%d\tJ\t%d\n",p->pid,jiffies);
	//*******************************
	return last_pid;
}

sche.c:

记录加入睡眠状态的时间。

sleep_on() 和 interruptible_sleep_on() 让当前进程进入睡眠状态,可以在kernel/sched.c中找到源代码。

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;
	//****************************************new code
	fprintk(3,"%d\tW\t%d\n",p->pid,jiffies);
	//****************************************
	schedule();
	if (tmp){
		tmp->state=0;
		fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);
	}
}
//函数功能,给出一个参数是pcb的队列,主要功能是:
//将当前运行进程加入到阻塞队列,同时将阻塞队列中的进程加入到就绪队列中,
int sys_pause(void)
{
	current->state = TASK_INTERRUPTIBLE;
	
	fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
	
	schedule();
	return 0;
}
//将进程加入到阻塞队列
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;

	//增加一个阻塞状态,从执行到阻塞***************************************************************** 
	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);
		//增加一个就绪状态,从阻塞到就绪***************************************************************** 
			
	}
}
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;
	}
}
void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;

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

/* this is the scheduler proper: */

	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) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	
	//************************************
	//其中的next就是需要切换的进程的在队列中的下标,当这个next和当前进程不是同一个就在文件中打印当前J状态,如果一样,就增加一个R状态
	if(task[next]->pid != current->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);
	}
	
	switch_to(next);
}

主要的修改代码就这些,只需要在state发生了变换的地方修改,且加上在调度切换的时候,判断一下是否调度的进程和当前进程为同一个。

exit.c:

在这个文件中也有两处地方进行了state的切换,这里不再列举,只需要如法炮制进行fprintk就可以了

测试:

进入修改后的系统,运行了process.c的代码

找到/var/proccess.log文件,建议拿到本机来查看,因为在实验环境中比较卡,而且因为main()函数中的sys_pause()这一句,让0号进程产生wait状态的情况特别多,更不用说linux0.11中了,直接卡死
哈工大OS实验四——进程运行轨迹的跟踪与统计
这里的实验数据存在一些问题:所有的进程没有N状态,事后翻源码发现fork.c中的fprintk语句把描述符3写成了“3”,导致了N状态没有写入proccess.log中
上图就是运行process.c时,操作系统写入到process.log的字符。在这里我感觉有些不对,因为有多次我执行完process.c后直接退出了,但是字符没有写入文件中,我猜测是因为:需要等待,因为每当进行一个进程切换,就需要读一次磁盘,所以当执行完process.c后需要等很长一段时间,等待CPU将写磁盘的工作做完,这个结论暂时也不知道是否正确。
哈工大OS实验四——进程运行轨迹的跟踪与统计我等待了很长时间后,切换了4百万次,其中百分之99都是main.c中的for(;; ) pause();造成的
哈工大OS实验四——进程运行轨迹的跟踪与统计
后面想一想完全可以将pause()函数中的fprintk函数去掉,来保证完成速度,当时并没有意识到……

上一篇:技术支持垃圾邮件使用iframe“冻结”浏览器


下一篇:CodeForces - 32E Hide-and-Seek【计算几何】