进程轨迹的跟踪和统计
关键字:PCB 进程切换 fork() schedule()
实验分析
进程状态分析
linux0.11中一共有五种状态,宏定义详见linux/include/linux/sched.h文件
1. TASK_RUNNING 0
2. TASK_INTERRUPTIBLE 1
3. TASK_UNINTERRUPTIBLE 2
4. TASK_ZOMBIE 3
5. TASK_STOPPED 4
TASK_RUNNING 进程处于运行或者就绪状态。linux0.11中新建态,运行态和就绪态为同一状态,运行态和就绪态的区别在于是否分配了时间片。
TASK_INTERRUPTIBLE 进程处于可中断等待状态
TASK_UNINTERRUPTIBLE 进程处于不可中断等待状态
以上两种状态都表明进程处于等待状态,唯一的区别在于唤醒的方式不同,可中断等待状态可以由所等待的信号唤醒,也可由wake_up()唤醒。不可中断等待状态只能由wake_up()唤醒。
TASK_ZOMBIE 进程处于僵死状态,停止运行,等待父进程对其PCB资源占用的回收。
TASK_STOPPED 进程已经停止运行。
进程处于TASK_ZOMBIE 状态时,其所占用的资源(除其本身PCB所占用的内存页)均已回收,但是还没有给父进程发送子进程终止信号SIGCHLD。当进程进行调度时发现进程状态为TASK_ZOMBIE,那么就给其父进程发送子进程终止信号SIGCHLD,由其父进程回收PCB所占用的内存页,然后重新调度。父进程一般调用系统调用waitpid()来等待子进程的结束,然后回收子进程所占用的资源。
linux0.11中,除进程0外,其他所有的进程均由父进程产生,系统初始化的时候,会产生进程0,进程0通过fork()产生进程1,然后由进程1产生其他进程。进程1的生命周期为从创建到操作系统退出整段时间,操作系统工作时,进程1是不会消亡的,此外进程1为所有状态为TASK_ZOMBIE的父进程,根据POSIX.1的要求,若父进程已经先行终止,则子进程应该被进程1收容,所以每一个状态为TASK_ZOMBIE的进程都会有一个父进程。
状态转移分析
新建 -> 就绪
linux0.11中,新建态和就绪态为同一个状态,通过fork()产生一个子进程,fork()会调用copy_process()函数,分析copy_process()函数可知,子进程会复制一份父进程的PCB,代码段,数据段,页目录项,页表项等,所以在本次实验中,process.log文件只需在进程0中打开一次即可,因为所有的子进程都会继承它。
就绪 -> 运行
linux0.11中就绪态和运行态也用同一种状态标识,代码实现上区别这两种状态,是通过schedule()函数调度获得cpu的控制权,一旦获得cpu的控制权,就为运行态,否则为就绪态。
运行 -> 就绪
通过schedule()函数调度,时间片用完的进程将会被其他进程抢占,失去cpu的控制权,从而逻辑上由运行态转为就绪态。
运行 -> 等待
linux0.11中,进程由运行态转为等待状态有这样几种情况:
主动暂停,由系统调用sys_pause()控制。
不可中断睡眠,由系统调用 sleep_on()控制。
可中断睡眠, 由系统调用 interruptible_sleep_on()控制。
父进程等待子进程退出, 由系统调用sys_waitpid()控制。
等待 -> 就绪
对于可中断睡眠状态的进程可以通过信号值来唤醒,也可通过wake_up()显示唤醒。对不可中断睡眠状态的进程只能通过wake_up()显示唤醒。
退出
进程运行结束,或者人为的杀死,就会转为僵死状态,处于僵死状态的进程会在调度时候被释放回收。
通过以上分析,linux0.11中状态转移图为:
为了逻辑上清晰,区分了新建态,就绪态,和运行态。
所以本次实验将要修改的代码有:
fork.c中的copy_process函数;sched.c中的schedule函数、sys_pause函数、sleep_on函数、interruptible_sleep_on函数、wake_up函数;exit.c中的do_exit函数、sys_waitpid函数。
进程切换点分析与修改
fork.c copy_process()函数
位置:linux/kernel
对应状态转移:新建态 就绪态 运行态。
先将新建的进程状态置为不可中断状态,因为此时还没有给新建的子进程PCB初始化,为了防止在PCB初始化过程中被调度,所以一开始置为不可中断。接着当PCB初始化完毕,会将子进程的状态置为运行态,此时表明子进程创建完毕,由前面介绍可知,新建态 就绪态 运行态为同一状态,所以此时要写两条记录:新建和就绪。
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->start_time = jiffies;
.
.
.
set_tss_desc(gdt&#43;(nr<<1)&#43;FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt&#43;(nr<<1)&#43;FIRST_LDT_ENTRY,&(p->ldt));
fprintk(3, "%ld\t%c\t%ld\n", last_pid, &#39;N&#39;, jiffies); //向log文件输出
fprintk(3, "%ld\t%c\t%ld\n", last_pid, &#39;J&#39;, jiffies); //向log文件输出
p->state &#61; TASK_RUNNING;/* do this last, just in case */
return last_pid;
sched.c schedule()函数
schedule()负责从队列中选择一个将要运行的进程。
添加的代码有两部分&#xff1a;
如果当前等待的进程所等待的信号被置位且当前进程处于可中断等待状态&#xff0c;则将其置为就绪态&#xff0c;所以此时添加一条记录。
在修改被调度到的进程时要对上一个运行的进程状态进行判断&#xff0c;如果上一个(即current)进程此时的状态是就绪态(TASK_RUNNING)&#xff0c;则说明上一个进程是被抢占的(可能是时间片用完&#xff0c;或其他进程的优先级提高)&#xff0c;则也要记录上一个进程的状态改变&#xff0c;由运行态变成就绪态。
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 &#61; &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm (*p)->signal |&#61; (1< (*p)->alarm &#61; 0; } if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&(*p)->state&#61;&#61;TASK_INTERRUPTIBLE) { if((*p)->state !&#61; TASK_RUNNING) fprintk(3,"%ld\t%c\t%ld\n",(*p)->pid,&#39;J&#39;,jiffies); (*p)->state&#61;TASK_RUNNING; } } /* this is the scheduler proper: */ while (1) { c &#61; -1; next &#61; 0; i &#61; NR_TASKS; p &#61; &task[NR_TASKS]; while (--i) { if (!*--p) continue; if ((*p)->state &#61;&#61; TASK_RUNNING && (*p)->counter > c) c &#61; (*p)->counter, next &#61; i; } if (c) break; for(p &#61; &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) (*p)->counter &#61; ((*p)->counter >> 1) &#43;(*p)->priority; } if(task[next]->pid !&#61; current->pid) { if(current->state &#61;&#61; TASK_RUNNING) { fprintk(3,"%ld\t%c\t%ld\n", current->pid, &#39;J&#39;, jiffies); } fprintk(3,"%ld\t%c\t%ld\n", task[next]->pid, &#39;R&#39;, jiffies); } switch_to(next); } sys_pause() 这个函数是进程主动进行等待&#xff0c;让出CPU&#xff0c;然后让系统进行进程调度。 int sys_pause(void) { if(current->state !&#61; TASK_INTERRUPTIBLE) fprintk(3,"%ld\t%c\t%ld\n", current->pid, &#39;W&#39;, jiffies); current->state &#61; TASK_INTERRUPTIBLE; schedule(); return 0; } sleep_on & interruptible_sleep_on 两个函数类似&#xff0c;一个用于可中断&#xff0c;一个用于不可中断。 void sleep_on(struct task_struct **p) { struct task_struct *tmp; if (!p) return; if (current &#61;&#61; &(init_task.task)) panic("task[0] trying to sleep"); tmp &#61; *p; *p &#61; current; if(current->state !&#61; TASK_UNINTERRUPTIBLE) fprintk(3,"%ld\t%c\t%ld\n",current->pid,&#39;W&#39;,jiffies); current->state &#61; TASK_UNINTERRUPTIBLE; schedule(); if (tmp) { if(tmp->state !&#61; 0) fprintk(3,"%ld\t%c\t%ld\n",tmp->pid,&#39;J&#39;,jiffies); tmp->state&#61;0; } } wake_up() 用于唤醒进程 void wake_up(struct task_struct **p) { if (p && *p) { if((**p).state !&#61;0) fprintk(3,"%ld\t%c\t%ld\n",(**p).pid,&#39;J&#39;,jiffies); (**p).state&#61;0; *p&#61;NULL; } } 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&#61;0 ; i if (task[i] && task[i]->father &#61;&#61; current->pid) { task[i]->father &#61; 1; if (task[i]->state &#61;&#61; TASK_ZOMBIE) /* assumption task[1] is always init */ (void) send_sig(SIGCHLD, task[1], 1); } for (i&#61;0 ; i if (current->filp[i]) sys_close(i); iput(current->pwd); current->pwd&#61;NULL; iput(current->root); current->root&#61;NULL; iput(current->executable); current->executable&#61;NULL; if (current->leader && current->tty >&#61; 0) tty_table[current->tty].pgrp &#61; 0; if (last_task_used_math &#61;&#61; current) last_task_used_math &#61; NULL; if (current->leader) kill_session(); current->state &#61; TASK_ZOMBIE; fprintk(3, "%ld\t%c\t%ld\n", current->pid, &#39;E&#39;, jiffies); current->exit_code &#61; code; tell_father(current->father); schedule(); return (-1);/* just to suppress warnings */ } sys_waitpid() 父进程等待子进程结束 if (flag) { if (options & WNOHANG) return 0; current->state&#61;TASK_INTERRUPTIBLE; fprintk(3, "%ld\t%c\t%ld\n", current->pid, &#39;W&#39;, jiffies); schedule(); if (!(current->signal &&#61; ~(1< goto repeat; else return -EINTR; } 调度算法的修改 修改时间片即可&#xff0c;由老师提示可知 count priority的初值为15个系统滴答&#xff0c;而系统滴答的定义老师也给出了具体解释和位置。 最简单的修改方法就是修改 HZ的值即可。 编写样本程序 process.c ... int main(int argc, char * argv[]) { pid_t child[5]; int i; for (i &#61; 0; i <5; i&#43;&#43;) { child[i] &#61; fork(); if (child[i] <0) { printf("An error occurred in forking!\n"); return 1; } if (child[i] &#61;&#61; 0) { cpuio_bound(4, i, 4 - i); return 0; } printf("Child Process ID: %d\n", child[i]); } for (i &#61; 0; i <5; i&#43;&#43;) { wait(NULL); printf("%d over.\n", i); } return 0; } ... 数据收集 在linux0.11 依次运行 process->eixt->sync&#xff0c;即可收集到修改时间片前的log和修改时间片之后的log。 收集数据如下&#xff1a; 1N48 新建进程1 1J48 进程1就绪 0J48 1R48 进程0被进程1抢占 2N49 进程1生成进程2 2J49 1W49 进程1主动等待 2R49 进程2运行 3N64 3J64 2J64 3R64 3W68 2R68 2E73 1J73 1R73 4N74 4J74 问题回答 1. 结合自己的体会&#xff0c;谈谈从程序设计者的角度看&#xff0c;单进程编程和多进程编程最大的区别是什么&#xff1f; 并发技术&#xff0c;是可以让计算机在同一时间同时执行多条任务的技术。多线程、多进程技术都满足并发。多进程编程主要内容包括进程控制和进程之间的通信。单进程(单线程或多线程)模型采用全异步的模式处理请求&#xff0c;做到高并发量、高吞吐量。多进程相对于多线程&#xff0c;优势在于任务独立。线程崩溃会影响整个进程&#xff0c;其他线程也无法继续进行。多进程编程则不受其他任务影响。多进程的资源分配方案相比单进程更自由&#xff0c;而任务间的通信也更复杂。 2. 你是如何修改时间片的&#xff1f;仅针对样本程序建立的进程&#xff0c;在修改时间片前后&#xff0c;log文件的统计结果(不包括Graphic)都是什么样&#xff1f;结合你的修改分析一下为什么会这样变化&#xff0c;或者为什么没变化&#xff1f; 修改了include/linux/sched.h中 INIT_TASK.priority 的值&#xff0c;分别为 15 和 30。 log1 INIT_TASK.priority &#61;&#61; 15 (Unit: tick) Process Turnaround Waiting CPU Burst I/O Burst 0 20741 67 8 378 1 26 0 2 24 2 24 4 20 0 3 3003 0 4 2999 4 20736 11 53 20672 5 3 1 2 0 6 266 0 14 252 7 51 0 51 0 8 98 1 97 0 9 26 1 25 0 10 80 1 79 0 11 3 0 3 0 12 6 1 5 0 13 3 0 3 0 14 1024 0 7 1017 15 494 64 0 430 16 738 318 100 320 17 933 522 200 211 18 1018 617 300 101 19 1002 602 400 0 20 3 0 3 0 21 1 1 0 0 Average: 2285.41 100.50 Throughout: 0.11/s log2 INIT_TASK.priority &#61;&#61; 30 (Unit: tick) Process Turnaround Waiting CPU Burst I/O Burst 0 7000 67 8 455 1 26 6 1 19 2 19 0 19 0 3 3009 4 6 2999 4 7013 6 49 6958 5 3 0 3 0 6 266 2 13 251 7 51 1 50 0 8 97 0 97 0 9 25 0 25 0 10 80 1 79 0 11 3 0 3 0 12 1038 0 7 1031 13 614 124 0 490 14 813 363 100 350 15 962 522 200 240 16 1032 631 300 101 17 1000 600 400 0 18 4 0 3 0 19 0 0 0 0 Average: 1152.75 116.35 Throughout: 0.28/s 分析数据 观察上述两组数据&#xff0c;可以看出&#xff1a;第一&#xff0c;系统中进程0、1以及shell进程是占用系统最多的进程&#xff0c;也是系统中必须存在的进程&#xff0c;也可以得出实际上系统大部分时间都在等待中。第二&#xff0c;优先级发生变化&#xff0c;平均运行滴答数缩小到了二分之一&#xff0c;等待滴答数浮动不大&#xff0c;吞吐量增加到了大约二倍。由于操作时间不同&#xff0c;造成的数据结果也不一致。