进程笔记 - 下载本文

#define TASK_UNINTERRUPTIBLE 2

#define TASK_ZOMBIE 4

僵死状态。进程的执行被终止,但还未完全退出时的状态。系统总会假设一个进程退出前其父进程始终对其感兴趣,所以当进程被终止时,系统会通知父进程来收集相关信息,当父进程收集完毕后(发布wait()调用后),进程废除过程才会继续进行以丢弃数据。(如果子进程退出时,父进程已经不存在了,在父进程的工作会由init进程代理)

#define TASK_STOPPED 8

暂停状态。有一些信号可以让进程暂停被执行而处于暂停状态,比如SIGSTOP (Stop executing 不可中断的休眠状态。与TASK_INTERUPTIBLE类似,但不同的是其不会响应信号,其会一直等待直到条件得到满足。

temporarily), SIGTSTP (Terminal stop signal), SIGTTIN (Background process attempting to read

from tty), SIGTTOU (Background process attempting to write to tty).另外,当一个进程被另外一个进程监视(ptrace)时,任何信号都会让被监视进程处于TASK_STOPPED状态(一个典型例子是程序的DEBUG时,被调试程序就经常处于该状态,看到有的资料上也将该状态分离出来,叫着TASK_TRACED)

结合下面这个图我们可以看看一个进程的生命周期:

fork () New Process context switch (in/out) Run in User Mode Ready to Run system call, interrupt/return Zombie wake up context switch (in/out) Run in Kernel Mode exit Sleeping sleep interrupt/return 调用fork函数而创建一个新进程后,如果紧接着调用了exec族函数的话,新进程会被放置到可执行队列等待被CPU执行,如果仅仅是调用fork函数,那么新进程的状态和父进程的状态完全相同(Run in User Mode 或Run in Kernel Mode)。

当进程在用户模式运行时,如果发生了系统调用,则进程会进入到内核模式(Run in Kernel Mode)。假设此时的是一个磁盘文件读取操作,而该操作比较慢,进程将放弃CPU而进入休眠状态(Sleeping)并且时机成熟(读取完成,数据就绪)而被唤醒。

当进程退出(比如调用exit()函数)后,其会变成僵死进程而进入Zombie状态。

11

9,进程调度

在分时系统中,系统将CPU时间划分成无数个时间片(quantum)分配给不同的进程,一个时间片只执行一个进程,并且不停地切换,以让用户感觉到各个进程是在“同时运行”,这中间所需要的策略和算法便是进程调度。

一个很好的例子是:假设目前系统只运行了两个进程,一个解压缩程序和一个文本编辑器。由于解压缩程序会大量地占用CPU而不是I/O设备(将解压后的文件写入磁盘),所以对解压缩程序而言CPU是其最大的影响因素,我们将这类程序归类为“CPU-Bound(CPU限制型)”。与之相对应的,文本编辑器对CPU的占用很少,其大多数时间时在等待用户输入,我们将其归类为“I/O-Bound(I/O限制型)”。为了让系统响应对用户更友好,Linux的设计者更倾向于调高I/O-Bound类型进程的优先级。如果现在是解压缩程序正在被CPU执行,而文本编辑器正在等待用户输入而处于休眠状态(TASK_INTERRUPTIBLE),当用户在文本编辑器中敲击键盘时,系统将产生一个中断(interrupt),文本编辑器进程将被唤醒,同时内核会意识到文本编辑器比解压缩进程具有更高的优先级,所以它会将解压缩进程的进程描述符(PCB)中的need_resched字段设成1,表示需要调度器(scheduler)重新调度。当中断处理完成以后,调度器会重新调度并选中文本编辑器进程进行任务切换(taskswitch,context switch),解压缩进程对CPU的拥有权被抢占(preempted,被抢占并不意味着被挂起,而事实上此时解压缩程序仍然处于TASK_RUNNING状态,表示“可执行”),进而执行文本编辑器程序以显示用户敲入的字符。当文本编辑器将此次击键处理完毕后,其将主动放弃CPU而再次进入休眠状态以等待用户的下次键入,调度程序将再次将解压缩进程选中并交给CPU执行。

先看看PCB中和进程调度相关的几个字段: ?

struct list_head run_list;

系统维护着一个可执行队列(runqueue),所有TASK_RUNNING状态的进程都会被记录在其中。在使用调度程序进行调度时,调度程序会在该队列中选取一个合适的进程来与当前进程进行切换。 ?

long nice;

nice值作用之一用于用户修正进程的优先级。增加nice值会降低进程的优先级,反之则升高优先级。另外一个作用是影响进程的时间片长度,nice值越小,时间片长度会增加,反之减少。(注:2.4以下的内核中priority字段被nice取代) nice值的范围是 -20 ~ 19 ?

long counter;

一个计数器,用于记录进程在当前时间片内还允许被执行的时间(单位tick,滴答)。counter的初始值就是该进程的时间片长度,当值为0时说明其时间片耗尽。只能等待下一个调度周期从新分配。 ?

unsigned long rt_priority;

进程可分为实时进程和非实时进程(普通进程),它们的优先级策略不一样,实时进程采用“静态优先级”的形式,让进程拥有一个不可变的优先级值,调度器永远不改变它。普通进程采用“动态优先级”的形式,其优先级值是在调度时通过一系列参数计算出来的。 rt_priority表示实时进程的静态优先级,范围在1~99,如果是普通进程则该字段值为0 ?

