四:進程運行軌跡的跟蹤與統計

實驗目的

  • 掌握 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

在這裏插入圖片描述
可以發現,等待狀態和佔用時間都變大了。

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