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

实验目的

  • 掌握 Linux 下的多进程编程技术;
  • 通过对进程运行轨迹的跟踪来形象化进程的概念;
  • 在进程运行轨迹跟踪的基础上进行相应的数据统计,从而能对进程调度算法进行实际的量化评价,更进一步加深对调度和调度算法的理解,获得能在实际操作系统上对调度算法进行实验数据对比的直接经验

实验内容

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

本次实验包括如下内容:

  • 基于模板 process.c编写多进程的样本程序,实现如下功能:
    • 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒;
    • 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;
  • 在 Linux0.11 上实现进程运行轨迹的跟踪。
    • 基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。
  • 在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序,也可以使用 python 脚本程序—— stat_log.py(在 /home/teacher/ 目录下) ——进行统计。
  • 修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。

实验步骤

打开log文件

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

预备:Linux 的进程初始化

之前的故事:
boot/目录中,引导程序把内核从磁盘加载到内存中,并让系统进入保护模式下运行后进入系统初始化程序init/main.c 该程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断处理、块设备、和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理。
Now
在完成了这些操作之后,系统各部分已经处于可运行状态。此后程序把自己“手工”移动到 进程0 中运行,并使用fork()创建出进程1。在进程1中程序将继续进行应用环境的初始化并执行shell登陆程序。而原进程0则会在系统空闲时被调度执行此时进程0仅执行pause() 系统调用,其中又会去执行调度函数。

打开 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);

// ……

这段代码建立了文件描述符012,它们分别就是stdinstdoutstderr。这三者的值是系统标准(Windows 也是如此),不可改变。

内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。

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

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

//……
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 文件的参数的含义是建立只写文件,如果文件已存在则清空已有内容。文件的权限是所有人可读可写。

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

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

建议将此函数放入到 kernel/printk.c 中。
使用方式:

// 向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:
先简单说,jiffies 实际上记录了从开机以来共经过了多少个 10ms,修改时间片时会提到。

寻找状态切换点

预备:Linux 的进程运行状态

进程状态保存在任务数据结构(task_struct 即为一个ADT:抽象数据类型)中的state字段

struct task_struct {
/* these are hardcoded - don't touch */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	long signal;
	//... 中间略,就是说明下大概什么样子
	struct desc_struct ldt[3];
/* tss for this task */
	struct tss_struct tss;
};

state字段在各个状态宏中的值即为下图中圆形内的标号

#define TASK_RUNNING		0
#define TASK_INTERRUPTIBLE	1
#define TASK_UNINTERRUPTIBLE	2
#define TASK_ZOMBIE		3
#define TASK_STOPPED		4

在这里插入图片描述

Linux 中的五种进程状态:

  • 运行状态(TASK_RUNNING)

    从资源占用角度:

    • 此时正在占用CPU,称为运行态(running)
    • 此时没被CPU运行,称其就绪态

    从权限角度:

    • 进程在内核代码中运行,称其为内核态
    • 进程在执行用户自己的代码时,称其为用户态

    以上四种状态在内核中表示方法相同,都处于TASK_RUNNING状态(== 0)。在上图中即为中间一列。

  • 可中断睡眠状态(TASK_INTERRUPTIBLE)
    该进程处于阻塞状态时,可由系统产生了某个中断或者释放了进程等待的资源,或收到了一个信号,都可以转换到就绪态。

  • 不可中断睡眠状态(TASK_UNINTERRUPTIBLE)
    不会因为收到了信号而被唤醒,只有使用了wake_up()函数明确唤醒时才能转换到就绪态。

  • 暂停状态(TASK_STOPPED)
    进程收到信号SIGSTOP,SIGTSTP,SIGTTINSIGTTOU时就会进入暂停状态,向其发送SIGCONT可转换到就绪态,在调试期间接收到任何信号都会进入该状态。但在 Linux 0.11中,还未实现对该状态的转换处理,处于该状态的进程将被作为进程终止来处理。

  • 僵死状态(TASK_ZOMBIE)
    进程已停止运行,父进程没有调用wait()询问其状态时,称进程处于僵死状态。为了让父进程能够获取停止运行的信息,此时子进程的任务数据结构(task_struct)还需要保留着。一旦父进程调用wait()取得了子进程的信息,处于该状态进程的任务数据结构就会被释放。

进程的新建(N)及由新建(N)切换到就绪(J)状态

新建进程是由fork()实现的,内核中为sys_fork()位于 kernel/system_call.s:208

sys_fork:
	call find_empty_process //获取系统中一个可用的pid
	testl %eax,%eax
	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: 68

