参考
- 哈工大操作系统实验
- B站UP主的视频讲解
- Linux内核完全注释:基于0.11内核(修正版V3.0)
- https://www.cnblogs.com/wanghuizhao/p/16644919.html
- http://t.csdn.cn/EacOH
一、实验目的
- 掌握Linux下的多进程编程技术;
- 通过对进程运行轨迹的跟踪来形象化进程的概念;
- 在进程运行轨迹跟踪的基础上进行相应的数据统计,从而能对进程调度算法进行实际的量化评价,更进一步加深对调度和调度算法的理解,获得能在实际操作系统上对调度算法进行实验数据对比的直接经验。
二、实验内容
进程从创建(Linux下调用fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态;在运行的过程中如果启动了一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出CPU;当文件读写完毕以后,操作系统会在将其切换成就绪态,等待进程调度算法来调度该进程执行……
本次实验包括如下内容:
-
基于模板“
process.c
”编写多进程的样本程序,实现如下功能:
- 所有子进程都并行运行,每个子进程的实际运行时间一般不超过30秒;
- 父进程向标准输出打印所有子进程的id,并在所有子进程都退出后才退出;
-
在Linux 0.11上实现进程运行轨迹的跟踪。基本任务是在内核中维护一个日志文件/var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一log文件中。
-
在修改过的0.11上运行样本程序,通过分析log文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序,也可以使用python脚本程序—— stat_log.py ——进行统计。
-
修改0.11进程调度的时间片,然后再运行同样的样本程序,统计同样的时间数据,和原有的情况对比,体会不同时间片带来的差异。
/var/process.log文件的格式必须为:
pid X time
其中:
- pid是进程的ID;
- X可以是N,J,R,W和E中的任意一个,分别表示进程新建(N)、进入就绪态(J)、进入运行态®、进入阻塞态(W)和退出(E);
- time表示X发生的时间。这个时间不是物理时间,而是系统的滴答时间(tick);
三个字段之间用制表符分隔。 例如:
12 N 1056
12 J 1057
4 W 1057
12 R 1057
13 N 1058
13 J 1059
14 N 1059
14 J 1060
15 N 1060
15 J 1061
12 W 1061
15 R 1061
15 J 1076
14 R 1076
14 E 1076
……
三、相关知识补充
1.fork()
参考 :Linux 高级编程 - fork 进程控制 - 夏威廉的文章 - 知乎 https://zhuanlan.zhihu.com/p/84342331
在Linux中使用fork()来创建一个子进程:pid_t fork(void)
.
fork()函数有些特殊,成功则返回两次,失败返回-1,利用这个特性可以判断当前的进程是子进程还是父进程:在子进程中返回0,在父进程中返回子进程的ID。
-
fork() 的写时复制技术
通过执行fork,子进程得到父进程的一个副本,例如子进程获得父进程的数据空间,堆和栈的副本,但是它们并不共享存储空间,它们只共享代码段。但是现在的系统实现中,并不执行拷贝父进程的副本,而是使用写时复制技术。
写时复制:在fork之后,这些区域由父子进程共享,而且内核将它们的访问权限变为只读,如果父子进程中的任一个试图修改这些区域,内核只为修改区域的那片内存创建一个副本给子进程。
不管哪种技术实现,最后父子进程的数据都是独立的,不会相互影响。
-
子进程的执行位置
fork函数还有一个特点,子进程不是从main()函数开始执行,而是从fork()函数返回的地方开始执行。
2.wait()
参考:http://t.csdn.cn/eRFur
编程过程中,有时需要让一个进程等待另一个进程,最常见的是父进程等待自己的子进程,或者父进程回收自己的子进程资源包括僵尸进程,这时候就需要用到wait()函数.
函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
3.getpid()
原型:pid_t getpid(void);
功能:获取进程标识码
四、实现
1.基于模板“process.c”编写多进程的样本程序,实现如下功能:
- 所有子进程都并行运行,每个子进程的实际运行时间一般不超过30秒;
- 父进程向标准输出打印所有子进程的id,并在所有子进程都退出后才退出;
(1) 复制进程运行轨迹的跟踪与统计中提供的 process.c代码,然后在 main() 函数中添加代码,添加后完整的process.c文件如下:
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
#include <sys/wait.h>
#include <sys/types.h>
#define HZ 100
void cpuio_bound(int last, int cpu_time, int io_time);
int main(int argc, char * argv[])
{
pid_t father,son1,son2,tmp1,tmp2;
tmp1 = fork();
if(tmp1 == 0)
{
/* son1 */
son1 = getpid();
printf("I am son1!\r\n");
printf("The son1's pid:%d\r\n",son1);
cpuio_bound(10,3,2);
printf("Son1 is finished!\r\n");
}
else if(tmp1 > 0)
{
son1 = tmp1;
tmp2 = fork();
if(tmp2 == 0)
{
/* son2 */
son2 = getpid();;
printf("I am soni2!\r\n");
printf("The son2's pid:%d\r\n",son2);
cpuio_bound(5,1,2);
printf("Son2 is finished!\r\n");
}
else if(tmp2 > 0)
{
son2 = tmp2;
father = getpid();
printf("The father get son1's pid:%d\r\n",tmp1);
printf("The father get son2's pid:%d\r\n",tmp2);
wait((int*)NULL);
wait((int*)NULL);
printf("Now is the father's pid:%d\r\n",father);
}
else
printf("Creat son2 failed!\r\n");
}
else
printf("Creat son1 failed!\r\n");
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;
}
}
(2) 在Ubuntu下编译运行,其结果如下:
gcc -o process process.c
./process
(3) 将process.c 文件复制到linux-0.11中的 /usr/root/ 下执行:
- 进入 lab3目录下,执行下面的命令就行挂载:
sudo ./mount-hdc
- 然后进行复制:
sudo cp process.c ./hdc/usr/root/
- 运行linux0.11:
./run
- 编译执行:
gcc -o process process.c
./process
2.在Linux 0.11上实现进程运行轨迹的跟踪。基本任务是在内核中维护一个日志文件/var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一log文件中.
(1) 打开log文件
为了能尽早开始记录,应当在内核启动时就打开log文件。内核的入口是init/main.c中的main()(Windows环境下是start()),其中一段代码是:
……
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}
……
这段代码在进程0中运行,先切换到用户模式,然后全系统第一次调用fork()建立进程1。进程1调用init()。在init()中:
……
setup((void *) &drive_info); //加载文件系统
(void) open("/dev/tty0",O_RDWR,0); //打开/dev/tty0,建立文件描述符0和/dev/tty0的关联
(void) dup(0); //让文件描述符1也和/dev/tty0关联
(void) dup(0); //让文件描述符2也和/dev/tty0关联
……
这段代码建立了文件描述符0、1和2,它们分别就是stdin、stdout和stderr。这三者的值是系统标准(Windows也是如此),不可改变。可以把log文件的描述符关联到3。文件系统初始化,描述符0、1和2关联之后,才能打开log文件,开始记录进程的运行轨迹。为了能尽早访问log文件,我们要让上述工作在进程0中就完成。所以把这一段代码从init()移动到main()中,放在move_to_user_mode()之后(不能再靠前了),同时加上打开log文件的代码。修改后的main()如下:
……
move_to_user_mode();
/***************添加开始***************/
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0); //建立文件描述符0和/dev/tty0的关联
(void) dup(0); //文件描述符1也和/dev/tty0关联
(void) dup(0); //文件描述符2也和/dev/tty0关联
(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中很重要。
(2) 写log文件
将进程运行轨迹的跟踪与统计中提供的fprintk()函数的源码添加到 kernel/printk.c 中,之后就可以通过下面的方式写log文件:
fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'R', jiffies); //向log文件输出
其中 jiffies 是开机以后计时器计时的滴答数(定义在kernel/sched.c文件的一个全局变量).
(3) 寻找状态切换点
所谓寻找状态切换点就是找到进程状态变化的地方,即在什么位置写log文件.linux-0.11进程状态的切换如下:
- 新建:kernel/fork.c/copy_process()函数
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;
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));
/*
* 新建一个进程
*/
fprintk(3,"%ld\t%c\t%ld\n",p->pid,'N',jiffies);
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;
}
- 退出:kernel/exit.c/do_exit()函数
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));
for (i=0 ; i<NR_TASKS ; i++)
if (task[i] && task[i]->father == current->pid) {
task[i]->father = 1;
if (task[i]->state == TASK_ZOMBIE)
/* assumption task[1] is always init */
(void) send_sig(SIGCHLD, task[1], 1);
}
for (i=0 ; i<NR_OPEN ; i++)
if (current->filp[i])
sys_close(i);
iput(current->pwd);
current->pwd=NULL;
iput(current->root);
current->root=NULL;
iput(current->executable);
current->executable=NULL;
if (current->leader && current->tty >= 0)
tty_table[current->tty].pgrp = 0;
if (last_task_used_math == current)
last_task_used_math = NULL;
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 */
}
- 运行–>就绪和就绪–>运行:kernel/sched.c/schedule() 函数
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,"%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)
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恰好是当前处于运行态的进程,
* 则状态不改变
*/
if(current->pid != task[next]->pid)
{
if(current->state == TASK_RUNNING)
fprintk(3,"%ld\t%c\t%ld\n",current->pid,'J',jiffies);
//下一个要运行的进程
fprintk(3,"%ld\t%c\t%ld\n",task[next]->pid,'R',jiffies);
}
switch_to(next);
}
- 运行–>阻塞
kernel/sched.c/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,"%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);
}
}
kernel/sched.c/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;
/**
* 将当前进程设置为可中断阻塞态,放入*p指定的阻塞队列的头部,tmp是原来的头部
* 当前进程 运行-->可中断阻塞
*/
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);
}
}
- 主动进入阻塞态:kernel/sched.c/sys_pause() 函数
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
/**
* 系统无事可做的时候,进程0会不停地调用sys_pause(),以激活调度算法,此时的状态可以是
* 等待态,等待其他可运行的进程;也可以叫运行态,因为它是唯一一个在CPU上运行的进程,
* 只不过运行的效果是等待
*/
if(current->pid != 0)
{
/**
* 运行-->阻塞
*/
fprintk(3,"%ld\t%c\t%ld\n",current->pid,'W',jiffies);
}
schedule();
return 0;
}
- 主动进入阻塞态:kernel/exit.c/sys_waitpid() 函数
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
int flag, code;
struct task_struct ** p;
verify_area(stat_addr,4);
repeat:
flag=0;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
if (!*p || *p == current)
continue;
if ((*p)->father != current->pid)
continue;
if (pid>0) {
if ((*p)->pid != pid)
continue;
} else if (!pid) {
if ((*p)->pgrp != current->pgrp)
continue;
} else if (pid != -1) {
if ((*p)->pgrp != -pid)
continue;
}
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;
}
- 阻塞–>就绪:kernel/sched.c/wake_up() 函数
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;
}
}
(4) 重新编译内核,然后运行。打开/var/process.log可看到记录的信息。
(5) 数据统计
为方便数据处理,将process.log文件拷贝到ubuntu下处理,具体做法如下:
- 运行内核(在lab3目录下):
./run
- 将文件写入磁盘:
sync
- 然后退出,挂在文件系统:
sudo ./mount-hdc
- 拷贝
cp ./hdc/var/process.log .
下面用实验提供的脚本处理得到的log文件。
将stat_log.py拷贝到当前目录下,然后用chmod +x stat_log.py
加上执行权限,然后直接运行。但是实际操作之后会报错,以现在的能力无法解决,所以就先放一下吧。
3.修改时间片
下面是0.11的调度函数schedule,在文件kernel/sched.c中定义为:
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;
} //找到counter值最大的就绪态进程
if (c) break; //如果有counter值大于0的就绪态进程,则退出
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
//如果没有,所有进程的counter值除以2衰减后再和priority值相加,产生新的时间片
}
switch_to(next); //切换到next进程
分析代码可知,0.11的调度算法是选取counter值最大的就绪进程进行调度。其中运行态进程(即current)的counter数值会随着时钟中断而不断减1(时钟中断10ms一次),所以是一种比较典型的时间片轮转调度算法。另外,由上面的程序可以看出,当没有counter值大于0的就绪进程时,要对所有的进程做“§->counter = (§->counter >> 1) + (*p)->priority”。其效果是对所有的进程(包括阻塞态进程)都进行counter的衰减,并再累加priority值。这样,对正被阻塞的进程来说,一个进程在阻塞队列中停留的时间越长,其优先级越大,被分配的时间片也就会越大。所以总的来说,Linux 0.11的进程调度是一种综合考虑进程优先级并能动态反馈调整时间片的轮转调度算法。
此处要求实验者对现有的调度算法进行时间片大小的修改,并进行实验验证。
为完成此工作,我们需要知道两件事情:
进程counter是如何初始化的?
当进程的时间片用完时,被重新赋成何值?
首先回答第一个问题,显然这个值是在fork()中设定的。Linux 0.11的fork()会调用copy_process()来完成从父进程信息拷贝(所以才称其为fork),看看copy_process()的实现(也在kernel/fork.c文件中),会发现其中有下面两条语句:
*p = *current; //用来复制父进程的PCB数据信息,包括priority和counter
p->counter = p->priority; //初始化counter
因为父进程的counter数值已发生变化,而priority不会,所以上面的第二句代码将p->counter设置成p->priority。每个进程的priority都是继承自父亲进程的,除非它自己改变优先级。查找所有的代码,只有一个地方修改过priority,那就是nice系统调用:
int sys_nice(long increment)
{
if (current->priority-increment>0)
current->priority -= increment;
return 0;
}
本实验假定没有人调用过nice系统调用,时间片的初值就是进程0的priority,即宏INIT_TASK中定义的:
#define INIT_TASK \
{ 0,15,15, //分别对应state;counter;和priority;
接下来回答第二个问题,当就绪进程的counter为0时,不会被调度(schedule要选取counter最大的,大于0的进程),而当所有的就绪态进程的counter都变成0时,会执行下面的语句:
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
显然算出的新的counter值也等于priority,即初始时间片的大小。