实验内容
- 基于模板 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!
",i+1);
return -1;
}
/*父进程继续fork*/
}
/*打印所有子进程PID*/
for(i=0;i<10;i++)
printf("Child PID: %d
",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
"
"push %%ds
"
"pop %%fs
"
"pushl %0
"
/* 注意对于Windows环境来说,是_logbuf,下同 */
"pushl $logbuf
"
"pushl %1
"
/* 注意对于Windows环境来说,是_sys_write,下同 */
"call sys_write
"
"addl $8,%%esp
"
"popl %0
"
"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
"
"push %%ds
"
"pop %%fs
"
"pushl %0
"
"pushl $logbuf
"
"pushl %1
"
"pushl %2
"
"call file_write
"
"addl $12,%%esp
"
"popl %0
"
"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 N %d
",p->pid,jiffies);
// ……
/* 设置进程状态为就绪。所有就绪进程的状态都是
TASK_RUNNING(0),被全局变量 current 指向的
是正在运行的进程。*/
p->state = TASK_RUNNING;
//新增修改
fprintk(3,"%d J %d
",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 J %d
",(*p)->pid,jiffies);
}
// ……
/*编号为next的进程 运行*/
if(current->pid != task[next] ->pid)
{
/*新建修改--时间片到时程序 => 就绪*/
if(current->state == TASK_RUNNING)
fprintk(3,"%d J %d
",current->pid,jiffies);
fprintk(3,"%d R %d
",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 W %d
",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 W %d
",current->pid,jiffies);
schedule();
if (tmp)
{
tmp->state=0;
/*
*修改--原等待队列 第一个进程 => 唤醒(就绪)
*/
fprintk(3,"%d J %d
",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 W %d
",current->pid,jiffies);
schedule();
if (*p && *p != current) {
(**p).state=0;
/*
*修改--当前进程进程 => 可中断睡眠
*/
fprintk(3,"%d J %d
",(*p)->pid,jiffies);
goto repeat;
}
*p=NULL;
if (tmp)
{
tmp->state=0;
/*
*修改--原等待队列 第一个进程 => 唤醒(就绪)
*/
fprintk(3,"%d J %d
",tmp->pid,jiffies);
}
}
4.修改wake_up函数
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0;
/*
*修改--唤醒 最后进入等待序列的 进程
*/
fprintk(3,"%d J %d
",(*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 E %d
",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 W %d
",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资源,所以情况要复杂得多。在设计多进程编程时,要考虑资源的分配,时间片的分配等达到系统调度的平衡。要综合考虑所有进程的情况以达到最优的并行执行效果。且多进程编程的功能更为强大,且应用范围较于单进程编程更加广泛。
问题二
- 将时间片变小,进程调度次数变多,系统会使得该进程等待时间变长。
- 将时间片增大,进程因中断/睡眠而产生的调度次数也增多,等待时间也会变长。
- 总结:时间片要设置合理,不能过大或者过小。