我们发现,创建一个新的子进程和加载运行一个执行程序文件是两个不同的概念,创建子进程时,完全复制了父进程的代码段和数据段以及环境。

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;
	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;
	/**********************此处修改**********************/
	//创建新的进程,准备资源,新建态
	fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'N', 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 */
	/**********************此处修改**********************/
	//资源准备完毕,进程为就绪态
	fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'J', jiffies);
	/**********************结束**********************/
	return last_pid;
}
进程的就绪(J)状态和运行(R)状态之间的切换

根据前文预备知识所述,在linux-0.11的内核代码中是不区分就绪状态和运行状态的,二者都是TASK_RUNNING。 所以代码中并没有显示地对这两种状态切换的代码。
实现进程切换的代码是switch_to(int)(kernel/sched.c: 141)函数。
在切换进程之前的代码实现了进程调度的算法(schedule()),这些将在下文中讨论。进程调度算法按照一定的策略找到下个执行的进程的ID(next),**使用switch_to函数将cpu的使用权交给next号进程。
需要注意的是,next号进程可能就是当前正在执行的进程,所以首先要判断next进程是否是当前进程
只有当二者不相等时,才真正地发生了进程的调度。
next号进程就由就绪状态切换到运行状态。
而如果当前进程是运行状态,则其状态需要切换到就绪态。

//kernel/sched.c:141
//.....
/**********************此处修改**********************/
	if (task[next] != current) { //判断next是否为当前进程
        if (current->state == TASK_RUNNING) {
            fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'J', jiffies); //如果current进程是就绪态,说明它被next抢占了,记录从运行态到就绪态的过程
        }
        fprintk(3, "%ld\t%c\t%ld\n", task[next]->pid, 'R', jiffies);//next进程将被切换到运行态
    }
/**********************修改结束**********************/
	switch_to(next);
}
预备:睡眠函数 sleep_on() 和唤醒函数 wake_up()

主要功能是:

  • 当一个进程所请求的资源正忙或不再内存中时暂时切换出去,放在等待队列中等待一段时间。当切换回来后再继续运行。

  • 放入等待队列的方式是利用了函数中的tmp指针作为各个正在等待任务的联系。
    函数中共涉及到三个任务指针操作:*p,tmpcurrent

    • *p是等待队列头指针
    • tmp是在函数堆栈上建立的临时指针,存储在当前任务内核态堆栈上;
    • current是当前任务指针。

    在这里插入图片描述

在刚进入该函数时,队列头指针*p指向已经在等待队列中等待的任务结构。
在系统刚开始执行时,等待队列上无等待任务:

  • 开始*p指向NULL
  • 调用调度程序之前,*p指向了当前的任务结构,tmp指向了原等待任务。
  • 在执行调度程序并在本任务被唤醒重新返回执行之前,当前任务指针被指向新加入的等待任务,即调用本函数的任务。

通过tmp的链接作用,在几个进程为等待同一资源而多次调用该函数时,内核程序就隐式构筑成一个的等待队列

在这里插入图片描述
interruptible_sleep_onsleep_on的功能基本类似。
只是在进行调度之前,把当前任务设置成可中断等待状态,并在本任务被唤醒后还需要判断自己是否为 等待队列中的第一个任务。*
如果不是,则需要将自己重新设置成可中断等待状态,并执行schedule()函数,让出CPU的执行权。

进程由运行(R)状态切换到阻塞(W)状态

当一个进程需要等待一个外部事件时,就需要将自身的状态由运行态切换到阻塞态。

//kernel/sched.c: 151
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, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
	/**********************修改结束**********************/
	schedule();
	if (tmp){ //后进先出原则,让原先的等待任务先就绪态
		tmp->state=0;
		/**********************此处修改**********************/
		fprintk(3, "%ld\t%c\t%ld\n", tmp->pid, 'J', jiffies);
		/**********************修改结束**********************/
	}
}

//kernel/sched.c: 167
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, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
	/**********************修改结束**********************/
	schedule();
	if (*p && *p != current) {
		(**p).state=0;
		/**********************此处修改**********************/
		fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);
		/**********************修改结束**********************/
		goto repeat;
	}
	*p=NULL;
	if (tmp){
		tmp->state=0;
		/**********************此处修改**********************/
		fprintk(3, "%ld\t%c\t%ld\n", tmp->pid, 'J', jiffies);
		/**********************修改结束**********************/
	}
}

除了上述的2个函数可以实现将进程从执行状态切换到阻塞状态,sys_pause()(kernel/sched.c: 144)sys_waitpid(kernel/exit.c: 142)也可以做到。
schedule函数发现系统中没有可执行的任务时,就会切换到进程0而进程0就会循环执行sys_pause将自己设置为可中断的阻塞态,所以在sys_pause()中不应该记录进程0.。
理论上,该系统调用将导致进程进入睡眠状态,直到收到一个用于终止进程或者促使进程调用的信号量。然而这些功能在0.11版本内核中没有没实现。

