进程运行轨迹的跟踪与统计
进程从创建(Linux 下调用 fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态;在运行的过程中如果启动了一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出 CPU;当文件读写完毕以后,操作系统会在将其切换成就绪态,等待进程调度算法来调度该进程执行……
process.c解读
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;
}
}
使用last来判断退出,当last时间为小于0表示时间到达了。
记录一个开始时间,
消耗时间来假设使用了一个cpu时间,减去一个cpu时间,表示调用
使用sleep函数来模拟一秒,直到大于等于一个io时间退出,减去一个io时间,表示调用
调用完仍然last仍然没有小于0,继续调用。
proccess.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 p1,p2,p3,p4;
p1=fork();
if(p1<0){
printf("file to fork");
}else if(p1==0){
printf("ppid:%4d pid:%4d\n",getppid(),getpid());
cpuio_bound(10,1,0);
}else{
printf("ppid:%4d pid:%4d fpid:%4d\n",getppid(),getpid(),p1);
}
p2=fork();
if(p2<0){
printf("file to fork");
}else if(p2==0){
printf("ppid:%4d pid:%4d\n",getppid(),getpid());
cpuio_bound(10,0,1);
}else{
printf("ppid:%4d pid:%4d fpid:%4d\n",getppid(),getpid(),p2);
}
p3=fork();
if(p3<0){
printf("file to fork");
}else if(p3==0){
printf("ppid:%4d pid:%4d\n",getppid(),getpid());
cpuio_bound(10,1,1);
}else{
printf("ppid:%4d pid:%4d fpid:%4d\n",getppid(),getpid(),p3);
}
p4=fork();
if(p4<0){
printf("file to fork");
}else if(p4==0){
printf("ppid:%4d pid:%4d\n",getppid(),getpid());
cpuio_bound(10,1,9);
}else{
printf("ppid:%4d pid:%4d fpid:%4d\n",getppid(),getpid(),p4);
}
wait(NULL);
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;
}
}
执行得到结果
由此可见,首先实现的是281进程,创建了282,283,284,285四个进程,
282创建了288,289,290三个进程
283创建了286,287两个进程
284创建了291
288创建了292,293
292创建了296
289创建了295
自定义文件描述符
将log文件作为一个进程默认打开的文件,当需要时可以进行输入和输出,可以看成是一个基本输入输出文件,这里需要补充的知识。也就是说将log文件作为和屏幕一样的设备。
我们可以看源码的main函数,其中对于基本输入输出的定义。在init/main.c中
这里当进行完了所有的设置的初始化之后,先进行了一个fork(),进行了分支,将子进程去调用init()函数初始化,父进程被阻断。再看init函数里面
前面的setup函数用于加载文件系统,
open函数用于打开终端,作为这个进程的文件描述符,dup用于复制文件描述符,因为在之前这个进程没有打开任何的文件,所以open的文件描述序号为0,然后复制了两次文件描述符0,作为文件描述符1和2,这三个文件描述符就是标准输入,标准输出和标准错误输出,同时因为fork函数作为创建进程的函数,子进程会继承父进程的文件描述符,所以子进程默认保护文件描述符0,1,2
我们可以将这三行放在fork函数之前,这样每一个进程都会被监视。
同时再打开一个文件,这样之后的每一个进程开始创建都有文件描述符3,指向的是/var/process.log文件。
这个我们需要知道的是,这个创建过程我们是在内核完成的,那么在内核中我们肯定不能使用用户态的write函数,所以需要一个在内核中写文件的函数,这里直接给出fprintk(int fd,const char *fmt,……)函数,给出文件描述符,用法和fprintf一样。当进程状态发生了切换,我们只需要进行fprintk将其输出到/var/process.log中。
#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;
}
将这个函数写入kernel/printk.c中
时钟中断的实现
kernal中的sched.c是进行时钟中断的一个文件,其中的sched.c被init/main.c调用,用于对时钟的初始化。
其中定义了一个
long volatile jiffies=0;//这个记录了计算机从开机到当前发生了时钟中断次数
下面的timer_interrupt函数主要是当一个进程到了时钟中断,会调用一个do_timer函数,这个do_timer再调用shchedule来进行就绪状态的进程的选择执行,最后再从0环退出到3环。
timer_interrupt:
! ……
! 增加jiffies计数值
incl jiffies
! ……
call do_timer # 'do_timer(long CPL)' does everything from
addl $4,%esp # task switching to accounting ...
jmp ret_from_sys_call
再之后,在sched_init()中还定义了
// 设置8253模式
outb_p(0x36, 0x43);
outb_p(LATCH&0xff, 0x40);
outb_p(LATCH>>8, 0x40);
// 在 kernel/sched.c 中
#define LATCH (1193180/HZ)
// 在 include/linux/sched.h 中
#define HZ 100
再加上 PC 机 8253 定时芯片的输入时钟频率为 1.193180MHz,即 1193180/每秒,LATCH=1193180/100,时钟每跳 11931.8 下产生一次时钟中断,即每 1/100 秒(10ms)产生一次时钟中断,所以 jiffies 实际上记录了从开机以来共经过了多少个 10ms。
时钟每秒跳动1193180次,那么LATCH=1193180/100也就是说,每跳动11931.8次就进行一次jiffies的增加,表示的是进行了1/100秒。
寻找状态切换点:
要显示进程状态要修改的文件主要是
fork.c 其中包含产生进程的代码
sched.c 其中包含调度相关代码 只需要修改.state进行修改的地方 但是需要注意的是就绪状态和运行状态是使用一个标志0来表示的,这就需要我们对schedule()函数的代码做详细的分析,得出在什么时候需要将一个进程的状态置为运行,什么时候是就绪
exit.c主要是进程退出的代码。只需要.state进行了修改的地方进行输出,且当我们需要在schedule()函数中添加
fork()
一个进程的从fork()开始,所以我们可以看fork()函数的源代码,fork函数的源代码在sys_fork()中,在kernel/system_call.s中实现
.align 2
sys_fork:
call find_empty_process
testl %eax,%ea
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process
addl $20,%esp
1: ret
其中的copy_process进行的就是一个新的进程的创建工作,在kernel/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;
//*********************************new code
fprintk(3,"%d\tN\t%d\n",p->pid,jiffies);
//*********************************
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));
p->state = TASK_RUNNING; /* do this last, just in case 设置进程的状态为就绪态*/
//********************************new code
fprintk(3,"%d\tJ\t%d\n",p->pid,jiffies);
//*******************************
return last_pid;
}
sche.c:
记录加入睡眠状态的时间。
sleep_on() 和 interruptible_sleep_on() 让当前进程进入睡眠状态,可以在kernel/sched.c中找到源代码。
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;
//****************************************new code
fprintk(3,"%d\tW\t%d\n",p->pid,jiffies);
//****************************************
schedule();
if (tmp){
tmp->state=0;
fprintk(3,"%d\tJ\t%d\n",tmp->pid,jiffies);
}
}
//函数功能,给出一个参数是pcb的队列,主要功能是:
//将当前运行进程加入到阻塞队列,同时将阻塞队列中的进程加入到就绪队列中,
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
fprintk(3,"%d\tW\t%d\n",current->pid,jiffies);
schedule();
return 0;
}
//将进程加入到阻塞队列
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,"%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);
//增加一个就绪状态,从阻塞到就绪*****************************************************************
}
}
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;
}
}
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,"%d\tJ\t%d\n",p->pid,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就是需要切换的进程的在队列中的下标,当这个next和当前进程不是同一个就在文件中打印当前J状态,如果一样,就增加一个R状态
if(task[next]->pid != current->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);
}
主要的修改代码就这些,只需要在state发生了变换的地方修改,且加上在调度切换的时候,判断一下是否调度的进程和当前进程为同一个。
exit.c:
在这个文件中也有两处地方进行了state的切换,这里不再列举,只需要如法炮制进行fprintk就可以了
测试:
进入修改后的系统,运行了process.c的代码
找到/var/proccess.log文件,建议拿到本机来查看,因为在实验环境中比较卡,而且因为main()函数中的sys_pause()这一句,让0号进程产生wait状态的情况特别多,更不用说linux0.11中了,直接卡死
这里的实验数据存在一些问题:所有的进程没有N状态,事后翻源码发现fork.c中的fprintk语句把描述符3写成了“3”,导致了N状态没有写入proccess.log中
上图就是运行process.c时,操作系统写入到process.log的字符。在这里我感觉有些不对,因为有多次我执行完process.c后直接退出了,但是字符没有写入文件中,我猜测是因为:需要等待,因为每当进行一个进程切换,就需要读一次磁盘,所以当执行完process.c后需要等很长一段时间,等待CPU将写磁盘的工作做完,这个结论暂时也不知道是否正确。
我等待了很长时间后,切换了4百万次,其中百分之99都是main.c中的for(;; ) pause();造成的
后面想一想完全可以将pause()函数中的fprintk函数去掉,来保证完成速度,当时并没有意识到……