unsigned long policy;

这就是常说的调度策略,实时进程和非实时进程(普通进程)分享CPU的方式是不一样的。实时进程采用CHED_FIFO与SCHED_RR实时调度策略,普通进程只有SCHED_OTHER分时调度策略。

#define SCHED_OTHER 0

12

分时调度策略,其采用优先级等形式,让CPU分配尽量公平。 #define SCHED_FIFO 1

First In First Out, 进程根据静态优先级进行排队,如果优先级相同的话,则谁先ready就调度谁。被调度的进程将一直占用CPU直到:被高优先级进程抢占或者等待资源为休眠或者主动放弃CPU

#define SCHED_RR 2

Round Robin(轮询),其和SCHED_FIFO很类似,但其会给进程分配时间片,时间片到则切换进程

另外,在策略中有一个SCHED_YIELD位(#define SCHED_YIELD 0x10),该位被设定时则表示进程主动放弃CPU if (policy & SCHED_YIELD) ?

//give up CPU

volatile long need_resched;

该字段值用于指示是否请求激活调度程序重新进行调度。调度器的启动方式有“主动式”和“被动式”两种,need_resched字段用于被动式启动,稍候会提到。 ?

struct mm_struct *mm;

mm_struct数据结构是描述内存存储信息的数据结构,进程控制块task_struct中用mm指针指想mm_struct数据结构.也就是在进程的属性中通过mm指针来管理起对应的内存区.

调度工作大部分是在schedule()函数中完成的,其会遍历可执行队列中的进程,并调用一个名为goodness()的函数来计算进程的权值(weight),权值大的优先级就高,优先级高的进程会被调度。 计算进程p的权值:

static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm) {

int weight; /*

* weight为-1则表示没有其他进程就绪. */ weight = -1;

/*

*如果p的策略SCHED_YIELD位被设置,则表示p主动放弃CPU */

if (p->policy & SCHED_YIELD) goto out; /*

*普通进程. */

if (p->policy == SCHED_OTHER) { /*

* 首先将该进程剩余的时间片赋值给weight */

13

weight = p->counter; if (!weight) goto out;

#ifdef CONFIG_SMP /*

* 如果不是跨CPU调度的话,当前CPU中的一些缓存数据还可以使用 */

if (p->processor == this_cpu)

weight += PROC_CHANGE_PENALTY; #endif /*

*对于不需要切换内存的进程也给一定的优惠权值+1 * 有两种情况不需要切换内存:

* 1. p和当前进程共享内存(比如同一进程内的线程:轻量级进程) * 2. p是内核进程,没有mm属性 */

if (p->mm == this_mm || !p->mm) weight += 1;

/*

*让进程的nice值参与权值运算,由于nice值越小权值应该越大,并且nice * 值的范围是-20~19所以用20-nice */

* 所以给非跨CPU调度的进程一定的“优惠”: * 权值增加一个常量值:PROC_CHANGE_PENALTY

weight += 20 - p->nice; goto out; } /*

* 实时进程的计算方式十分简单:直接用rt_priority字段。

* 1000是为了让实时的权值大于任何普通进程

*/

weight = 1000 + p->rt_priority; out:

return weight; }

调度器选定新的进程后,需要进行新进程和旧进程的切换工作(Context Switch),分为两个方面:切换内存(switch_mm)和切换cpu寄存器(switch_to)

关于切换内存:内核空间的进程的mm属性为空没必要切换,这是因为内核进程是轻量级进程(线程),它们是共享内核空间的,对于两个普通的进程则调用switch_mm()函数进行内存切

14

换(至于如何切换,这里略过,我还不懂linux内存管理,以后更新) 而switch_to方法,是一段汇编代码(看看就好): #define switch_to(prev,next,last) do { \\ asm volatile(\ \\ \ \\

\ \\保存esi、edi、ebp寄存器 \ \\esp保存到prev->thread.esp中 \ \\从next->thread.esp恢复esp \

\\在prev->thread.eip中保存\:\的跳转地址, \\当prev被再次切换到的时候将从那里开始执行 \

\\在栈上保存next->thread.eip,__switch_to()返回时将转到那里执行, \\即进入next进程的上下文

\ \\跳转到__switch_to(),进一步处理(见下) \ \\ \ \\ \ \\

\ \\先恢复上次被切换走时保存的寄存器值, \\再从switch_to()中返回。 :\ \\%0

\ \,

\\因为进程切换后,恢复的栈上的prev信息不是刚被切换走的进程描述符, \\因此此处使用ebx寄存器传递该值给prev :\ \\%3

\ \\%4 \ \\eax,edx \ \\ebx } while (0)

启动调度器有两种方式:一是进程为等待核心事件而主动调用schedule()函数将自己挂起以让出CPU,这称为“主动式”;二是不直接调用schedule()函数,而是设置前面提到的need_resched字段,当进程由内核态返回到用户态时(内核2.6中,包括由用户态到内核态时),系统检查need_resched字段,如果其被设置,那么schedule函数将被调用,这称为“被动式”。

10,进程控制和常用操作 ?

退出/终止进程

void _exit(int status) 与 void exit(int status) 这两个函数都是让进程退出,参数status表示进程将以何种状态退出,在中预定义了一些状态,比如EXIT_SUCCESS(值为0)表示以成功状态退出,EXIT_FAILURE(值为1)表示以失败状态

15