實驗內容
- 基於模板 process.c 編寫多進程的樣本程序,實現如下功能: + 所有子進程都並行運行,每個子進程的實際運行時間一般不超過 30 秒; + 父進程向標准輸出打印所有子進程的 id,並在所有子進程都退出后才退出。
- 在 Linux0.11 上實現進程運行軌跡的跟蹤。 + 基本任務是在內核中維護一個日志文件 /var/process.log,把從操作系統啟動到系統關機過程中所有進程的運行軌跡都記錄在這一 log 文件中。
- 在修改過的 0.11 上運行樣本程序,通過分析 log 文件,統計該程序建立的所有進程的等待時間、完成時間(周轉時間)和運行時間,然后計算平均等待時間,平均完成時間和吞吐量。可以自己編寫統計程序,也可以使用 python 腳本程序—— stat_log.py(在 /home/teacher/ 目錄下) ——進行統計。
- 修改 0.11 進程調度的時間片,然后再運行同樣的樣本程序,統計同樣的時間數據,和原有的情況對比,體會不同時間片帶來的差異。
- 結合自己的體會,談談從程序設計者的角度看,單進程編程和多進程編程最大的區別是什么?
- 你是如何修改時間片的?僅針對樣本程序建立的進程,在修改時間片前后,log 文件的統計結果(不包括 Graphic)都是什么樣?結合你的修改分析一下為什么會這樣變化,或者為什么沒變化?
步驟
1.修改process.c文件
實驗樓在teacher文件夾內提供了process.c文件的模板,另外哈工大git上也有這個文件,在對其進行修改的過程中主要是在main函數內增加一些語句,用 fork() 建立若干個同時運行的子進程,父進程等待所有子進程退出后才退出,每個子進程各自執行 cpuio_bound(),從而實現樣本程序。下面貼出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[])
{
pid_t n_proc[10]; /*10個子進程 PID*/
int i;
for(i=0;i<10;i++)
{
n_proc[i] = fork();
/*子進程*/
if(n_proc[i] == 0)
{
cpuio_bound(20,2*i,20-2*i); /*每個子進程都占用20s*/
return 0; /*執行完cpuio_bound 以后,結束該子進程*/
}
/*fork 失敗*/
else if(n_proc[i] < 0 )
{
printf("Failed to fork child process %d!\n",i+1);
return -1;
}
/*父進程繼續fork*/
}
/*打印所有子進程PID*/
for(i=0;i<10;i++)
printf("Child PID: %d\n",n_proc[i]);
/*等待所有子進程完成*/
wait(&i); /*Linux 0.11 上 gcc要求必須有一個參數, gcc3.4+則不需要*/
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(¤t_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;
}
}
2.修改main.c文件
修改main.c的作用是使得操作系統在啟動時就打開log文件,main.c文件在init目錄下
move_to_user_mode();
/***************自定義代碼塊--開始***************/
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
(void) open("/var/process.log",O_CREAT|O_TRUNC|O_WRONLY,0666);
/***************自定義代碼塊--結束***************/
if (!fork()) { /* we count on this going ok */
init();
}
3.修改printk.c文件
系統在內核狀態下只能使用printk函數,下面對printk增加了fprintk函數:(文件位置kernel/printk.c)
#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;
}
4.修改fork.c文件
fork.c文件在kernel目錄下,下面做出兩處修改:
int copy_process(int nr,……)
{
struct task_struct *p;
// ……
// 獲得一個 task_struct 結構體空間
p = (struct task_struct *) get_free_page();
// ……
p->pid = last_pid;
// ……
// 設置 start_time 為 jiffies
p->start_time = jiffies;
//新增修改
fprintk(3,"%d\tN\t%d\n",p->pid,jiffies);
// ……
/* 設置進程狀態為就緒。所有就緒進程的狀態都是
TASK_RUNNING(0),被全局變量 current 指向的
是正在運行的進程。*/
p->state = TASK_RUNNING;
//新增修改
fprintk(3,"%d\tJ\t%d\n",p->pid,jiffies);
return last_pid;
}
5.修改sched.c文件
文件位置:kernel/sched.c,下面做出兩處修改:
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
// ……
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
{
(*p)->state=TASK_RUNNING;
/*新建修改--可中斷睡眠 => 就緒*/
fprintk(3,"%d\tJ\t%d\n",(*p)->pid,jiffies);
}
// ……
/*編號為next的進程 運行*/
if(current->pid != task[next] ->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);
}
1.修改sys_pause函數
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
/*
*修改--當前進程 運行 => 可中斷睡眠
*/
if(current->pid != 0)
fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
return 0;
}
2.修改sleep_on函數
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,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
if (tmp)
{
tmp->state=0;
/*
*修改--原等待隊列 第一個進程 => 喚醒(就緒)
*/
fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);
}
}
3.修改interruptible_sleep_on函數
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;
/*
*修改--喚醒隊列中間進程,過程中使用Wait
*/
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);
}
}
4.修改wake_up函數
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;
}
}
6.修改exit.c文件
此文件的位置在kernel目錄下,修改了兩處位置,如下:
int do_exit(long code)
{
int i;
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
// ……
current->state = TASK_ZOMBIE;
/*
*修改--退出一個進程
*/
fprintk(3,"%d\tE\t%d\n",current->pid,jiffies);
current->exit_code = code;
tell_father(current->father);
schedule();
return (-1); /* just to suppress warnings */
}
// ……
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,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
if (!(current->signal &= ~(1<<(SIGCHLD-1))))
goto repeat;
else
return -EINTR;
}
return -ECHILD;
}
7.make
上述步驟中已經修改了所有的必要文件,直接執行make命令編譯內核即可
8.編譯運行process.c
將process.c拷貝到linux0.11系統中,這個過程需要掛載一下系統硬盤,掛載拷貝成功之后再卸載硬盤,然后啟動模擬器進入系統內編譯一下process.c文件,過程命令及截圖如下:
sudo ./mount-hdc
cp ./process.c ./hdc/usr/root/
sudo umonut hdc
./run
gcc -o process process.c
編譯process.c的過程如下:

