一、实验内容
1.基于模板 process.c 编写多进程的样本程序,实现如下功能:
- 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒;
- 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;
之后要在linux-0.11的环境下编译运行process.c已生成一些进程供跟踪和统计
2.在 Linux0.11 上实现进程运行轨迹的跟踪。
- 基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。
要求:
三个字段之间用制表符分隔。
3.在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。
4.修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。
二、基于模板 process.c 编写多进程的样本程序
1.理解fork()
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
#include <stdio.h>
#include <unistd.h> //fork函数的头文件
int main(int argc, char * argv[])
{
int id = fork();
printf("id = %d\n", id);
if(!id) {
printf("id == %d\n", id);
printf("I am child [%d] and my parent is [%d].\n", getpid(), getppid());
}
return 0;
}
父进程打印出“id = 37500”
子进程打印出“id = 0”
if语句块的内容由子进程执行。
2.理解wait(NULL)
在父进程中调用**wait(NULL);**的效果:阻塞父进程,等待子进程结束。如果子进程结束,则返回子进程的pid;如果没有子进程则立刻返回-1。如果有多个子进程,则任意一个子进程结束时,wait函数就会返回子进程的pid,所以等所有子进程结束,父进程才结束的写法是:while(wait(NULL) != -1);
3.process.c的代码(已知cpuio_bound函数,实现main函数即可)
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h> //使用了times函数
#define HZ 100 //每1/100秒产生1次时钟中断。
void cpuio_bound(int last, int cpu_time, int io_time);
int main(int argc, char * argv[])
{
if(!fork()) {
printf("I am child [%d] and my parent is [%d].\n", getpid(), getppid());
fflush(stdout);
cpuio_bound(10, 1, 0);
return 0;
}
if(!fork()) {
printf("I am child [%d] and my parent is [%d].\n", getpid(), getppid());
fflush(stdout);
cpuio_bound(10, 0, 1);
return 0;
}
if(!fork()) {
printf("I am child [%d] and my parent is [%d].\n", getpid(), getppid());
fflush(stdout);
cpuio_bound(10, 1, 9);
return 0;
}
if(!fork()) {
printf("I am child [%d] and my parent is [%d].\n", getpid(), getppid());
cpuio_bound(10, 9, 1);
return 0;
}
while(wait(NULL) != -1); //所有子进程退出后,父进程才推出;可在process.log中检验。
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
* 加上很合理。*/
//为啥要算上核心态的CPU时间?
//用户CPU时间和系统CPU时间之和为CPU时间,即命令占用CPU执行的时间总和。
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;
}
}
我忘记了“父进程向标准输出打印所有子进程的 id”,改由子进程自己打印id了。
三、在 Linux0.11 上实现进程运行轨迹的跟踪与统计
1.实现对process.log文件的访问
为了能尽早开始记录,则要尽早访问log文件。而访问log文件需要满足以下3个前提:
①加载文件系统。
②建立文件描述符0、1和2与/dev/tty0的关联。
③把log文件的描述符关联到3。
原本是由进程1去实现上述①和②,但为了尽早开始记录,改由进程0实现①~③。
由于要先完成move_to_user_mode()之前的初始化工作后才能加载文件系统,所以“添加开始”的位置不能再靠前了。
而且由于进程0建立、就绪和运行后才有process.log,所以日志中没有进程0的建立和就绪。
记得在linux-0.11环境的var目录下新建process.log文件!
把init()中的①~②删去。
2.写log文件的准备工作
(1)实现fprintk函数以写log文件(在kernel/printk.c实现)
fprintk函数的用法:
// 向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)log文件中的time指滴答数。
产生一次时钟中断则滴答1下,并且每10ms滴答一下。
(3)关键
必须找到所有发生进程状态切换的代码点,并在这些点添加适当的代码,来输出进程状态变化的情况到 log 文件中。
新建:N
就绪:J
运行:R
阻塞:W
退出:E
写入信息是要判断进程状态是否真正的发生了改变。 十分重要!如果忽略了,那么很可能导致“Error at line 31: It is a clone of previous line.”
3.写process.log,参考资料
(1)修改kernel/fork.c
调用copy_process函数实现创建进程
①无->新建
②新建->就绪
(2)修改kernel/sched.c+kernel/exit.c
①就绪->运行 | 运行->就绪,在schedule函数中
②阻塞->就绪
schedule函数中:
wake_up函数中:
③运行->阻塞 (2种:被动 + 主动)
- 被动
sleep_on函数中:
interruptible_sleep_on函数中:
不要漏了if,否则会出现重复打印的情况,导致“Error at line 31: It is a clone of previous line.”
- 主动
sys_pause函数中:sys_waitpid函数中:
在kernel/exit.c中
④运行->退出
do_exit函数中:
在kernel/exit.c中
4.一些步骤
(1)在linux-0.11目录下,make all
(2)通过sudo ./mount-hdc
的方式(oslab目录下)把process.c放到linux-0.11环境下的root目录中。
(3)在oslab目录下,./run
运行linux-0.11,然后在linux-0.11中编译运行process.c
(4)在linux-0.11的shell中输入sync,写入磁盘
(5)通过sudo ./mount-hdc
将linux-0.11环境下的var目录中的process.log文件拷贝到ubuntu中
(6)将实验楼里/home/teacher/stat_log.py通过剪切板复制到做实验的虚拟机中。并赋予可执行的权限。
chmod +x stat_log.py
5.进程运行轨迹的跟踪与统计结果
四、上述“三、”中可能遇到的问题及解决办法
1.问题1(主要是粗心造成的)
对红框的解释:①1 R 78;②1 R 78。
(1)先看“2 E 78”(结合图1):此时当前进程是进程2,它首先tell_father,发送唤醒进程1的信号(还没更改进程1的状态,只是信号!),然后fprintk,即打印“2 E 78”,最后调用schedule。
(2)再看图2,schedule函数中,先把有唤醒信号并且处于非中断阻塞状态的进程设置为就绪态。由于我的粗心,写成了R,所以此刻打印第1条“1 R 78”;
(3)最好看图3,schedule函数中,由于选中了进程1(此时task[next]指进程1),而当前进程(还是进程2)不是执行态,故不打印。所以此刻打印第2条“1 R 78”。
图1:
图2:
图3:
将之前的R修改成J后,进行运行轨迹的跟踪结果就正确了。
2.问题2:It is a clone of previous line.
存在重复打印,原因上文论述了。
五、修改 0.11 进程调度的时间片
1.基本认知
①时间片的初值就是进程 0 的 priority,即宏 INIT_TASK 中定义的:
#define INIT_TASK \
{ 0,15,15,
// 上述三个值分别对应 state、counter 和 priority;
②进程0fork出进程1,此时的进程0的counter(剩余的时间)显然不是初始值,所以用进程0的priotity来设置进程1的初始counter。进程1的priority继承进程0。
③当所有进程的counter等于0时,需要重置时间片,按照如下表达式:
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
由于counter为0,所以,重置后,counter为priority的值,因此优先级越高的进程时间片也越大。
2.修改INIT_TASK,在include/linux
3.结果
①(0,30,30)
②(0,15,15)
③(0,5,5)
④(0, 30, 15)
呃,对进程6–10没啥影响?