//kernel/sched.c: 144
int sys_pause(void)
{
	current->state = TASK_INTERRUPTIBLE;
	/**********************此处修改**********************/
	if (current->state != TASK_INTERRUPTIBLE && current->pid != 0)
		fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
	/**********************修改结束**********************/
	schedule();
	return 0;
}
进程由阻塞(W)状态切换到就绪(J)状态

将进程由阻塞态切换到就绪态的函数,除了之前小节中提到的sleep_oninterruptible_sleep_on函数,wake_upschedule函数也能完成该切换。
wake_up函数直接将等待队列中的第一个任务唤醒:

//kernel/sched.c:188
void wake_up(struct task_struct **p)
{
	if (p && *p) {
		(**p).state=0;
		/**********************此处修改**********************/
		fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);
		/**********************修改结束**********************/
		*p=NULL;
	}
}

而在schedule函数中,首先会检测进程的报警定时值(alarm),并将处于可中断状态且接收到信号的进程唤醒。

// kernel/sched.c
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, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies);
				/**********************修改结束**********************/

			}

		}
//kernel/sched.c:130
//...
}

附上schedule()的完整代码:

// kernel/sched.c
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, "%ld\t%c\t%ld\n", (*p)->pid, 'J', 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;
	}
/**********************此处修改**********************/
	if (task[next] != current) { //判断next是否为当前进程
        if (current->state == TASK_RUNNING) {
            fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'J', jiffies); //如果current进程是就绪态,说明它被next抢占了,记录从运行态到就绪态的过程
        }
        fprintk(3, "%ld\t%c\t%ld\n", task[next]->pid, 'R', jiffies);//next进程将被切换到运行态
    }
/**********************修改结束**********************/
	switch_to(next);
}
进程的退出(E)

进程退出状态的设置是在do_exit(kernel/exit.c: 102)中实现的。
该函数释放当前进程的代码段和数据段所占的内存页,执行与文件系统相关的清理工作,最后将进程状态设置为僵死状态,并给退出码赋值。

//do_exit(kernel/exit.c: 102)
int do_exit(long code)
{
	//...
		tty_table[current->tty].pgrp = 0;
	if (last_task_used_math == current)
		last_task_used_math = NULL;
	if (current->leader)
		kill_session();
	current->state = TASK_ZOMBIE;
	current->exit_code = code;
	/**********************此处修改**********************/
	fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'E', jiffies);
	/**********************修改结束**********************/
	tell_father(current->father);
	schedule();
	return (-1);	/* just to suppress warnings */
}

sys_waitpid该函数主要用来等待子进程结束,函数中将当前进程状态修改为可中断等待,然后退出,所以只需要记录进程状态修改为等待即可。

//kernel/exit.c:144
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,"%ld\t%c\t%ld\n",current->pid,'W',jiffies);
		/**********************修改结束**********************/
		schedule();
		if (!(current->signal &= ~(1<<(SIGCHLD-1))))
			goto repeat;
		else
			return -EINTR;
	}
	return -ECHILD;
}

数据统计

首先是process.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[])
{
    int pid, stat_addr;
    if (! (pid = fork())) {
        cpuio_bound(30, 1, 1);
    } else {
        printf("fork %d.\n", pid);
        if (! (pid = fork())) {
            cpuio_bound(30, 30, 0);
        } else {
            printf("fork %d.\n", pid);
            if (! (pid = fork())) {
                cpuio_bound(30, 0, 30);
            } else {
                printf("fork %d.\n", pid);
                if (! (pid = fork())) {
                    cpuio_bound(30, 9, 1);
                } else {
                    printf("fork %d.\n", pid);
                    if (! (pid = fork())) {
                        cpuio_bound(30, 1, 9);
                    } else {
                        printf("fork %d.\n", pid);
                    }
                }
            }
        }
    }
    wait(&stat_addr);
    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;
	}
}

挂载hdc提取出process.log文件,在同一目录下运行提供的stat_log.py程序
只统计 PID 为 0,1,2,3,4,5,6 的进程

修改系统的调度时间片

PC机 8253定时芯片的输入时钟频率为1.193180MHz,即1193180/每秒LATCH=1193180/100,时钟每跳11931.8下产生一次时钟中断,即每1/100秒(10ms)产生一次时钟中断所以jiffies实际上记录的滴答数就表示从开机以来共经过了多少个10ms。
  而进程调度的时间片大小其实就是每次时钟中断的时间间隔, 所以只要修改宏HZ就可以了. 如: 将HZ修改为200就表示把进程调度的时间片大小修改为20ms

//include/linux/sched.h:5
#define HZ 200 //将100改为200

在这里插入图片描述
可以发现,等待状态和占用时间都变大了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章