使用./process即可運行目標文件,運行后會生成log文件,生成log文件后將其拷貝到oslab根目錄,命令如下:
sudo ./mount-hdc
cp ./hdc/var/process.log ./
sudo umonut hdc
9.process.log自動化分析
由於默認的python腳本是使用的python2環境,我在Ubuntu上安裝的是python3環境,所以對python腳本大概修改了下,直接把print命令更改下,然后有一處的異常處理將逗號更改為as即可,截圖如下:

修改了python腳本並確定可以執行之后,使用如下命令執行自動化分析:
./stat_log.py process.log 0 1 2 3 4 5 -g
分析結果如下:

10.修改時間片
通過分析實驗樓給出的schedule調度函數可以知道0.11 的調度算法是選取 counter 值最大的就緒進程進行調度。函數代碼如下:
while (1) {
c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS];
// 找到 counter 值最大的就緒態進程
while (--i) {
if (!*--p) continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
// 如果有 counter 值大於 0 的就緒態進程,則退出
if (c) break;
// 如果沒有:
// 所有進程的 counter 值除以 2 衰減后再和 priority 值相加,
// 產生新的時間片
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
}
// 切換到 next 進程
switch_to(next);
找到時間片的定義:
#define INIT_TASK \
{ 0,15,15,
// 上述三個值分別對應 state、counter 和 priority;
據此注釋可以修改時間片
1.時間片為10
重復process.log自動化分析步驟得出如下結果:
Process Turnaround Waiting CPU Burst I/O Burst
7 2298 97 0 2200
8 2319 1687 200 432
9 2368 2098 270 0
10 2358 2087 270 0
11 2347 2076 270 0
12 2336 2066 270 0
13 2326 2055 270 0
14 2315 2044 270 0
15 2304 2034 270 0
16 2292 2021 270 0
Average: 2326.30 1826.50
Throughout: 0.41/s
2.時間片為15
重復process.log自動化分析步驟得出如下結果:
Process Turnaround Waiting CPU Burst I/O Burst
7 2247 142 0 2105
8 2202 1686 200 315
9 2246 1991 255 0
10 2230 1975 255 0
11 2215 1959 255 0
12 2199 1944 255 0
13 2183 1928 255 0
14 2168 1912 255 0
15 2152 1897 255 0
16 2137 1881 255 0
Average: 2197.90 1731.50
Throughout: 0.44/s
3.時間片為20
重復process.log自動化分析步驟得出如下結果:
Process Turnaround Waiting CPU Burst I/O Burst
7 2587 187 0 2400
8 2567 1766 200 600
9 2608 2308 300 0
10 2585 2285 300 0
11 2565 2264 300 0
12 2544 2244 300 0
13 2523 2223 300 0
14 2503 2202 300 0
15 2482 2182 300 0
16 2461 2161 300 0
Average: 2542.50 1982.20
Throughout: 0.38/s
問題回答
問題一
單進程編程較於多進程編程要更簡單,利用率低,因為單進程是順序執行的,而多進程編程是同步執行的,需要復雜且靈活的調度算法,充分利用CPU資源,所以情況要復雜得多。在設計多進程編程時,要考慮資源的分配,時間片的分配等達到系統調度的平衡。要綜合考慮所有進程的情況以達到最優的並行執行效果。且多進程編程的功能更為強大,且應用范圍較於單進程編程更加廣泛。
問題二
- 將時間片變小,進程調度次數變多,系統會使得該進程等待時間變長。
- 將時間片增大,進程因中斷/睡眠而產生的調度次數也增多,等待時間也會變長。
- 總結:時間片要設置合理,不能過大或者過小。
