在做這次實驗的時候,一定要耐心一點。我當時做的時候就因爲在修改的過程中出現了一點小小的失誤,導致了整個實驗重新來過一遍。爲了避免這樣的情況發生,一定要記住修改的時候,要注意修改的內容無誤。
本次實驗包括如下三個方面的內容:
-
基於模板“process.c”編寫多進程的樣本程序,實現如下功能:
-
所有子進程都並行運行,每個子進程的實際運行時間一般不超過30秒;
-
父進程向標準輸出打印所有子進程的id,並在所有子進程都退出後才退出;
-
-
在Linux0.11上實現進程運行軌跡的跟蹤。基本任務是在內核中維護一個日誌文件/var/process.log,把從操作系統啓動到系統關機過程中所有進程的運行軌跡都記錄在這一log文件中。
-
在修改過的0.11上運行樣本程序,通過分析log文件,統計該程序建立的所有進程的等待時間、完成時間(週轉時間)和運行時間,然後計算平均等待時間,平均完成時間和吞吐量。可以自己編寫統計程序,也可以使用python腳本程序—— stat_log.py ——進行統計。
-
修改0.11進程調度的時間片,然後再運行同樣的樣本程序,統計同樣的時間數據,和原有的情況對比,體會不同時間片帶來的差異。
首先,要進行的操作是在init/main.c中的main()中添加創建日誌文件/var/process.log的語句.這一步在實驗指導書中很明確的解釋瞭如何操作以及爲何這麼做,我在這就不多說了。把init()的一小段代碼移動到main()中,放在move_to_user_mode()之後,同時加上打開log文件的代碼。修改後的main()如下:
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
sti();
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();
}
接下來,我們要做的就是把給定的fprintk()函數放入到kernel/printk.c中。fprintk()函數如下:
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;
}
在kernel/sched.c文件中定義爲一個全局變量long volatile jiffies=0,記錄着中斷髮生的次數。還是在這個文件中的sched_init()函數中,時鐘中斷處理函數被設置如下:
set_intr_gate(0x20,&timer_interrupt);
而在kernel/system_call.s文件中將timer_interrupt定義爲:
timer_interrupt:
……
incljiffies #增加jiffies計數值
……
下面將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(current->pid != task[next]->pid)//判斷當前的進程是否爲被選出的進程
{
if(current->state == TASK_RUNNING)
{
fprintk(3, "%1d\t%c\t%1d\n", current->pid, 'J', jiffies);
}
fprintk(3, "%1d\t%c\t%1d\n", task[next]->pid, 'R', jiffies);
}*/
if(current->state == TASK_RUNNING && current != task[next])
fprintk(3,"%ld\t%c\t%ld\n",current->pid,'J',jiffies);
if(current != task[next])
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 )// 當不是進程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 )// 當不是進程0的時候打印
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
schedule();
*p=tmp;
if (tmp)
{
tmp->state=TASK_RUNNING;
fprintk(3,"%ld\t%c\t%ld\n",tmp->pid,'J',jiffies);
}
}
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 )// 當不是進程0的時候打印
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
schedule();
if (*p && *p != current) {
(**p).state=TASK_RUNNING;
fprintk(3, "%ld\t%c\t%ld\n", (**p).pid, 'J', jiffies);
goto repeat;
}
*p=tmp;
if (tmp)
{
tmp->state=TASK_RUNNING;
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;
}
}
}
接着是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;
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));
fprintk(3, "%1d\t%c\t%1d\n", last_pid, 'N', jiffies);
p->state = TASK_RUNNING; /* do this last, just in case */
fprintk(3, "%1d\t%c\t%1d\n", last_pid, 'J', jiffies);
return last_pid;
}
int find_empty_process(void)
{
int i;
repeat:
if ((++last_pid)<0) last_pid=1;
for(i=0 ; i<NR_TASKS ; i++)
if (task[i] && task[i]->pid == last_pid) goto repeat;
for(i=1 ; i<NR_TASKS ; i++)
if (!task[i])
return i;
return -EAGAIN;
}
再來是exit.c:
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));
for (i=0 ; i<NR_TASKS ; i++)
if (task[i] && task[i]->father == current->pid) {
task[i]->father = 1;
if (task[i]->state == TASK_ZOMBIE)
//fprintk(3, "%1d\t%c\t%1d\n", task[i]->pid, 'E', jiffies);
/* assumption task[1] is always init */
(void) send_sig(SIGCHLD, task[1], 1);
}
for (i=0 ; i<NR_OPEN ; i++)
if (current->filp[i])
sys_close(i);
iput(current->pwd);
current->pwd=NULL;
iput(current->root);
current->root=NULL;
iput(current->executable);
current->executable=NULL;
if (current->leader && current->tty >= 0)
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;
//fprintk(3, "%1d\t%c\t%1d\n", current->pid, 'E', jiffies);
current->exit_code = code;
tell_father(current->father);
schedule();
return (-1); /* just to suppress warnings */
}
int sys_exit(int error_code)
{
return do_exit((error_code&0xff)<<8);
}
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
int flag, code;
struct task_struct ** p;
verify_area(stat_addr,4);
repeat:
flag=0;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
if (!*p || *p == current)
continue;
if ((*p)->father != current->pid)
continue;
if (pid>0) {
if ((*p)->pid != pid)
continue;
} else if (!pid) {
if ((*p)->pgrp != current->pgrp)
continue;
} else if (pid != -1) {
if ((*p)->pgrp != -pid)
continue;
}
switch ((*p)->state) {
case TASK_STOPPED:
if (!(options & WUNTRACED))
continue;
put_fs_long(0x7f,stat_addr);
return (*p)->pid;
case TASK_ZOMBIE:
current->cutime += (*p)->utime;
current->cstime += (*p)->stime;
flag = (*p)->pid;
//fprintk(3, "%1d\t%c\t%1d\n", (*p)->pid, 'E', jiffies);
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 )// 當不是進程0的時候打印
fprintk(3, "%1d\t%c\t%1d\n", current->pid, 'W', jiffies);
schedule();
if (!(current->signal &= ~(1<<(SIGCHLD-1))))
goto repeat;
else
return -EINTR;
}
return -ECHILD;
}
下面這段話一定要認真閱讀:Linux0.11支持四種進程狀態的轉移:就緒到運行、運行到就緒、運行到睡眠和睡眠到就緒,此外還有新建和退出兩種情況。其中就緒與運行間的狀態轉移是通過schedule()(它亦是調度算法所在)完成的;運行到睡眠依靠的是sleep_on()和interruptible_sleep_on(),還有進程主動睡覺的系統調用sys_pause()和sys_waitpid();睡眠到就緒的轉移依靠的是wake_up()。所以只要在這些函數的適當位置插入適當的處理語句就能完成進程運行軌跡的全面跟蹤了。
編寫樣本程序,樣本程序的作用就是生成幾個進程,把我們之前對0.11的修改,通過這些進程的調用來寫到log文件中去。這個樣本程序的編寫很簡單就不多說了。下面是process.c:
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
#include <sys/types.h>
#include <sys/wait.h>
#define HZ 100
void cpuio_bound(int last, int cpu_time, int io_time);
int main(int argc, char * argv[])
{
pid_t pid;
int i = 0;
for(i=0;i<3;i++)
{
pid = fork();
if(pid<0)
{
printf("error in fork!");
}
else if(pid==0)
{
printf("process id is %d\n ",getpid());
cpuio_bound(10,i,10-i);
return;
}
}
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(¤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;
}
}
這樣就把這個實驗的大體部分做完了。通過查看log文件,可以看到如下面的結果類似的東西。
1 N 48
1 J 48
0 J 48
1 R 48
2 N 49
2 J 49
1 W 49
2 R 49
3 N 64
3 J 64
2 J 64
3 R 64
3 W 68
2 R 68
4 N 79
4 J 79
2 W 79
4 R 79
5 N 84
5 J 84
4 W 84
5 R 84
4 J 140
4 R 140
5 E 140
6 N 141
6 J 141
4 W 142
6 R 142
4 J 238
4 R 238
6 E 238
7 N 239
7 J 239
4 W 239
7 R 239
4 J 263
4 R 263
7 E 263
8 N 265
8 J 265
4 W 266
至於最後用腳本運行的結果是什麼,現在有點記不清了,實在是不好意思啦。
對於時間片的修改,是修改INIT_TASK中的priority的值,可以實現時間片的大小調整。變化不明顯,感覺上是因爲輸入輸出設備的操作佔用的時間影響的。
報告:
1.從程序設計者的角度來看,單進程和多進程的區別有以下幾點
(1)單進程是順序執行的,是從上到下執行的,程序設計者需要做的是控制好程序的順序。而多進程是同時執行的,進程可以說是並行執行的,程序設計者需要做的是合理安排每個進程的流程。
(2)單進程的數據是同步的,不需要考慮數據的共享,只要後面的程序使用前面的結果即可。而多進程就需要考慮數據的共享問題,同時對某個數據進行操作很有可能會出錯。
(3)單進程的複雜度較小,而多進程則較爲複雜。
(4)單進程的用途較爲單一,而多進程的用途廣泛。
2.根據指導書上給的提示,我們可以知道,如果在實驗假定沒有人調用過nice系統調用,時間片的初值就是進程0的priority,即宏INIT_TASK中定義的:
#define INIT_TASK \
{ 0,15,15, //分別對應state;counter;和priority;
想要修改時間片,只要修改這個宏定義的第三個值即可。
在進行一系列的數據分析之後從log文件的統計結果中可以發現:在一定的範圍內,平均等待時間,平均完成時間的變化隨着時間片的增大而減小。這是因爲在時間片小的情況下,cpu將時間耗費在調度切換上,所以平均等待時間增加。而超過一定的範圍之後,這些參數將不再有明顯的變化,這是因爲在這種情況下,RR輪轉調度就變成了FCFS先來先服務了。隨着時間片的修改,吞吐量始終沒有明顯的變化,這是因爲在單位時間內,系統所能完成的進程數量是不會變的。