实验4 进程运行轨迹的跟踪与统计
進程運行軌跡的跟蹤與統計
難度系數:★★★☆☆
實驗目的
- 掌握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進程調度的時間片,然后再運行同樣的樣本程序,統計同樣的時間數據,和原有的情況對比,體會不同時間片帶來的差異。
/var/process.log文件的格式必須為:
pid X time其中:
- pid是進程的ID;
- X可以是N,J,R,W和E中的任意一個,分別表示進程新建(N)、進入就緒態(J)、進入運行態(R)、進入阻塞態(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 ......實驗報告
完成實驗后,在實驗報告中回答如下問題:
- 結合自己的體會,談談從程序設計者的角度看,單進程編程和多進程編程最大的區別是什么?
- 你是如何修改時間片的?僅針對樣本程序建立的進程,在修改時間片前后,log文件的統計結果(不包括Graphic)都是什么樣?結合你的修改分析一下為什么會這樣變化,或者為什么沒變化?
評分標準
- process.c,50%
- 日志文件建立成功,5%
- 能向日志文件輸出信息,5%
- 5種狀態都能輸出,10%(每種2%)
- 調度算法修改,10%
- 實驗報告,20%
實驗提示
process.c的編寫涉及到fork()和wait()系統調用,請自行查閱相關文獻。0.11內核修改涉及到init/main.c、kernel/fork.c和kernel/sched.c,開始實驗前如果能詳細閱讀《注釋》一書的相關部分,會大有裨益。
- 編寫樣本程序
所謂樣本程序,就是一個生成各種進程的程序。我們的對0.11的修改把系統對它們的調度情況都記錄到log文件中。在修改調度算法或調度參數后再運行完全一樣的樣本程序,可以檢驗調度算法的優劣。理論上,此程序可以在任何Unix/Linux上運行,所以建議在Ubuntu上調試通過后,再拷貝到0.11下運行。
process.c是樣本程序的模板(在/home/teacher/目錄下)。它主要實現了一個函數:
/** 此函數按照參數占用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); 比如一個進程如果要占用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:cpuio_bound(10, 1, 9); // I/O時間是CPU時間的9倍修改此模板,用fork()建立若干個同時運行的子進程,父進程等待所有子進程退出后才退出,每個子進程按照你的意愿做不同或相同的cpuio_bound(),從而完成一個個性化的樣本程序。它可以用來檢驗有關log文件的修改是否正確,同時還是數據統計工作的基礎。
wait()系統調用可以讓父進程等待子進程的退出。
小技巧:
在Ubuntu下,top命令可以監視即時的進程狀態。在top中,按u,再輸入你的用戶名,可以限定只顯示以你的身份運行的進程,更方便觀察。按h可得到幫助。
在Ubuntu下,ps命令可以顯示當時各個進程的狀態。“ps aux”會顯示所有進程;“ps aux | grep xxxx”將只顯示名為xxxx的進程。更詳細的用法請問man。
在Linux 0.11下,按F1可以即時顯示當前所有進程的狀態。
- log文件
操作系統啟動后先要打開/var/process.log,然后在每個進程發生狀態切換的時候向log文件內寫入一條記錄,其過程和用戶態的應用程序沒什么兩樣。然而,因為內核狀態的存在,使過程中的很多細節變得完全不一樣。
打開log文件
為了能盡早開始記錄,應當在內核啟動時就打開log文件。內核的入口是init/main.c中的main()(Windows環境下是start()),其中一段代碼是:
…… move_to_user_mode(); if (!fork()) { /* we count on this going ok */init(); } ……這段代碼在進程0中運行,先切換到用戶模式,然后全系統第一次調用fork()建立進程1。進程1調用init()。在init()中:
…… setup((void *) &drive_info); //加載文件系統 (void) open("/dev/tty0",O_RDWR,0); //打開/dev/tty0,建立文件描述符0和/dev/tty0的關聯 (void) dup(0); //讓文件描述符1也和/dev/tty0關聯 (void) dup(0); //讓文件描述符2也和/dev/tty0關聯 ……這段代碼建立了文件描述符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); (void) open("/dev/tty0",O_RDWR,0); //建立文件描述符0和/dev/tty0的關聯 (void) dup(0); //文件描述符1也和/dev/tty0關聯 (void) dup(0); //文件描述符2也和/dev/tty0關聯 (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文件
log文件將被用來記錄進程的狀態轉移軌跡。所有的狀態轉移都是在內核進行的。在內核狀態下,write()功能失效,其原理等同于《系統調用》實驗中不能在內核狀態調用printf(),只能調用printk()。編寫可在內核調用的write()的難度較大,所以這里直接給出源碼。它主要參考了printk()和sys_write()而寫成的:
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);if (fd < 3) /* 如果輸出到stdout或stderr,直接調用sys_write即可 */{__asm__("push %%fs\n\t""push %%ds\n\t""pop %%fs\n\t""pushl %0\n\t""pushl $logbuf\n\t" /* 注意對于Windows環境來說,是_logbuf,下同 */"pushl %1\n\t""call sys_write\n\t" /* 注意對于Windows環境來說,是_sys_write,下同 */"addl $8,%%esp\n\t""popl %0\n\t""pop %%fs"::"r" (count),"r" (fd):"ax","cx","dx");}else /* 假定>=3的描述符都與文件關聯。事實上,還存在很多其它情況,這里并沒有考慮。*/{if (!(file=task[0]->filp[fd])) /* 從進程0的文件描述符表中得到文件句柄 */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(),唯一的區別是第一個參數是文件描述符,而不是文件指針。例如:
fprintk(1, "The ID of running process is %ld", current->pid); //向stdout打印正在運行的進程的ID fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'R', jiffies); //向log文件輸出跟蹤進程運行軌跡- jiffies,滴答
jiffies在kernel/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:……incl jiffies #增加jiffies計數值……這說明jiffies表示從開機時到現在發生的時鐘中斷次數,這個數也被稱為“滴答數”。
另外,在kernel/sched.c中的sched_init()中有下面的代碼:
outb_p(0x36, 0x43); //設置8253模式 outb_p(LATCH&0xff, 0x40); outb_p(LATCH8, 0x40);這三條語句用來設置每次時鐘中斷的間隔,即為LATCH,而LATCH是定義在文件kernel/sched.c中的一個宏:
#define LATCH (1193180/HZ)
#define HZ 100 //在include/linux/sched.h中
再加上PC機8253定時芯片的輸入時鐘頻率為1.193180MHz,即1193180/每秒,LATCH=1193180/100,時鐘每跳11931.8下產生一次時鐘中斷,即每1/100秒(10ms)產生一次時鐘中斷,所以jiffies實際上記錄了從開機以來共經過了多少個10ms。
- 尋找狀態切換點
必須找到所有發生進程狀態切換的代碼點,并在這些點添加適當的代碼,來輸出進程狀態變化的情況到log文件中。此處要面對的情況比較復雜,需要對kernel下的fork.c、sched.c有通盤的了解,而exit.c也會涉及到。我們給出兩個例子描述這個工作該如何做,其他情況實驗者可仿照完成。
第一個例子是看看如何記錄一個進程生命期的開始,當然這個事件就是進程的創建函數fork(),由《系統調用》實驗可知,fork()功能在內核中實現為sys_fork(),該“函數”在文件kernel/system_call.s中實現為:
sys_fork:call find_empty_process……push %gs //傳遞一些參數pushl %esipushl %edipushl %ebppushl %eaxcall copy_process //調用copy_process實現進程創建addl $20,%esp所以真正實現進程創建的函數是copy_process(),它在kernel/fork.c中定義為:
int copy_process(int nr,……) {struct task_struct *p;……p = (struct task_struct *) get_free_page(); //獲得一個task_struct結構體空間……p->pid = last_pid;……p->start_time = jiffies; //設置start_time為jiffies……p->state = TASK_RUNNING; //設置進程狀態為就緒。所有就緒進程的狀態都是//TASK_RUNNING(0),被全局變量current指向的//是正在運行的進程。return last_pid; }因此要完成進程運行軌跡的記錄就要在copy_process()中添加輸出語句。這里要輸出兩種狀態,分別是“N(新建)”和“J(就緒)”。
第二個例子是記錄進入睡眠態的時間。sleep_on()和interruptible_sleep_on()讓當前進程進入睡眠狀態,這兩個函數在kernel/sched.c文件中定義如下:
void sleep_on(struct task_struct **p) {struct task_struct *tmp;……tmp = *p;*p = current; //仔細閱讀,實際上是將current插入“等待隊列”頭部,tmp是原來的頭部current->state = TASK_UNINTERRUPTIBLE; //切換到睡眠態schedule(); //讓出CPUif (tmp)tmp->state=0; //喚醒隊列中的上一個(tmp)睡眠進程。0換作TASK_RUNNING更好//在記錄進程被喚醒時一定要考慮到這種情況,實驗者一定要注意!!! } /* 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();if (*p && *p != current) { //如果隊列頭進程和剛喚醒的進程current不是一個,說明從隊列中間喚醒了一個進程,需要處理(**p).state=0; //將隊列頭喚醒,并通過goto repeat讓自己再去睡眠goto repeat;}*p=NULL;if (tmp)tmp->state=0; //作用和sleep_on函數中的一樣 }相信實驗者已經找到合適的地方插入記錄進程從運行到睡眠的語句了。
總的來說,Linux 0.11支持四種進程狀態的轉移:就緒到運行、運行到就緒、運行到睡眠和睡眠到就緒,此外還有新建和退出兩種情況。其中就緒與運行間的狀態轉移是通過schedule()(它亦是調度算法所在)完成的;運行到睡眠依靠的是sleep_on()和interruptible_sleep_on(),還有進程主動睡覺的系統調用sys_pause()和sys_waitpid();睡眠到就緒的轉移依靠的是wake_up()。所以只要在這些函數的適當位置插入適當的處理語句就能完成進程運行軌跡的全面跟蹤了。
-
為了讓生成的log文件更精準,以下幾點請注意:
-
進程退出的最后一步是通知父進程自己的退出,目的是喚醒正在等待此事件的父進程。從時序上來說,應該是子進程先退出,父進程才醒來。
- schedule()找到的next進程是接下來要運行的進程(注意,一定要分析清楚next是什么)。如果next恰好是當前正處于運行態的進程,swith_to(next)也會被調用。這種情況下相當于當前進程的狀態沒變。
-
系統無事可做的時候,進程0會不停地調用sys_pause(),以激活調度算法。此時它的狀態可以是等待態,等待有其它可運行的進程;也可以叫運行態,因為它是唯一一個在CPU上運行的進程,只不過運行的效果是等待。
-
管理log文件
日志文件的管理與代碼編寫無關,有幾個要點要注意:
- 每次關閉bochs前都要執行一下“sync”命令,它會刷新cache,確保文件確實寫入了磁盤。
- 在0.11下,可以用“ls -l /var”或“ll /var”查看process.log是否建立,及它的屬性和長度。
- 一定要實踐《實驗環境的搭建與使用》一章中關于文件交換的部分。最終肯定要把process.log文件拷貝到主機環境下處理。
- 在0.11下,可以用“vi /var/process.log”或“more /var/process.log”查看整個log文件。不過,還是拷貝到Ubuntu下看,會更舒服。
- 在0.11下,可以用“tail -n NUM /var/process.log”查看log文件的最后NUM行。
一種可能的情況下,得到的process.log文件的前幾行是:
1 N 48 //進程1新建(init())。此前是進程0建立和運行,但為什么沒出現在log文件里? 1 J 49 //新建后進入就緒隊列 0 J 49 //進程0從運行->就緒,讓出CPU 1 R 49 //進程1運行 2 N 49 //進程1建立進程2。2會運行/etc/rc腳本,然后退出 2 J 49 1 W 49 //進程1開始等待(等待進程2退出) 2 R 49 //進程2運行 3 N 64 //進程2建立進程3。3是/bin/sh建立的運行腳本的子進程 3 J 64 2 E 68 //進程2不等進程3退出,就先走一步了 1 J 68 //進程1此前在等待進程2退出,被阻塞。進程2退出后,重新進入就緒隊列 1 R 68 4 N 69 //進程1建立進程4,即shell 4 J 69 1 W 69 //進程1等待shell退出(除非執行exit命令,否則shell不會退出) 3 R 69 //進程3開始運行 3 W 75 4 R 75 5 N 107 //進程5是shell建立的不知道做什么的進程 5 J 108 4 W 108 5 R 108 4 J 110 5 E 111 //進程5很快退出 4 R 111 4 W 116 //shell等待用戶輸入命令。 0 R 116 //因為無事可做,所以進程0重出江湖 4 J 239 //用戶輸入命令了,喚醒了shell 4 R 239 4 W 240 0 R 240 ……- 數據統計
為展示實驗結果,需要編寫一個數據統計程序,它從log文件讀入原始數據,然后計算平均周轉時間、平均等待時間和吞吐率。任何語言都可以編寫這樣的程序,實驗者可自行設計。我們用python語言編寫了一個——stat_log.py(這是python源程序,可以用任意文本編輯器打開)。
python是一種跨平臺的腳本語言,號稱“可執行的偽代碼”,非常強大,非常好用,也非常有用,建議閑著的時候學習一下。其解釋器免費且開源,Ubuntu下這樣安裝:
sudo apt-get install python
然后只要給stat_log.py加上執行權限(chmod +x stat_log.py)就可以直接運行它。
此程序必須在命令行下加參數執行,直接運行會打印使用說明。
Usage:./stat_log.py /path/to/process.log [PID1] [PID2] ... [-x PID1 [PID2] ... ] [-m] [-g] Example:# Include process 6, 7, 8 and 9 in statistics only. (Unit: tick)./stat_log.py /path/to/process.log 6 7 8 9# Exclude process 0 and 1 from statistics. (Unit: tick)./stat_log.py /path/to/process.log -x 0 1# Include process 6 and 7 only. (Unit: millisecond)./stat_log.py /path/to/process.log 6 7 -m# Include all processes and print a COOL "graphic"! (Unit: tick)./stat_log.py /path/to/process.log -g運行“./stat_log.py process.log 0 1 2 3 4 5 -g”(只統計PID為0、1、2、3、4和5的進程)的輸出示例:
(Unit: tick) Process Turnaround Waiting CPU Burst I/O Burst0 75 67 8 01 2518 0 1 25172 25 4 21 03 3003 0 4 29994 5317 6 51 52605 3 0 3 0 Average: 1823.50 12.83 Throughout: 0.11/s -----===< COOL GRAPHIC OF SCHEDULER >===-----[Symbol] [Meaning]~~~~~~~~~~~~~~~~~~~~~~~~~~~number PID or tick"-" New or Exit "#" Running"|" Ready":" Waiting/ Running with "+" -| Ready \and/or Waiting-----===< !!!!!!!!!!!!!!!!!!!!!!!!! >===-----40 -0 41 #0 42 # 43 # 44 # 45 # 46 # 47 # 48 |0 -1 49 | :1 -2 50 | : #2 51 | : # 52 | : # 53 | : # 54 | : # 55 | : # 56 | : # 57 | : # 58 | : # 59 | : # 60 | : # 61 | : # 62 | : # 63 | : # 64 | : |2 -3 65 | : | #3 66 | : | # 67 | : | # …………小技巧:如果命令行程序輸出過多,可以用“command arguments | more”的方式運行,結果會一屏一屏地顯示。“more”在Linux和Windows下都有。Linux下還有一個“less”,和“more”類似,但功能更強,可以上下翻頁、搜索。
- 修改時間片
下面是0.11的調度函數schedule,在文件kernel/sched.c中定義為:
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;} //找到counter值最大的就緒態進程if (c) break; //如果有counter值大于0的就緒態進程,則退出for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; //如果沒有,所有進程的counter值除以2衰減后再和priority值相加,產生新的時間片 } switch_to(next); //切換到next進程分析代碼可知,0.11的調度算法是選取counter值最大的就緒進程進行調度。其中運行態進程(即current)的counter數值會隨著時鐘中斷而不斷減1(時鐘中斷10ms一次),所以是一種比較典型的時間片輪轉調度算法。另外,由上面的程序可以看出,當沒有counter值大于0的就緒進程時,要對所有的進程做“(p)->counter = ((p)->counter >> 1) + (*p)->priority”。其效果是對所有的進程(包括阻塞態進程)都進行counter的衰減,并再累加priority值。這樣,對正被阻塞的進程來說,一個進程在阻塞隊列中停留的時間越長,其優先級越大,被分配的時間片也就會越大。所以總的來說,Linux 0.11的進程調度是一種綜合考慮進程優先級并能動態反饋調整時間片的輪轉調度算法。
此處要求實驗者對現有的調度算法進行時間片大小的修改,并進行實驗驗證。
為完成此工作,我們需要知道兩件事情:
- 進程counter是如何初始化的
- 當進程的時間片用完時,被重新賦成何值?
首先回答第一個問題,顯然這個值是在fork()中設定的。Linux 0.11的fork()會調用copy_process()來完成從父進程信息拷貝(所以才稱其為fork),看看copy_process()的實現(也在kernel/fork.c文件中),會發現其中有下面兩條語句:
*p = *current; //用來復制父進程的PCB數據信息,包括priority和counter p->counter = p->priority; //初始化counter 因為父進程的counter數值已發生變化,而priority不會,所以上面的第二句代碼將p->counter設置成p->priority。每個進程的priority都是繼承自父親進程的,除非它自己改變優先級。查找所有的代碼,只有一個地方修改過priority,那就是nice系統調用:int sys_nice(long increment) {if (current->priority-increment>0)current->priority -= increment; return 0; }本實驗假定沒有人調用過nice系統調用,時間片的初值就是進程0的priority,即宏INIT_TASK中定義的:
接下來回答第二個問題,當就緒進程的counter為0時,不會被調度(schedule要選取counter最大的,大于0的進程),而當所有的就緒態進程的counter都變成0時,會執行下面的語句:
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;顯然算出的新的counter值也等于priority,即初始時間片的大小。
提示就到這里。如何修改時間片,自己思考、嘗試吧,
=========================================================
實驗報告
一,實驗具體步驟:
1,按照提示修改~/oslab/linux-0.11/init/main.c,注意是移動,不是復制;
2,在kernel/printk.c同一目錄下,建立fprint.c文件,內容在提示的基礎代碼上加上#include "stdarg.h"這一行,因為va_list args數據結構的定義在這個文件里面;
3,修改~/oslab/linux-0.11/kernel/Makefile文件:
... OBJS = sched.o system_call.o traps.o asm.o fork.o \panic.o printk.o vsprintf.o sys.o exit.o \signal.o mktime.o fprintk.o ...... ### Dependencies: fprintk.s fprintk.o: fprintk.c ../include/linux/sched.h ../include/sys/stat.h exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \ ...4,修改kernel/fork.c: ...p->utime = p->stime = 0;p->cutime = p->cstime = 0;p->start_time = jiffies;fprintk(3,"%ld\t%c\t%ld\n",last_pid,'N',jiffies); p->tss.back_link = 0;p->tss.esp0 = PAGE_SIZE + (long) p;p->tss.ss0 = 0x10;p->tss.eip = eip; ...... 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",last_pid,'J',jiffies);return last_pid; } ...
5,修改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);}} ......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(current->state == TASK_RUNNING && current != task[next]) {/*輸出就緒的Log*/ fprintk(3,"%ld\t%c\t%ld\n",current->pid,'J',jiffies); }if(current != task[next]) {/*輸出可運行的Log*/ fprintk(3,"%ld\t%c\t%ld\n",task[next]->pid,'R',jiffies); }switch_to(next); } ...... int sys_pause(void) {current->state = TASK_INTERRUPTIBLE;if (current->pid != 0) fprintk(3,"%ld\t%c\t%ld\n",current->pid,'W',jiffies); schedule();return 0; } ...... 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;if (current->pid != 0) fprintk(3,"%ld\t%c\t%ld\n",current->pid,'W',jiffies); schedule();if (tmp)tmp->state=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;if (current->pid != 0) 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);} } ...... void wake_up(struct task_struct **p) {if (p && *p) {if((**p).state != TASK_RUNNING){ fprintk(3,"%ld\t%c\t%ld\n",(**p).pid,'J',jiffies); (**p).state=TASK_RUNNING; }} } ...
6,修改kernel/exit.c: ...case TASK_ZOMBIE:current->cutime += (*p)->utime;current->cstime += (*p)->stime;flag = (*p)->pid;code = (*p)->exit_code;fprintk(3,"%ld\t%c\t%ld\n",flag,'E',jiffies);release(*p);put_fs_long(code,stat_addr);return flag;default:flag=1;continue;}}if (flag) {if (options & WNOHANG)return 0;current->state=TASK_INTERRUPTIBLE;if (current->pid!=0){ fprintk(3,"%ld\t%c\t%ld\n",current->pid,'W',jiffies);}schedule();if (!(current->signal &= ~(1<<(SIGCHLD-1))))goto repeat; ...
7,把process.c,stat_log.py放到hdc/var中;
8,在Linux-0.11下編譯運行process.c,然后sync將process.log寫入硬盤;
9,推出linux-0.11,在hdc/var下統計process.log內容:
./stat_log.py process.log 0 1 2 3 4 5 -g10,實驗截圖:
11,實驗體會
如同實驗提示中所說“必須找到所有發生進程狀態切換的代碼點,并在這些點添加適當的代碼,來輸出進程狀態變化的情況到log文件中。此處要面對的情況比較復雜,需要對kernel下的fork.c、sched.c有通盤的了解,而exit.c也會涉及到”,所以要完全理解這個實驗的關鍵就是讀懂上面的三個.c文件,但是對我來說感覺難度還是很大,上面的結果是部分參考了老師的答案,有些甚至不理解,實驗的第二部分也沒有做,打算在通篇完成8個實驗后再回頭理解這些細節。
總結
以上是生活随笔為你收集整理的实验4 进程运行轨迹的跟踪与统计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 随机字符串_python生
- 下一篇: 基于空间方法的图神经网络模型_用于时空图