目录
实验目的
- 掌握 Linux 下的多进程编程技术;
- 通过对进程运行轨迹的跟踪来形象化进程的概念;
- 在进程运行轨迹跟踪的基础上进行相应的数据统计,从而能对进程调度算法进行实际的量化评价,更进一步加深对调度和调度算法的理解,获得能在实际操作系统上对调度算法进行实验数据对比的直接经验
实验内容
进程从创建(Linux 下调用 fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态;在运行的过程中如果启动了一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出 CPU;当文件读写完毕以后,操作系统会在将其切换成就绪态,等待进程调度算法来调度该进程执行……
本次实验包括如下内容:
- 基于模板
process.c
编写多进程的样本程序,实现如下功能:- 所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒;
- 父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;
- 在 Linux0.11 上实现进程运行轨迹的跟踪。
- 基本任务是在内核中维护一个日志文件
/var/process.log
,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。
- 基本任务是在内核中维护一个日志文件
- 在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序,也可以使用 python 脚本程序——
stat_log.py
(在 /home/teacher/ 目录下) ——进行统计。 - 修改 0.11 进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。
实验步骤
打开log文件
操作系统启动后先要打开 /var/process.log
,然后在每个进程发生状态切换的时候向 log 文件内写入一条记录,其过程和用户态的应用程序没什么两样。然而,因为内核状态的存在,使过程中的很多细节变得完全不一样。
预备:Linux 的进程初始化
之前的故事:
在 boot/
目录中,引导程序把内核从磁盘加载到内存中,并让系统进入保护模式下运行后进入系统初始化程序init/main.c
该程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断处理、块设备、和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理。
Now
在完成了这些操作之后,系统各部分已经处于可运行状态。此后程序把自己“手工”移动到 进程0 中运行,并使用fork()
创建出进程1。在进程1中程序将继续进行应用环境的初始化并执行shell登陆程序。而原进程0则会在系统空闲时被调度执行,此时进程0仅执行pause()
系统调用,其中又会去执行调度函数。
打开 log 文件
为了能尽早开始记录,应当在内核启动时就打开 log 文件。内核的入口是 init/main.c 中的 main(),其中一段代码是:
//……
move_to_user_mode();
if (!fork()) {
/* we count on this going ok */
init();
}
//……
这段代码在进程 0 中运行,先切换到用户模式,然后全系统第一次调用 fork()
建立进程 1。进程 1 调用init()
。这就是上文预备知识所提到的操作。
在init()
中:
// ……
//加载文件系统
setup((void *) &drive_info);
// 打开/dev/tty0,建立文件描述符0和/dev/tty0的关联
(void) open("/dev/tty0",O_RDWR,0);
// 让文件描述符1也和/dev/tty0关联
(void) dup(0);
// 让文件描述符2也和/dev/tty0关联
(void) dup(0);
// ……
这段代码建立了文件描述符0
、1
和2
,它们分别就是stdin
、stdout
和 stderr
。这三者的值是系统标准(Windows 也是如此),不可改变。
内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
可以把 log 文件的描述符关联到 3
。文件系统初始化,描述符 0
、1
和 2
关联之后,才能打开 log 文件,开始记录进程的运行轨迹。
为了能尽早访问 log 文件,我们要让上述工作在进程 0 中就完成。所以把这一段代码从init()
移动到 main()
中,放在move_to_user_mode()
之后(不能再靠前了),同时加上打开 log 文件的代码。
//……
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);
(void) open("/var/process.log",O_CREAT|O_TRUNC|O_WRONLY,0666);
/***************添加结束***************/
if (!fork()) {
/* we count on this going ok */
init();
}
//……
打开 log 文件的参数的含义是建立只写文件,如果文件已存在则清空已有内容。文件的权限是所有人可读可写。
这样,文件描述符 0
、1
、2
和 3
就在进程 0 中建立了。根据 fork()
的原理,进程 1 会继承这些文件描述符,所以 init()
中就不必再 open()
它们。此后所有新建的进程都是进程 1 的子孙,也会继承它们。但实际上,init()
的后续代码和 /bin/sh
都会重新初始化它们。所以只有进程 0 和进程 1 的文件描述符肯定关联着 log 文件,这一点在接下来的写 log 中很重要。
写log文件
在内核状态下,write()
功能失效,其原理等同于《系统调用》实验中不能在内核状态调用 printf()
,只能调用 printk()
。编写可在内核调用的 write() 的难度较大,所以这里直接给出源码。它主要参考了 printk()
和 sys_write()
而写成的:
#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 中。
使用方式:
// 向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);
关于jiffies:
先简单说,jiffies 实际上记录了从开机以来共经过了多少个 10ms,修改时间片时会提到。
寻找状态切换点
预备:Linux 的进程运行状态
进程状态保存在任务数据结构(task_struct
即为一个ADT:抽象数据类型)中的state
字段
struct task_struct {
/* these are hardcoded - don't touch */
long state; /* -1 unrunnable, 0 runnable, >0 stopped */
long counter;
long priority;
long signal;
//... 中间略,就是说明下大概什么样子
str