实验环境:进程运行轨迹的跟踪与统计
实验任务:
在 Linux0.11 上实现进程运行轨迹的跟踪。
基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。
在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序,也可以使用 python 脚本程序—— stat_log.py(在 /home/teacher/ 目录下) ——进行统计。
修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。
实验理论:
在main程序生成了一系列进程之后,系统是如何分时运行这些进程或者说如何调度这些进程运行的呢?
内核是通过执行 sched.c程序中的调度函数 schedule()和 system_call.s 中的定时时钟中断过程_timer_interrupt 来操作的。内核设定每 10 毫秒发出一次时钟中断,并在该中断过程中,通过调用 do_timer()函数检查所有进程的当前执行情况来确定进程的下一步状态。并且根据每个进程的当前状态,调度程序会按序地调度每个进程运行。
对于进程在执行过程中由于想用的资源暂时缺乏而临时需要等待一会时,它就会在系统调用中通过sleep_on()类函数间接地调用 schedule()函数,将 CPU 的使用权自愿地移交给别的进程使用。至于系统接下来会运行哪个进程,则完全由 schedule()根据所有进程的当前状态和优先权决定。对于一直在可运行状态的进程,当时钟中断过程判断出它运行的时间片已被用完时,就会在 do_timer()中执行进程切换操作,该进程的 CPU 使用权就会被不情愿地剥夺,让给别的进程使用。
根据实验楼提供的模板,做如下修改:
/*
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();
/*子进程*/
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;
}
对/init/main.c文件做如下修改,在 0 进程运行时将文件描述符0,1,2关联dev/tty0,并将文件描述符3关联 log 文件,用于记录进程的运行轨迹。
进程 1 会继承这些文件描述符,所以 init() 中就不必再 open() 它们。此后所有新建的进程都是进程 1 的子孙,也会继承它们。但实际上,init() 的后续代码和 /bin/sh 都会重新初始化它们。所以只有进程 0 和进程 1 的文件描述符肯定关联着 log 文件,这一点在接下来的写 log 中很重要。
注意!!!
任务0 是个闲置(‘idle’)任务,只有当没有其它任务可以运行时才调用它。
它不能被杀死,也不能睡眠。任务0 中的状态信息’state’是从来不用的。
系统无事可做的时候,进程 0 会不停地调用 sys_pause(),以激活调度算法。此时它的状态可以是等待态,等待有其它可运行的进程;也可以叫运行态,因为它是唯一一个在 CPU 上运行的进程,只不过运行的效果是等待。
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);
//打开process.log文件,关联到文件描述符3
(void) open("/var/process.log",O_CREAT|O_TRUNC|O_WRONLY,0666);
/*open dup返回的一定是未使用的最小的描述符数值*/
/***************添加结束***************/
if (!fork()) { /* we count on this going ok */
init();
}
在kernel/printk.c 中添加如下代码实现fprintk()函数
#include
#include
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()函数的使用例子:
// 向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);
fprintk(3,"%d\tN\t%d\n",p->pid,jiffies);
以下就是找到所有发生进程状态切换的代码点,并在这些点添加适当的代码,然后输出进程状态变化的情况到 log 文件中。
在/kernel/fork.c中作如下两处修改:
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;
/*
*新建一个进程 N表示新建
*/
fprintk(3,"%d\tN\t%d\n",p->pid,jiffies);
// ……
/* 设置进程状态为就绪。所有就绪进程的状态都是
TASK_RUNNING(0),被全局变量 current 指向的
是正在运行的进程。*/
p->state = TASK_RUNNING;
/*
*新建 => 就绪 J表示就绪
*/
fprintk(3,"%d\tJ\t%d\n",p->pid,jiffies);
return last_pid;
}
在 kernel/sched.c 文件中修改如下:
在函数schedule中做两处修改:
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);
}
在函数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;
}
在函数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);
}
}
在函数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;
/*
*这一部分属于 唤醒队列中间进程,通过goto实现唤醒 队列头进程 过程中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);
}
}
在函数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;
}
}
在/kernel/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));
// ……
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;
}
总的来说,Linux 0.11 支持四种进程状态的转移:就绪到运行、运行到就绪、运行到睡眠和睡眠到就绪,此外还有新建和退出两种情况。其中就绪与运行间的状态转移是通过 schedule()(它亦是调度算法所在)完成的;运行到睡眠依靠的是 sleep_on() 和 interruptible_sleep_on(),还有进程主动睡觉的系统调用 sys_pause() 和 sys_waitpid();睡眠到就绪的转移依靠的是 wake_up()。所以只要在这些函数的适当位置插入适当的处理语句就能完成进程运行轨迹的全面跟踪了。
至此,本实验需要修改的所有文件,都完成,make all重新编译内核。
接下来将process.c拷贝到linux0.11系统中,运行该新内核系统,然后在linux0.11中编译该process.c文件:
sudo ./mount-hdc
cp ./process.c ./hdc/usr/root/
sudo umonut hdc
./run
gcc -o process process.c
然后运行process可执行程序,在/var/目录下会生成一个process.log文件,
退出linux0.11,通过挂载形式,将其拷贝到ubuntu文件目录下,进行分析。
sudo ./mount-hdc
cp ./hdc/var/process.log ./
sudo umonut hdc
chmod +x stat_log.py
./stat_log.py process.log 0 1 2 3 4 5 -g | more
linux-0.11 的调度算法是选取 counter 值最大的就绪进程进行调度。
其中运行态进程(即 current)的 counter 数值会随着时钟中断而不断减 1(时钟中断 10ms 一次),所以是一种比较典型的时间片轮转调度算法。
另外,当没有 counter 值大于 0 的就绪进程时,要对所有的进程做 (*p)->counter = ((*p)->counter >> 1) + (*p)->priority。其效果是对所有的进程(包括阻塞态进程)都进行 counter 的衰减,并再累加 priority 值。这样,对正被阻塞的进程来说,一个进程在阻塞队列中停留的时间越长,其优先级越大,被分配的时间片也就会越大。
时间片的定义:
#define INIT_TASK \
{ 0,15,15,
// 上述三个值分别对应 state、counter 和 priority;
在include/linux/sched.h中修改#define INIT_TASK宏定义中counter和priority数值,原始时间片为15,据此可以修改时间片。
时间片为10
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
时间片为15
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
时间片为20
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
总结: