操作系统实验3:进程运行轨迹的跟踪与统计
实验内容请查看实验指导手册
绪论
开始实验之前,需要弄清楚实验具体要做什么:
- 参考
home/teacher
目录下的process.c
编写一个能够创建多个并行子进程的程序。 - 在linux0.11系统的内核中,添加和修改程序,使得我们启动linux0.11并且运行
process.c
后,系统能够打印出一个监视进程运行的process.log
文件
一、实验内容
1.编写process.c文件
process.c代码如下:
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
#include <sys/types.h>
#define HZ 100
void cpuio_bound(int last, int cpu_time, int io_time);
/*
1. 所有子进程都并行运行,每个子进程的实际运行时间一般不超过30秒;
2. 父进程向标准输出打印所有子进程的id,并在所有子进程都退出后才退出;
*/
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();
/*如果fork 失败*/
if(n_proc[i] < 0 )
{
printf("Failed to fork child process %d!\n",i+1);
return -1;
}
else if(n_proc[i] == 0)/*子进程*/
{
cpuio_bound(20,2*i,20-2*i); /*每个子进程都占用20s*/
exit(0); /*执行完cpuio_bound 以后,结束该子进程,下一次循环,该子进程不会fork出新的进程*/
}
/*一直都是那一个父进程在fork出子进程*/
}
/*打印所有子进程PID*/
for(i=0;i<10;i++)
printf("Child PID: %d\n",n_proc[i]);
/*等待所有子进程完成;
这里不是很完美,由于wait函数的特点,父进程只要有回收了一个子进程,就会结束自己*/
wait(&i); /*Linux 0.11 上 gcc要求必须有一个参数, gcc3.4+则不需要*/
return 0;
}
/* cpuio_bound函数无需修改,直接用 */
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)
{
times(&start_time);
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;
sleep_time=0;
while (sleep_time < io_time)
{
sleep(1);
sleep_time++;
}
last -= sleep_time;
}
}
process.c
的保存路径为~/oslab/hdc/usr/root/
如果你对fork()函数和wait()函数的作用以及工作方式还不是很清楚的话,建议你花点时间,看一下相关文章,这应该会帮助你更好的理解process.c
程序的功能作用
2.日志文件
2.1 修改main.c
为了让linux0.11在启动之后,就创建process.log
并开始记录,需要将~/oslab/linux-0.11/init/main.c
文件做如下修改:
//……
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();
}
//……
2.2 添加fprintk()函数
在内核状态下,write()
功能失效,其原理等同于《系统调用》实验中不能在内核状态调用 printf()
,只能调用printk()
。编写可在内核调用的 write() 的难度较大,所以这里直接给出源码。它主要参考了 printk()
和 sys_write()
而写成的:因为和 printk()
的功能近似,建议将此函数放入到 ~/oslab/linux-0.11/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;
}
fprintk()
的使用方式类同与 C 标准库函数fprintf()
,唯一的区别是第一个参数是文件描述符,而不是文件指针。例如:
// 向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);
2.3 寻找状态切换点
我们来理一下思路:
- 首先,修改了初始化文件
main.c
,让系统一启动就会创建一个process.log
,这个文件的路径在~/oslab/hdc/var/
(需要挂载虚拟机才看得到哦); - 然后,我们往内核中添加了一个
fprintk()
函数,使得系统可以向process.log
文件进行打印输出。 - 因此,最后一步,我们需要去寻找系统内核代码的合适位置,插入
fprintk()
,打印出我们想要的进程状态。
对以下文件进行了修改:
/kernel/fork.c
/* ... */
p->start_time = jiffies;
/*因为更新了启动时间,这里就是一个进程的新建*/
fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'N', jiffies);
/* ... */
p->state = TASK_RUNNING; /* do this last, just in case */
/*上述语句,将一个新建态的进程变为了就绪态的进程,向log文件输出*/
fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'J', jiffies);
/kernel/sched.c
- 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;
/*sleep_on函数,让进程从运行到睡眠,也就是进入到堵塞(W)*/
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
schedule();
if (tmp)
{
tmp->state = 0;
/*
*原等待队列 第一个进程 => 唤醒(就绪)
*/
fprintk(3,"%d\t%c\t%d\n",tmp->pid,'J',jiffies);
}
}
- 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;
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies);
schedule();
if (*p && *p != current)
{
(**p).state = 0;
/*
*当前进程进程 => 可中断睡眠 同上
*/
fprintk(3,"%d\t%c\t%d\n",(*p)->pid,'J',jiffies);
goto repeat;
}
*p = NULL;
if (tmp)
{
tmp->state = 0;
/*
*原等待队列 第一个进程 => 唤醒(就绪)
*/
fprintk(3,"%d\t%c\t%d\n",tmp->pid,'J',jiffies);
}
}
- schedule()函数
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;
}
/*
编号为next的进程 运行
很奇怪,这段代码,我在0.11内核中并没有看到,《linux0.11完全注释》上没有,
但是其他人的代码片中都加上这段代码?
*/
if(current->pid != task[next] ->pid)
{
/*时间片到时程序 => 就绪*/
if(current->state == TASK_RUNNING)
fprintk(3,"%d\t%c\t%d\n",current->pid,'J',jiffies);
fprintk(3,"%d\t%c\t%d\n",task[next]->pid,'R',jiffies);
}
switch_to(next); /*switch_to是一个宏*/
}
- wake_up()函数
void wake_up(struct task_struct **p)
{
if (p && *p)
{
(**p).state = 0;
/*睡眠到就绪态*/
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'J', jiffies);
*p = NULL;
}
}
/kernel/exit.c
- do_exit()函数
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)
/* 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;
current->exit_code = code;
/*TASK_ZOMBIE表示进程处于僵死状态,可以理解成进程被退出了吧*/
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()函数
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;
code = (*p)->exit_code;
/*可以看到,下一句是release释放进程,那么显然这里是进程的退出*/
fprintk(3, "%ld\t%c\%ld\n", (*p)->pid, '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;
/*
*当前进程 => 等待
*/
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;
}
2.4 测试
使用以下命令,重新编译内核:
/*确保卸载虚拟机挂载*/
cd ~/oslab/
sudo umount hdc
/*重新编译内核*/
cd ~/oslab/linux-0.11/
make all
使用以下命令,进入bochs:
cd ~/oslab/
./run
进入bochs后,依次输入以下命令:
gcc -o process process.c
./process
/*等待process执行完成*/
sync
ll /var
我的实验结果如图所示:
退出bochs,在终端中输入如下命令,在宿主机ubuntu上查看log文件更加方便:
/*将./hdc/var/process.log文件复制到/oslab文件夹中*/
cp ./hdc/var/process.log ~/oslab
3.统计数据
这个实验的负责团队用python编写一个数据统计程序stat_log.py,它从log文件读入原始数据,然后计算平均周转时间、平均等待时间和吞吐率。
从github上将stat_log.py
下载下来,保存在~/oslab/hdc/var/
,在ubuntu终端里,依次输入以下命令:
cd ./hdc/var/
chmod +x stat_log.py
python stat_log.py process.log 7 8 9 10 11 -g
如果process.log
文件数据内容不正确或者打印格式不正确的话,这个py程序将无法执行。
程序运行结果如下图所示:
4.修改时间片及测试
linux0.11采用的调度算法是一种综合考虑进程优先级并能动态反馈调整时间片的轮转调度算法。 那么什么是轮转调度算法呢?它为每个进程分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程;如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。
进程之间的切换是需要时间的,如果时间片设定得太小的话,就会发生频繁的进程切换,因此会浪费大量时间在进程切换上,影响效率;如果时间片设定得足够大的话,就不会浪费时间在进程切换上,利用率会更高,但是用户交互性会受到影响
时间片的初始值是进程0的priority,是在linux-0.11/include/linux/sched.h
的宏 INIT_TASK
中定义的,如下:我们只需要修改宏中的第三个值即可,该值即时间片的初始值。
#define INIT_TASK \
{ 0,15,15,
// 上述三个值分别对应 state、counter 和 priority;
由于我的process.log
数据存在问题,我没有做这个测试实验,下面贴一下别人的实验结果:
原始时间片为15,修改了两次时间片,分别为10和20,结果如下:
原始时间片:15
(Unit: tick)
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.45/s
**************************************************************
第一次修改时间片:10
(Unit: tick)
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.42/s
**************************************************************
第二次修改时间片:20
(Unit: tick)
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
结论:
时间片变小,进程因时间片到时产生的进程调度次数变多,该进程等待时间越长。
然而随着时间片增大,进程因中断或者睡眠进入的进程调度次数也增多,等待时间随之变长。
故而需要设置合理的时间片,既不能过大,也不能过小。
二、回答问题
- 结合自己的体会,谈谈从程序设计者的角度看,单进程编程和多进程编程最大的区别是什么?
- 单进程编程是一个进程从上到下顺序进行;多进程编程可以通过并发执行,即多个进程之间交替执行,如某一个进程正在I/O输入输出而不占用CPU时,可以让CPU去执行另外一个进程,这需要采取某种调度算法。
- 单进程编程的CPU利用率低,因为单进程在等待I/O时,CPU是空闲的;多进程编程的CPU利用率高,因为当某一进程等待I/O时,CPU会去执行另一个进程,因此CPU的利用率高。
- 你是如何修改时间片的?仅针对样本程序建立的进程,在修改时间片前后,log文件的统计结果(不包括Graphic)都是什么样?结合你的修改分析一下为什么会这样变化,或者为什么没变化?
- 时间片变小,进程因时间片到时产生的进程调度次数变多,该进程等待时间越长。
- 然而随着时间片增大,进程因中断或者睡眠进入的进程调度次数也增多,等待时间随之变长。
- 故而需要设置合理的时间片,既不能过大,也不能过小。
参考文献:
1.Linux中wait()函数
2.linux中fork()函数详解
3.Linux中的stdout和stderr
4.linux文件描述符限制及使用详解
5.哈工大操作系统实验课——进程运行轨迹的跟踪与统计(lab 4)
6.(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验3-进程运行轨迹的跟踪与统计
7.蓝桥云课-操作系统原理与实践
8.哈工大操作系统实验手册
9.哈工大实验“官方”github仓库
10.MOOC哈工大操作系统实验3:进程运行轨迹的跟踪与统计