ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

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

2021-04-30 12:32:54  阅读:206  来源: 互联网

标签:轨迹 int task current 哈工大 time 进程 OS tss


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

进程从创建(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;
	}
}

在这里插入图片描述
执行得到结果
由此可见,首先实现的是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中
在这里插入图片描述
这里当进行完了所有的设置的初始化之后,先进行了一个fork(),进行了分支,将子进程去调用init()函数初始化,父进程被阻断。再看init函数里面
在这里插入图片描述
前面的setup函数用于加载文件系统,
open函数用于打开终端,作为这个进程的文件描述符,dup用于复制文件描述符,因为在之前这个进程没有打开任何的文件,所以open的文件描述序号为0,然后复制了两次文件描述符0,作为文件描述符1和2,这三个文件描述符就是标准输入,标准输出和标准错误输出,同时因为fork函数作为创建进程的函数,子进程会继承父进程的文件描述符,所以子进程默认保护文件描述符0,1,2

我们可以将这三行放在fork函数之前,这样每一个进程都会被监视。
在这里插入图片描述同时再打开一个文件,这样之后的每一个进程开始创建都有文件描述符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中了,直接卡死
在这里插入图片描述
这里的实验数据存在一些问题:所有的进程没有N状态,事后翻源码发现fork.c中的fprintk语句把描述符3写成了“3”,导致了N状态没有写入proccess.log中
上图就是运行process.c时,操作系统写入到process.log的字符。在这里我感觉有些不对,因为有多次我执行完process.c后直接退出了,但是字符没有写入文件中,我猜测是因为:需要等待,因为每当进行一个进程切换,就需要读一次磁盘,所以当执行完process.c后需要等很长一段时间,等待CPU将写磁盘的工作做完,这个结论暂时也不知道是否正确。
在这里插入图片描述我等待了很长时间后,切换了4百万次,其中百分之99都是main.c中的for(;; ) pause();造成的
在这里插入图片描述
后面想一想完全可以将pause()函数中的fprintk函数去掉,来保证完成速度,当时并没有意识到……

标签:轨迹,int,task,current,哈工大,time,进程,OS,tss
来源: https://blog.csdn.net/ychychych1/article/details/114257006

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有