操作系统课程实验-linux-0-11进程运行轨迹的跟踪与统计

实验三 进程运行轨迹的跟踪与统计

一、提高CPU使用率

CPU可以用一句话概括:取指执行

提高CPU使用率的方法就是让CPU忙起来,减少不必要的等待

二、一个进程的运行轨迹

对于Linux 0.11内核来讲,系统最多可有64个进程同时存在。除了第一个进程手动建立以外,其余的都是现有进程使用系统调用fork创建的新进程,被创建的进程称为子进程,创建者,则称为父进程。

1. 日志文件

要记录进程运行轨迹,需要将状态输入到日志文件中,以待分析。

// init/main.c
move_to_user_mode();
/* =============== */
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
/* process.log 打开为3号文件描述符 */
(void) open("/var/process.log", O_CREAT|O_TRUNC|O_RDWR, 0666);
/* =============== */
if (!fork()) { /* we count on this going ok */
init();
}

2. 新建进程

// init/main.c
// fork() 的定义,为系统调用 __NR__fork = 2
static inline _syscall0(int,fork)

参考实验二

# kernel/system_call.s
# 实际的fork函数
sys_fork:
# 获取新进程号
call find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
# 复制父进程
call copy_process
addl $20,%esp

新建进程的主要函数就在copy_process中,在这里记录轨迹N(新建)J(就绪)到日志文件

// kernel/fork.c
/* =================== */
// 新建进程
fprintk(3, "%ld\tN\t%ld\n", p->pid, jiffies);
// 就绪
fprintk(3, "%ld\tJ\t%ld\n", p->pid, jiffies);
/* =================== */
return last_pid;
}

3. 调度进程

schedule函数为调度函数,这里找到下一个可运行的进程

// kernel/sched.c
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) {
// 如果设置过alarm并且超出时间,发信号终止进程
if ((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
// 如果设置过任务超时定时值 timeout,并且已经超时,则复位超时定时值,
// 并且如果任务处于可中断睡眠状态 TASK_INTERRUPTIBLE 下,将其置为就绪状态(TASK_RUNNING)。
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE) {
(*p)->state=TASK_RUNNING;
/* =================== */
// 队列里的进程变为就绪态
fprintk(3, "%ld\tJ\t%ld\n", (*p)->pid, jiffies);
/* =================== */
}
}

/* this is the scheduler proper: */

// 比较每个进程的时间片,选择时间片大的运行
// 如果时间片为0,则重新赋值
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;
}
/* =================== */
// 运行态 如果当前和下一个相同则跳过
if (task[next]->pid != current->pid) {
// 当前进程时间片到期,出让CPU
if (current->state == TASK_RUNNING)
fprintk(3, "%ld\tJ\t%ld\n", current->pid, jiffies);
fprintk(3, "%ld\tR\t%ld\n", task[next]->pid, jiffies);
}
/* =================== */
switch_to(next);
}

4. 进程睡眠

在手动建立0进程后,会运行for(;;) pause();,不断调用调度函数即下面的系统调用

// kernel/sched.c
int sys_pause(void)
{
/* ======================= */
if (current->state != TASK_INTERRUPTIBLE)
fprintk(3, "%ld\tW\t%ld\n", current->pid, jiffies);
/* ======================= */
current->state = TASK_INTERRUPTIBLE;
schedule();
return 0;
}

下面函数把当前任务置为可中断的或不可中断的睡眠状态,并让睡眠队列头指针指向当前任务。函数参数p是等待任务队列头指针。指针是含有一个变量地址的变量。这里参数p使用了指针的指针形式**p,这是因为C函数参数只能传值,没有直接的方式让被调用函数改变调用该函数程序中变量的值。但是指针*p指向的目标(这里是任务结构)会改变,因此为了能修改调用该函数程序中原来就是指针变量的值,就需要传递指针*p的指针,即**p

这个函数设计的很巧妙,使用临时变量tmp保存上一个等待进程,每次调度都会更新等待队列头,构成一个隐藏的等待队列

// 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中
tmp = *p;
// 设置新的队列头
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
/* ======================= */
fprintk(3, "%ld\tW\t%ld\n", current->pid, jiffies);
/* ======================= */
schedule();
// 当运行到此时,说明当前进程被唤醒,需要判断tmp,即上一个队列头,唤醒
if (tmp) {
tmp->state=0;
/* ======================= */
fprintk(3, "%ld\tJ\t%ld\n", tmp->pid, jiffies);
/* ======================= */
}
}

下面函数与sleep_on函数的区别 可中断睡眠状态

// kernel/sched.c
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\tW\t%ld\n", current->pid, jiffies);
/* ======================= */
schedule();
// 只有当这个等待任务被唤醒时,程序才又会从这里继续执行。表示进程已被明确地唤醒并执行。
// 如果队列中还有等待的任务,并且队列头指针 *p 所指向的任务不是当前任务,则说明在本任务
// 插入队列后还有任务进入队列,于是我们应该也要唤醒这些后续进入队列的任务,因此这里将队
// 列头所指任务先置为就绪状态,而自己则置为不可中断等待状态,即要等待这些后续进入队列的
// 任务被唤醒后才用 wake_up()唤醒本任务。然后跳转至 repeat 标号处重新执行调度函数。
// 如果不做这一步的话,会丢失新加入的进程
if (*p && *p != current) {
(**p).state=0;
/* ======================= */
fprintk(3, "%ld\tJ\t%ld\n", (**p).pid, jiffies);
/* ======================= */
goto repeat;
}
*p=NULL;
if (tmp) {
tmp->state=0;
/* ======================= */
fprintk(3, "%ld\tJ\t%ld\n", tmp->pid, jiffies);
/* ======================= */
}
}

唤醒不可中断等待任务。*p是任务等待队列头指针。由于新等待任务是插入在等待队列头指针处的,因此唤醒的是最后进入等待队列的任务。

// kernel/sched.c
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0;
/* ======================= */
fprintk(3, "%ld\tJ\t%ld\n", (**p).pid, jiffies);
/* ======================= */
*p=NULL;
}
}

5. 进程退出

进程资源的释放在kernel/exit.c

            case TASK_ZOMBIE:
current->cutime += (*p)->utime;
current->cstime += (*p)->stime;
flag = (*p)->pid;
code = (*p)->exit_code;
/* ======================= */
fprintk(3, "%ld\tE\t%ld\n", (*p)->pid, 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;
/* ======================= */
if (current->pid != 0)
// 有子进程
fprintk(3, "%ld\tW\t%ld\n", current->pid, jiffies);
/* ======================= */
schedule();
if (!(current->signal &= ~(1<<(SIGCHLD-1))))
goto repeat;
else
return -EINTR;
}
return -ECHILD;
}

6. 时间片修改

修改include/linux/sched.h中的频率,可以修改时钟中断的时间

// include/linux/sched.h
#define NR_TASKS 64
#define HZ 200

PC 机8253计数/定时芯片的输入时钟频率约为1.193180MHz
LATCH是设置8253芯片的初值

// kernel/sched.c
#define LATCH (1193180/HZ)

实验代码