1 多进程图像
多个进程合理有序推进的过程构成了多进程图像。将多进程组织起来才能保证合理有序的推进。linux0.11中采用的是:PCB + 状态 + 队列。
进程状态图用于描述一个进程生存期的状态。其状态包括新建态、就绪态、运行态、阻塞态、终止态。下图是截取自课件的进程状态图:
顺便贴上一个进程状态表
内核表示 | 含义 |
---|---|
TASK_RUNNING | 可运行(就绪态或运行态) |
TASK_INTERRUPTIBLE | 可中断的等待状态 |
TASK_UNINTERRUPTIBLE | 不可中断的等待状态 |
TASK_ZOMBIE | 僵死(僵尸态) |
TASK_STOPPED | 暂停 |
TASK_SWAPPING | 换入/换出 |
PCB是用来记录进程信息的数据结构(是一个结构体),里面有几个成员,这次实验要用到:
struct task_struct {
long state; //进程当前运行状态,有TASK_RUNNING、TASK_INTERRUPTIBLE等几种取值
long counter; //任务运行时间计数,即运行时间片。采用递减方式,counter越大表明任务已经运行的时间越短
long priority;//运行优先数,用于给counter赋初值。一个进程刚被创建时counter == priority。
...
}
Linux0.11中定义了一个任务指针数组,用于存放所有任务:
struct task_struct * task[NR_TASKS] = {&(init_task.task), }; //定义任务指针数组
2 实验内容
具体实验内容和实验提示请看实验指导书:操作系统实验手册
3 实验过程
实验过程一言难尽,全靠参考别人的 blog 才完成了实验 ( Ĭ ^ Ĭ )
3.1 编写 process.c
在编写 process.c 时遇到了一个问题,process.c 程序的注释方式要采用 “ /* */ ” ,不要采用双斜杆的方式注释,不然在bochs 中编译时会报错的。
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <errno.h>
#define HZ 100
void cpuio_bound(int last, int cpu_time, int io_time);
void main(void)
{
int pid = -1; /*进程ID*/
int i = 0; /*循环变量*/
for(i = 0; i < 3; i++)
{ /*利用for循环创建3个子进程。可以通过i来区别子进程:i == 0,1,2*/
pid = fork();
if(-1 == pid)
{
perror("error!");
exit(1);
}
if(0 == pid)
{ /*子进程直接退出*/
break;
}
}
if(0 == i)
{ /*子进程1*/
printf("parent id:%d, myself id:%d, i = %d\n", getppid(), getpid(), i);
cpuio_bound(10, 2, 3);
}
if(1 == i)
{ /*子进程2*/
printf("parent id:%d, myself id:%d, i = %d\n", getppid(), getpid(), i);
cpuio_bound(5, 1, 1);
}
if(2 == i)
{ /*子进程3*/
printf("parent id:%d, myself id:%d, i = %d\n", getppid(), getpid(), i);
cpuio_bound(20, 5, 3);
}
if(3 == i)
{ /*父进程,等待子进程全部退出才能退出*/
for(i = 0; i < 3; i++)
{
wait(NULL);
}
printf("parent id:%d, myself id:%d, i = %d\n", getppid(), getpid(), i);
}
}
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;
}
}
3.2 创建并打开日志
fprintk函数 可用于写日志,将此函数放入到 kernel/printk.c 中。并且我在 kernel.h 添加了该函数的声明。
/*
* 'kernel.h' contains some often-used function prototypes etc
*/
void verify_area(void * addr,int count);
volatile void panic(const char * str);
int printf(const char * fmt, ...);
int printk(const char * fmt, ...);
int fprintk(int fd, const char *fmt, ...); //在这里添加声明
int tty_write(unsigned ch,char * buf,int count);
void * malloc(unsigned int size);
void free_s(void * obj, int size);
fprintk函数 添加到 printk.c 文件中。
//fprintk函数 添加到 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);
if (fd < 3) /* 如果输出到stdout或stderr,直接调用sys_write即可 */
{
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
"pushl $logbuf\n\t" /* 注意对于Windows环境来说,是_logbuf,下同 */
"pushl %1\n\t"
"call sys_write\n\t" /* 注意对于Windows环境来说,是_sys_write,下同 */
"addl $8,%%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (count),"r" (fd):"ax","cx","dx");
}
else /* 假定>=3的描述符都与文件关联。事实上,还存在很多其它情况,这里并没有考虑。*/
{
if (!(file=task[0]->filp[fd])) /* 从进程0的文件描述符表中得到文件句柄 */
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;
}
然后在main函数中添加打开log文件的程序,这里有个知识点 :open dup函数返回的一定是未使用的最小的描述符数值。为了尽早纪录,所以要尽早打开log文件:
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
...
floppy_init();
sti();
move_to_user_mode();
/*********************添加开始**********************************/
setup((void *) &drive_info); //加载文件系统
(void) open("/dev/tty0",O_RDWR,0); //打开/dev/tty0文件,这里没有接受返回值(fd),不过也不需要接收,因为这是第一次打开文件,返回值肯定是0 -- 标准输入
(void) dup(0); //将文件描述符1与/dev/tty0文件关联。同样这里没有接收返回值,不过返回值肯定是1
(void) dup(0); //将文件描述符2与/dev/tty0文件关联
(void) open("/var/process.log",O_CREAT|O_TRUNC|O_WRONLY, 0666); //文件描述符3与/var/process.log相关联
//注意如果没有事先创建"/var/process.log",这里要设置文件访问权限,否则启动不了
/*********************添加结束**********************************/
if (!fork()) { /* we count on this going ok */
init(); //进程1 去执行init()
}
for(;;) pause(); //进程0始终执行pause()
}
3.3 寻找切换点
切换点主要存在 exit.c \ fork.c \ sched.c 三个文件中。
exit.c文件修改如下:
...
int do_exit(long code)
{
···
if (current->leader)
kill_session();
current->state = TASK_ZOMBIE; //转为僵尸态
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'E', 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)
{
···
switch ((*p)->state) {
case TASK_STOPPED:
if (!(options & WUNTRACED))
continue;
put_fs_long(0x7f,stat_addr);
return (*p)->pid;
case TASK_ZOMBIE: //僵尸态的进程应该是在这里退出的
current->cutime += (*p)->utime;
current->cstime += (*p)->stime;
flag = (*p)->pid;
code = (*p)->exit_code;
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;
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies); //阻塞态
schedule();
if (!(current->signal &= ~(1<<(SIGCHLD-1))))
goto repeat;
else
return -EINTR;
}
return -ECHILD;
}
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放入任务队列中去
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
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;
fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'N', 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 */
//就绪态
fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'J', jiffies); //就绪态
return last_pid;
}
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) {
if ((*p)->alarm && (*p)->alarm < jiffies) { //报警定时值不为0,且当前滴答值已经大于报警定时值(达到报警条件)
(*p)->signal |= (1<<(SIGALRM-1)); //设置SIGALRM信号
(*p)->alarm = 0; //取消报警定时
}
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
{
(*p)->state=TASK_RUNNING;
fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', 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) //就绪态的任务比较任务counter值(进程已经运行的时间,越大说明运行的时间越短),
c = (*p)->counter, next = i; //counter最大的最为下一个执行任务
}
if (c) break; //c>0,即找到了下一个需要执行的任务,则退出循环去执行下一个进程。
//若c==-1,即队列中全部是空任务,则退出循环去执行任务0(因为此时next==0)
//若C==0, 说明所有就绪态的任务的时间片都用光了,那么接着执行下面语句给所有任务更新时间片
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
if(current->pid != task[next]->pid && current->state == TASK_RUNNING)
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'J', jiffies); //上一个进程进入就绪态
if(current->pid != task[next]->pid)
fprintk(3, "%ld\t%c\t%ld\n", task[next]->pid, 'R', jiffies); //运行态
switch_to(next); //切换到下一个任务(current指向next对应的任务),switch_to() 之后就正式运行了
}
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
if(current->pid != 0) //0进程比较特殊
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies); //阻塞态
schedule();
return 0;
}
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task)) //判断当前任务是否是任务0
panic("task[0] trying to sleep");
tmp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies); //阻塞态
schedule();
if (tmp)
{
tmp->state=0;
fprintk(3, "%ld\t%c\t%ld\n", tmp->pid, 'J', jiffies); //就绪态
}
}
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\t%c\t%ld\n", current->pid, 'W', jiffies); //阻塞态
schedule();
if (*p && *p != current) {
(**p).state=0;
fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies); //就绪态
goto repeat;
}
*p=NULL;
if (tmp)
{
tmp->state=0;
fprintk(3, "%ld\t%c\t%ld\n", tmp->pid, 'J', jiffies); //就绪态
}
}
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0;
fprintk(3, "%ld\t%c\t%ld\n", (*p)->pid, 'J', jiffies); //就绪态
*p=NULL;
}
}
···
3.4 时间片
在第1节介绍PCB的部分成员时里面提到了任务运行时间计数 counter 的初始值是由 priority 决定的,因此我们要找到 priority 的初始值,然后改变它就可以改变时间片了。最后在 sched.h 中可以找到,我们修改 INIT_TASK 的第3个值(15)就可以修改时间片了。
/*
* INIT_TASK is used to set up the first task table, touch at
* your own risk!. Base=0, limit=0x9ffff (=640kB)
*/
#define INIT_TASK \
/* state etc */ { 0,15,15, \
/* signals */ 0,{{},},0, \
···
4 实验结果
需要用 stat_log.py 来统计实验结果,stat_log.py的源码在实验指导书中有。首先将 编好的 process.c 文件放到 oslab/hdc/usr/root 文件夹下,可以通过执行:
sudo ./mount-hdc
打开hdc文件夹。然后运行 Linux0.11 ,编译运行 process.c ,下图为 process 的执行结果:
···
12 N 14544
12 J 14544
4 W 14544
12 R 14544
13 N 14546
13 J 14546
14 N 14546
14 J 14547
15 N 14547
15 J 14548
12 W 14548
15 R 14548
15 J 14563
14 R 14563
14 J 14578
13 R 14578
13 J 14593
15 R 14593
15 J 14608
14 R 14608
14 J 14623
13 R 14623
···
先给 stat_log.py 加上执行权限(chmod +x stat_log.py),然后在 Ubuntu 中执行 ‘./stat_log.py process.log 12 13 14 15 -g | more’ ,可以看到如下结果(**如果log文件不对的话,即程序中写入log 的记录不对,执行 stat_log.py 将报错提示):
(Unit: tick)
Process Turnaround Waiting CPU Burst I/O Burst
12 2730 29 5 2696
13 1513 493 400 620
14 907 396 300 210
15 2726 619 1500 606
Average: 1969.00 384.25
Throughout: 0.15/s
-----===< COOL GRAPHIC OF SCHEDULER >===-----
[Symbol] [Meaning]
~~~~~~~~~~~~~~~~~~~~~~~~~~~
number PID or tick
"-" New or Exit
"#" Running
"|" Ready
":" Waiting
/ Running with
"+" -| Ready
\and/or Waiting
-----===< !!!!!!!!!!!!!!!!!!!!!!!!! >===-----
14544 -12
14545 #12
14546 # -13 -14
14547 # |13 |14 -15
14548 :12 | | +15
14549 : | | #15
14550 : | | #
14551 : | | #
14552 : | | #
参考
图1.1 进程状态图,截取自哈工大操作系统课程的课件。
[1] 哈工大操作系统实验手册
[2] HIT-OSLab
[3] 哈工大操作系统试验3 进程运行轨迹的跟踪与统计