目录
2.3.2 Linux 的进程控制块task_struct9
浅析Linux 与Minix下进程实现的异同
摘要:为了深刻描述程序动态执行过程的性质,人们引入“进程(Process)”概念。现今,进程是操作系统结构的基础。本文主要论述Linux 与Minix 操作系统下进程的实现原理,并给出二者的异同点。
关键字: Linux ; Minix; 操作系统; 进程
1. 引言
第一部计算机并没有操作系统。这是由于早期个人电脑的建立方式(如同建造机械算盘)与效能不足以执行如此程序。但在1947年发明了晶体管,以及莫里斯·威尔克斯(Maurice Vincent Wilkes)发明的微程序方法,使得电脑不再是机械设备,而是电子产品。系统管理工具以及简化硬件操作流程的程序很快就出现了,且成为操作系统的基础。
操作系统(英语:Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。操作系统的种类相当多,各种设备安装的操作系统可从简单到复杂,可分为智能卡操作系统、实时操作系统、传感器节点操作系统、嵌入式操作系统、个人计算机操作系统、多处理器操作系统、网络操作系统和大型机操作系统。按应用领域划分主要有三种:桌面操作系统、服务器操作系统和嵌入式操作系统。
本文的论述是基于Minix和Linux操作系统。
1.1 Minix 简介
Minix是一种基于微内核架构的类UNIX计算机操作系统,由Andrew S. Tanenbaum发明。Minix最初发布于1987年,开放全部源代码给大学教学和研究工作。2000年重新改为BSD授权,成为自由和开放源码软件。Minix为全球注册商标。
Minix的名称取自英语Mini UNIX,是一个迷你版本的类Unix操作系统(约300MB),其它类似的系统还有Idris,Coherent和Uniflex等。这些类Unix操作系统都是重新发展的,并没有使用任何AT&T的程序码[2]。
1.2 Linux简介
Linux是一种自由和开放源码的类Unix操作系统,存在着许多不同的Linux版本,但它们都使用了Linux内核。Linux可安装在各种计算机硬件设备中,比如手机、平板电脑、路由器、视频游戏控制台、台式计算机、大型机和超级计算机。Linux是一个领先的操作系统,世界上运算最快的10台超级计算机运行的都是Linux操作系统。严格来讲,Linux这个词本身只表示Linux内核,但实际上人们已经习惯了用Linux来形容整个基于Linux内核,并且使用GNU 工程各种工具和数据库的操作系统。Linux得名于天才程序员林纳斯·托瓦兹[1]。
Linux是其作者受到Minix的影响而作成的(Linus Torvalds不喜欢他的386电脑上的MS-DOS操作系统,安装了Minix,并以它为样本开发了原始的Linux内核)。但在设计哲学上,Linux则和Minix大相迳庭。Minix在内核设计上采用微内核的原则,但Linux则和原始的Unix相同都采用宏内核的概念。在Linux发展之初,双方还于1992年在新闻组上有过一场精彩的理念争论。Minix的作者和支持者认为Linux的单内核构造是“向七十年代的大倒退”,而Linux的支持者认为Minix本身没有实用性。
Linux 快速从一个个人项目进化成为一个全球数千人参与的开发项目。对于 Linux来说,最为重要的决策之一是采用 GPL(GNU GeneralPublic License)。在 GPL 保护之下,Linux 内核可以防止商业使用,并且它还从 GNU 项目(Richard Stallman 开发,其源代码要比 Linux 内核大得多)的用户空间开发受益。这允许使用一些非常有用的应用程序,例如GCC(GNU CompilerCollection)和各种 shell 支持[3]。
1.3 进程简介
多道程序在执行时,需要共享系统资源,从而导致各程序在执行过程中出现相互制约的关系,程序的执行表现出间断性的特征。这些特征都是在程序的执行过程中发生的,是动态的过程,而传统的程序本身是一组指令的集合,是一个静态的概念,无法描述程序在内存中的执行情况,即我们无法从程序的字面上看出它何时执行,何时停顿,也无法看出它与其它执行程序的关系,因此,程序这个静态概念已不能如实反映程序并发执行过程的特征。为了深刻描述程序动态执行过程的性质,人们引入“进程(Process)”概念。进程是60年代初首先由麻省理工学院的MULTICS系统和IBM公司的CTSS/360系统引入的。
进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。
2. Minix和Linux中进程的实现
2.1 进程的概述
一.操作系统引入进程的概念的原因:
1. 从理论角度看,是对正在运行的程序过程的抽象;
2. 从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
二.进程的特征:
1. 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
2. 并发性:任何进程都可以同其他进程一起并发执行
3. 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
4. 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
5. 结构性:进程由程序段、数据段和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
三.一个计算机系统进程包括(或者说“拥有”)下列数据:
那个程序的可运行机器码的一个在存储器的映像。分配到的存储器(通常包括虚拟内存的一个区域)。存储器的内容包括可运行代码、特定于进程的数据(输入、输出)、调用堆栈、堆栈(用于保存运行时运数中途产生的数据)。分配给该进程的资源的操作系统描述符,诸如文件描述符(Unix术语)或文件句柄(Windows)、数据源和数据终端。 安全特性,诸如进程拥有者和进程的权限集(可以容许的操作)。处理器状态(内文),诸如寄存器内容、物理存储器寻址等。当进程正在运行时,状态通常储存在寄存器,其他情况在存储器。
四.进程的切换:
进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。让进程来占用处理器,实质上是把某个进程存放在私有堆栈中寄存器的数据(前一次本进程被中止时的中间数据)再恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针PC,于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程 的上下文是存储在进程的私有堆栈中的。
五.进程的状态:
进程执行时的间断性,决定了进程可能具有多种状态。事实上,运行中的进程可能具有以下三种基本状态。程可以划分为运行、阻塞、就绪三种状态,并随一定条件而相互转化:就绪--运行,运行--阻塞,阻塞--就绪。
1.就绪状态(Ready):
进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
2.运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
3.阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理机分配给该进程,也无法运行。
2.2 Minix进程的实现
2.2.1 Minix的结构
Minix与Unix不同,Unix的核心是一个不分模块的单块程序,而Minix本身就是一组进程的集合,它们相互之间、以及与用户进程之间使用进程间通信机制 - 消息传递来进行通信。这种设计使得Minix的结构更加模块化和灵活。例如,这使得很容易就可以将整个文件系统替换成另一个完全不同的文件系统,而无需重新编译核心。
开始研究Minix之前,我们首先大概浏览一下整个系统。Minix被组织成4层,每一层执行一套定义得很完好的功能。
图1 Minix的四层结构
最底层捕获所有的中断和陷入,完成进程调度,并向高层提供一个采用消息进行通信的独立顺序进程模型。该层的代码有两大主要功能。第一是捕获陷入和中断、保存和恢复寄存器、调度以及向高层提供一个独立顺序进程模型。第二是处理消息机制:检查目标进程的合法性、定位物理内存中的发送和接收缓冲区、以及从发送方向接收方拷贝数据。其中中断处理的最底层部分用汇编语言编写,其余部分和其他层次用C语言编写。
第二层包括I/O进程,每类设备都有一个I/O进程。为了将其与普通用户进程相区别,我们称之为任务(tasks)。但任务与进程间的差别微乎其微。在许多系统中I/O任务被称作设备驱动程序(device driver)。这里“任务”和“设备驱动程序”可以换用。每一类设备都需要一个任务,包括磁盘、打印机、终端、网络接口以及时钟。如果有其他I/O设备,则它们也需要相应的任务。有一个任务 - 系统任务有些与众不同,它不对应于任何I/O设备。第二层的所有任务和第一层的代码链接成一个单一的二进制程序,称作核心(kernel)。某些任务共享公共的子例程,但它们相互之间完全独立,分别进行调度,并采用消息进行通信。从286开始的Intel处理器为每个进程赋予四种特权级中的一种。尽管任务与核心被编译在一起,但当核心和中断处理程序被执行时,它们被赋予较任务更高的特权级。所以真正的核心代码可以访问任一部分内存,以及任一处理器寄存器 - 实质上,核心可以使用系统中任何地方的数据执行任何指令。任务不能执行全部的机器指令,也不能访问所有CPU寄存器或所有的内存。但在为较低特权级的进程执行I/O时,任务可以访问属于这些进程的内存区域。有一个任务 - 系统任务,它并不执行一般意义的I/O,其作用提供某些特定服务,例如当进程本身不允许在不同的内存区域间进行拷贝时,由系统任务执行此操作。当然在不提供多特权级的机器上,例如老式的Intel处理器,无法强制执行这些限制。
第三层包含向用户进程提供有用服务的进程。这些服务器进程在低于核心和任务的特权级上运行,不能直接访问I/O端口。它们也不能访问属于自己的段以外的内存。内存管理器(Memory Manager - MM)负责执行所有牵涉到内存管理的系统调用,如FORK、EXEC和BRK。文件系统(File System - FS)负责执行文件系统的调用,READ、MOUNT和CHDIR。
最后,第四层包含所有的用户进程 - shell、编译器、编辑器以及用户的a.out程序。一个运行系统通常有一些进程在系统引导时启动,并一直运行,例如,一个精灵程序就是一个周期性运行或总是等待某个事件(例如网络上一个包的到达)的后台进程。从某种意义上说,精灵进程是一个单独启动而作为一个用户进程运行的服务器。但与装入特权进程表项的真正的服务器不同,这些程序无法象内存和文件服务器那样受到核心的特殊对待。
2.2.2 Minix中进程机制
(1) Minix中进程的管理
进程可以创建子进程,子进程又可以创建更多的子进程,这样便构造出一棵进程树。实际上,整个系统中所有的用户进程都属于以init为根节点的一棵进程树。Minix用于进程管理的两条最重要的系统调用是FORK和EXEC。FORK是创建一个新进程的唯一途径。EXEC允许一个进程执行一个指定的程序,当一个程序被执行时,将按照文件头中指定的大小为其分配一部分内存。尽管在进程运行期间,数据段、栈段和空闲未使用部分的大小可以不时地改变,但进程分配到的内存总量将保持不变。一个进程的所有信息被保存在进程表中,进程表划分成核心、内存管理器和文件系统三部分,分别拥有它们各自所需要的那些域。当出现一个新进程(通过FORK),或者一个老进程结束(通过EXIT或信号)时,内存管理器首先更新它那部分进程表,然后向文件系统和核心发送消息,以通知它们进行相应的操作。
(2)Minix中进程间的通信
Minix提供了三条原语来发送和接收消息,它们均通过C库例程调用。其中send(dest,&message)用来向进程dest发送一条消息,receive(source,&message)用来从进程source(或任何地方)接收一条消息,send_rec(src_dst,&message)用来发送一条消息,并等待同一个进程的应答。以上调用中第二个参数是消息数据的本地地址。核心中的消息传递机制将消息从发送者拷贝到接收者。应答消息(对于send_rec)将覆盖原先的消息。每个进程或任务都可以从/向同层和下一层中的进程或任务发送和接收消息,用户进程不能直接与I/O任务通信,系统强制地执行这一限制。当一个进程(作为特例,这里也包括任务)向一个当前未在等待消息的进程发送一条消息时,发送者将阻塞,直到目标进程执行receive。换言之,Minix使用会合的方法来避免对已发送而未接收到的消息进行缓冲的问题。尽管这没有带缓冲的方案灵活,但事实证明对Minix来说它已经足够了,而且由于不需要缓冲管理,所以简单许多。
(3)Minix中的进程调度
中断系统使多道程序操作系统持续不断地工作。当进程请求输入时,它们将阻塞以允许其他进程执行。当输入可用时,当前运行进程被磁盘、键盘或其他硬件中断。时钟也产生中断,这种中断使正在运行的未请求输入的用户进程最终放弃CPU,以使其他进程获得运行的机会。Minix最底层软件的任务就是通过将中断转换成消息来对其加以隐藏。就进程(以及任务)而言,当一个I/O设备完成一个操作时,它向某些进程发送一条消息,将其唤醒并使之成为就绪。
每当一个进程被中断时,不管中断源是常规的I/O设备还是时钟,都有机会重新确定哪个进程最需要运行机会。当然,在一个进程终止时也要执行该操作,但在类似Minix这样的系统中,由I/O操作和时钟引起的中断远远多于进程终止的情况。Minix调度程序使用一个多级排队系统,一共定义了16个队列[2]。任务和服务器一级的进程一直运行直到阻塞,而用户进程则采用时间片轮转调度。任务具有最高优先级,内存管理器和文件管理器次之,用户进程最低。
当调度程序选择一个进程来运行时,它首先检查是否有就绪的任务,如果有一个或多个,则队首的那个将运行。如果没有任务就绪,则检查并运行服务器进程(MM或FS)。若没有合适的服务器进程,则运行一个用户进程。如果没有进程就绪,则选择IDLE进程。这个循环一直执行到下一个中断到来。在每一个时钟滴答,都将检查当前进程是否是一个运行超过100毫秒的用户进程。如果是,则调用调度程序来查看是否有另一个用户进程在等待CPU,如果发现一个这样的进程,则当前进程被移到队列的末尾,而运行当前的队首进程。任务、内存管理器和文件系统不会被时钟剥夺,不论它们已运行了多久。
2.2.3 Minix中进程的数据结构
kernel/proc.h定义了内核进程表。一个进程的完整状态包括内存中的进程数据和进程表项中的信息。当一个进程没有执行时CPU寄存器内容就存储在这里,恢复执行时则重新存储。进程表中的每一项被定义为一个proc进程:
struct proc {
structstackframe_sp_reg;/*process' registers saved in stack frame */
reg_tp_ldt_sel;/*selectorin gdt with ldt base and limit */
structsegdesc_sp_ldt[2+NR_REMOTE_SEGS]; /* CS, DS and remote segments */
proc_nr_tp_nr;/*numberof this process (for fast access) */
structpriv*p_priv;/*system privileges structure */
charp_rts_flags;/*SENDING,RECEIVING, etc. */
charp_priority;/*currentscheduling priority */
charp_max_priority;/*maximumscheduling priority */
charp_ticks_left;/*numberof scheduling ticks left */
charp_quantum_size;/*quantumsize in ticks */
structmem_mapp_memmap[NR_LOCAL_SEGS]; /* memory map (T, D, S) */
clock_tp_user_time;/*usertime in ticks */
clock_tp_sys_time;/*systime in ticks */
structproc*p_nextready;/*pointer to next ready process */
structproc*p_caller_q;/*head of list of procs wishing to send */
structproc*p_q_link;/*link to next proc wishing to send */
message*p_messbuf;/*pointerto passed message buffer */
proc_nr_tp_getfrom;/*fromwhom does process want to receive? */
proc_nr_tp_sendto;/*towhom does process want to send? */
sigset_tp_pending;/*bitmap for pending kernel signals */
charp_name[P_NAME_LEN];/*nameof the process, including \0 */
};
每一项包括进程寄存器、栈指针、状态值、内存映射、栈限制、进程号、计数值、alarm时间以及消息信息。进程表项的第一部分是stackframe_s,该结构体的定义在kernel/type.h中,内存中的进程通过将它的栈指针赋予进程表项的地址来投入运行,并且将该结构弹出到CPU寄存器。进程表项中还有一个指向priv结构的指针,priv结构在kerenl/priv.h中有定义,它包含了消息允许的源(source)和目的(destination),以及其他一些特权。每一个进程都有指向其副本的一个指针,用户的权限与指向同一副本的用户进程的指针相同。p_max_priority是一个进程允许的最大优先级,表示一个进程首次处于就绪状态时需要放入哪个调试队列。因为如果进程阻碍了其他进行运行,那么它的优先级就会降低。p_priority被初始化为与p_max_priority相等,每次进程就绪时实际上是由p_priority决定使用哪个队列。p_ticks_left记录进程剩余的时间片,p_quantum_size记录分配给进程的时间片。p_user_time和p_sys_time记录每个进程所用的时间。
p_nextready指向下一个准备就绪的进程。当进程由于目标未在等待不能完成send时,发送者将被放置在目标的p_caller_q指针指向的队列中,这样目标最终进行receive时,它就可以很容易地找到要向它发送消息的进程。p_q_link用于将队列成员连接在一起。当一个进程执行了receive而没以被接收的消息时,它将阻塞,而它想要接收信号的进程号存储在p_getfrom中。同样地,当一个进程执行了send而没有接收者需要时,p_sendto存储目标进程号。消息地址缓存在p_messbuf中。p_pending使用一个位映射来跟踪那些没有被发送进程管理器的信号(因为进程管理器未在等待该进程)。数组p_name用来存储进程名称。
进程间通信的数据结构和函数原型在include/Minix/ipc.h中,该文件中定义了一个message结构体:
typedef struct{
intm_source;/*whosent the message */
intm_type;/*whatkind of message is it */
union {
mess_1m_m1;
mess_2m_m2;
mess_3m_m3;
mess_4m_m4;
mess_5m_m5;
mess_7m_m7;
mess_8m_m8;
} m_u;
} message;
其中包括消息源、消息的类型和以一个联合体m_u表示的消息的内容。消息内容一共有7种格式,比如要发送的消息包含3个整数和3个指针时使用m_m1,传递3个int,2个long,1个指针时使用m_m2,依次类推。
kernel/glo.h中包含了与进程控制和内核执行有关的一些变量:
/* Processschedulinginformation and the kernel reentry count. */
EXTERNstructproc *prev_ptr;/*previously running process */
EXTERNstructproc *proc_ptr;/*pointer to currently running process */
EXTERNstructproc *next_ptr;/*next process to run after restart() */
EXTERNstructproc *bill_ptr;/*process to bill for clock ticks */
EXTERNchark_reenter;/*kernel reentry count (entry count less 1) */
EXTERNunsignedlost_ticks;/*clock ticks counted outside clock task */
prev_ptr,proc_ptr,next_ptr分别指向之前、当前、之后的进程的进程表项。bill_ptr指示哪个进程来为时钟买单(billfor clockticks),举个例子,当用户进程调用文件系统,而文件系统正在运行时,proc_ptr指向该文件系统进程,而bill_ptr则指向进行调用的用户进程,那就是说用户进程要为时钟买单,即文件系统所用的CPU时间长度将从调用者拥有的系统时间中扣除。k_reenter记录重新进入内核的次数,即内核代码的嵌套执行次数。
kernel/protect.h中几乎所有内容都与支持保护模式的Intel处理器(80286、80386、80486、奔腾系列)体系结构细节相关。
/* Privileges.*/
#defineINTR_PRIVILEGE0/*kernel and interrupt handlers */
#defineTASK_PRIVILEGE1/*kernel tasks */
#defineUSER_PRIVILEGE3/*servers and user processes */
32位的Intel处理器提供了四种优先级别(privilegelevels),Minix3使用其中的三种。内核的最中心部分即运行于中断处理期间的部分和切换进程的部分运行在INTR_PRIVILEGE特权级,它们可以访问全部的内存空间和全部的CPU寄存器。系统任务运行在TASK_PRIVILEGE特权级上,它们被允许访问I/O,但不能使用那些修改特殊寄存器(如指向描述符表的寄存器)的指令。服务器进程和用户进程运行在USER_PRIVILEGE特权级,它们不能访问I/O端口、改变内存分配状态或改变处理器运行级别等。
Minix3使用一种多级调度算法,事实上调度器维护16个可运行的进程队列。时钟和系统任务拥有最高的优先级,设备驱动器获得低一些优先级,但它们的优先级不完全相同,用户进程的优先级最低,并且在启动时被赋予相同的初始值,通过nice命令可以提高或降低某个进程的优先级。系统初始化的过程中初始进程的排除情况由table.c中的image表决定。在kernel/table.c中image表提供了初始化所有从引导映像加载的进程所需的细节部分。
进程IDLE总是处于就绪状态,并且位于最低优先级别队列中。“qs”域展示了分配给每个进程的时钟数。通常情况下用户进程如Init子进程运行8个时钟,而时钟和系统任务可以最长运行64个时钟。不像用户空间服务器和驱动程序,时钟和系统任务即使阻塞了其他进程的运行机会,其优先级也不会降低。每个队列内部都采用时间片轮转调试算法。如果一个运行进程的时间片用完了,则它移到队列尾部并分配一个新的时间片。而当一个阻塞的队列被唤醒时,如果阻塞前没有用完时间片,则它将被放到队首。它并不会得到一个新的时间片,而只能得到阻塞时剩余的时间片。数组rdy_tail使得向队列尾部加一个进程变得非常简单。当一个运行的进程被阻塞或者被一个信号杀死时,它将被移出队列,调度队列中仅有可运行的进程。
2.3 Linux 中进程的实现
2.3.1 Linux中的进程
Linux 作为一个多用户操作系统,支持多道程序设计,支持分时处理和“软”实时处理,也带有某些微内核的特征,为实现上述目标,当然要有进程。Linux 进程符合一般操作系统教科书中的进程概念的解释。在 Linux 中,进程仍是最小调度单位 。 Linux 中的 PCB 是一个最重要的数据结构,叫做 task_struct ,放在include/linux/sched.h 中,其中包含管理进程所需的方方面面的信息。系统中最多同时可运行NR_TASKS个进程(它是结构数组的大小,默认值为 512)。
在 kernel/sched.c 中定义:struct task_struct * task[NR_TASKS]= {& init-task,};在/include/linux/tasks.h 中有:#define NR_TASKS 512。此外,系统定义了全局变量 nr-tasks,用来记录系统中的进程数,该变量随系统中实际进程数的变化而变化,其定义格式如下:
在/kernel/fork.c 中,int nr-tasks = 1;
每当创建一个新进程时,便在内存中申请一个空的 task-struct 区域,填入所需信息。同时,指向该结构的指针也被加入到 task 数组中。所有进程控制块都存储在 task[]数组中,在系统初始化后期,建立了第一个进程控制块 INIT-TASK。此外,为了便于找到当前正在执行的进程,Linux 中定义了一个current-set 指针数组,其中的每一成员指向某 CPU 上正在运行的进程,该数组定义格式如下:在/kernel/sched.c 中,struct task-struct * current-set[NR-CPUS];因为 Linux 支持多处理机(SMP),所以系统中允许有多个 CPU。显然,在单机系统中,系统中只有一个 CPU,NR-CPUS 定义格式如下:
在/include/linux/tasks.h 中有:
#ifdef_SMP_
#define NR-CPUS32
#else
#define NR-CPUS1
#endif
#definecurrent(0+current-set[smp-processor-id()])
Linux 中有普通进程和实时进程两种,实时进程具有一些紧迫性,对外部事件要做出快速的响应,实时进程的优先级高于普通进程。
2.3.2 Linux 的进程控制块 task_struct
在 Linux 中进程称为任务(task),进程控制块是task_struct 结构,在include/linux/sched.h中定义并解释如下:
structtask_struct{
/*these are hardcoded-don't touch*/
volatile longstate;/*-1 unrunnable,0 runnable,>0 stopped*/
long counter;
long priority;
unsigned longsignal;
unsigned longblocked; /*bitmap of masded signals*/
unsigned longflags; /*per process flags,defined below*/
int errno;
longdebugreg[8];/*Hardware debugging registers*/
/* in order tooffer binary compatibility with other flavors of UNIX,a few
of the kernelinternal tables must be modified. An "execution domain" is a set of
mappings fromthe conventions of another operating system to Linux.
For example,theiBCS2 module defines the execution domain to execute SCO Binary
files*/
structexec_domain *exec_domain;
/*variousfields*/
structlinux_binfmt *binfmt;
structtask_struct *next_task,*prev_task;
structtask_struct *next_run,*prev_run;
unsigned longsaved_kernel_stack;
unsigned longkernel_stack_page;
intexit_code,exit_signal;
unsigned longpersonality;
int dumpable:1;
int did_exec:1;
/*shouldn'tthis be pid_t?*/
int pid;
int pgrp;
inttty_old_pgrp;
int session;
/*Boolean valuefor session group leader*/
int leader;
intgroups[NGROUPS];
/*
*pointers to(original)parentprocess,youngest child,younger sibling,
*oldersibling,respectively.(p->father can be replaced with
*p->p_pptr->pid)
*/
structtask_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_ptr;
structwait_queue *wait_chldexit; /*for wait4()*/
unsigned shortuid,euid,suid,fsuid;
unsigned shortgid,egid,sgid,fsgid;
unsigned longtimeout,policy,rt_priority;
unsigned longit_real_value,it_prof_value,it_virt_value;
unsigned longit_real_incr,it_prof_incr,it_virt_incr;
structtimer_list real_timer;
long utime,stite,cutime,cstime,start_time;
/*mm fault andswap info:this can arguably be seen as either mm-specific or
thread-specific*/
unsigned longmin_flt,maj_flt,nswap,cmin_flt,cmaj_flt,cnswap;
intswappable:1;
unsigned longswap_address;
unsigned longold_maj_flt; /*old value of maj_flt*/
unsigned longdec_flt; /*page fault count of the last time*/
unsigned longswap_cnt; /*number of pages to swap on next pass*/
/*limits*/
struct rlimitrlim[RLIM_NLIMITS];
unsigned shortused_math;
char comm[16];/*the base name of the executable file*/
/*file systeminfo*/
int link_count;
structtty_struct *tty;/*NULL if no tty */
/* ipc stuff */
struct sem_undo*semundo;
structsem_queue *semsleeping;
/* ldt for thistask-used by Wine.If NULL,default_ldt is used */
struct desc_struct*ldt;
/* tss for thistask */
structthread_struct tss;
/* fileststeminformation */
structfs_struct *fs;
/* open fileinformation */
structfiles_struct *files;
/* memorymanagement info */
structmm_struct *mm;
/* signalhandlers */
struct signal_struct*sig;
#ifdef_SMP_
int processor;
intlast_processor;
intlock_depth;/* Lock depth.We can context switch in and out of holding a
syscall kernellock… */
#endif
};
这些数据成员可分为以下几方面内容:
(1)有关进程调度使用的数据成员
1.进程状态(Volatile long state)。Linux 共有 6 种进程状态,它们分别为:
① TASK-RUNNING 可运行态。
② TASK-INTERRUPTIBLE 可中断态。
③ TASK-UNINTERRUPTIBLE 不可中断态。
④ TASK-ZOMBIE 僵死态。
⑤ TASK-STOPPED 暂停态。
⑥ TASK-SWAPPING 交换态。
2.进程标志(Unsigned long flags)。Linux 中进程有 11 项标志,它们分别为:
① PF_ALIGNWARN 打印“对齐”警告信息。
② PF_PTRACED 被 ptrace 系统调用监控。
③ PF_TRACESYS 正在跟踪。
④ PF_FORKNOEXEC 进程刚创建,但还没执行。
⑤ PF_SUPPERPRIV 超级用户特权。
⑥ PF_DUMPCORE dumped core。
⑦ PF_SIGNALED 进程被信号杀出。
⑧ PF_STARTING 进程在被创建。
⑨ PF_EXITING 进程开始关闭。
⑩ PF_USEDFPU 该进程使用 FPU(SMP only)。
11PF_DTRACE delayed trace(used on m68k)。
3.进程优先级(long prioity;)。
4.实时进程优先级(unsigned long rt-priority;)。
5.进程动态优先级计数器(long counter;)用于轮转调度法。
6.调度策略(unsigined long policy;),其中包括:
① SCHED-OTHER 0 普通进程优先数轮转法(Round robin)。
② SCHED-FIFO 1 实时进程先进先出法。
③ SCHED-RR 2 实时进程优先数轮转法。
(2)有关信号处理使用的数据成员
1.进程收到信号(unsigned long signal;),共 32 位,代表 32 种信号置位有效。
2.进程收到信号屏蔽(unsigned long blocked;),置位屏蔽。
3.进程根据 sig 属性选择信号的自定义处理函数(struct signal_struct*sig;)。
(3)进程队列指针数据成员
1.进程数据块双向链表前后向指针(struct task_struct*next_task,*prev_task;)。
链表的头尾都是 0 号进程,即inti-task。
2.就绪队列双向链表的前后向指针(struct task_struct*next_run,*prev_run;)。
头尾指针均为 0 号进程,即init-task。
3.进程家族关系中,指向祖先进程,父进程,子进程及新老进程的指针(structtask_struct
*p_opptr,*p_pptr;和 struct task_struct*p_cptr,*p_ysptr,*p_osptr;)。
p_opptr 指向祖先进程;
p_pptr 指向父进程;
p_cptr 指向子进程;
p_ysptr 指向新兄弟进程;
p-osptr 指向老兄弟进程。
(4)有关进程标识的数据成员
1.用户标识和用户组标识(unsigned short uid,gid;)。
2.用户组标识组号(int groups[NGROUPS];)用以合法性检查。
3.有效用户标识和有效组标识(unsigned shorteuid,egid;)用于系统安全权限。
4.文件系统的用户标识和组标识,用于文件系统操作时合法性检查,它们是(unsigned
shortfsuid,fsgid;)。
5.备份用户标识和备份组标识(unsigned shortsuid,sgid;)。
6.进程标识号,进程组标识号及 session 标识(int pid,pgrp,session;)。
7.是否是 session 的主管(int leader;)。
(5)与时间有关的数据成员
1.进程申请延时(unsigned long timeout;)。用于软件定时,指出进程间隔多久被重新
唤醒,延时单位为 tick。
2.实时定时器(unsigned longit-real-value,it-real-incr;)。不论该进程是否运行都实时更
新,用于软件定时。
3.实时定时器结构(struct timer-listreal-timer;)。
4.虚拟定时器(unsigned longit-virt-value,it-virt-inct;)。只在进程运行于用户态时更新,
用于定时软件。
5.概况定时器(unsigned longit-prof-value,it-prof-inct;)。用于软件定时。
6.longutime,stime,cutime,cstime,start-time;
其中 utime 为进程在用户态的运行时间;
stime 为进程在系统态的运行时间;
cutime 为所有层次子进程在用户态运行时间总和;
cstime 为所有层次子进程在系统态运行时间总和;
start-time 为进程创建启动时间。
(6)与信号量有关的数据成员
1.为避免死锁而设在信号量上的取消操作(struct sem-undo*semundo;)。
2.与信号量操作有关的等待队列(struct sem-queue*semsleeping;)。
(7)与进程上下文有关的数据成员
1.进程关于 CPU 段式存储管理的局部描述表指针(struct desc-struct*ldt;)。
2.任务切换状态段(struct thread-struct tss;)。进程调度时,旧的进程的 TSS 保存到 PCB
的 TSS 中,将选中进程的 TSS 内容复制到 CPU 的 TSS 中。
3.为 MS-DOS 操作系统的仿真程序(或叫系统调用 VM86)保存的堆栈指针(unsigned
longsaved-kernel-stack;)。
4.进程在核心栈的基地址(unsigned longkernel-stack-page;)
(8)与文件系统有关的数据成员
1.保存进程与 VFS 有关的信息(struct fs-struct *fs;)。即 VFS 中的 root 和 pwd 索引节点,分别指向可执行映像所对应的根目录和当前目录。
2.进程打开的文件(struct files-struct *files;)。
3.文件链接数(int link-count;)。
(9)与内存有关的数据成员
Linux 中,每个进程都有自己的虚拟存储空间,所以要涉及到与内存有关的信息。
1.描述进程的虚拟内存(struct mm-struct *mm;)。
2.局部(进程)描述表(struct desc-struct *ldt;)。
3.进程只用的内存页面是否可以换出(int swappable:1;)为 1 表示可以换出。
4.进程下一次可换出的页面起始地址(unsigned longswap-address;)。
5.该进程累计的 minor 和 major 缺页次数(unsigned long min-flt,maj-flt;)。
6.该进程累计换出的页面数(unsigned long nswap;)。
7.以本进程作为祖先进程的所有层次子进程的累计,换入,换出页面计数(unsigned
longcmin-flt,cmaj-flt,cnswap;)。
8.下一次循环最多可换出的页数(unsigned longswap-cnt;)。
(10)支持对称多处理方式(SMP)时的数据成员
1.进程正在使用的 CPU(int processor;)。
2.进程最后一次只用的 CPU(int last-processor;)。
3.上下文切换时系统内核锁的深度(int lock-depth;)。
(11)其他数据成员
1.是否使用 FPU(unsigned short used-math;)。
2.进程正在运行的可执行文件的文件名(char comm[16];)。
3.资源管理结构成员(struct rlimitrlim[RLIM-NLIMITS];)。该结构中有两个成员,一
个是 rlim-cur,表示当前最大资源数目,另一个是 rlim-max 可拥有的资源最大数目。
4.最后一次出错的系统调用的出错号(int errn0; 0 表示无错)。
5.保存在 Intel CPU 调试寄存器的值(long debugreg[8];)共有 8 个寄存器。
6.保存与 UNIX 在 80386 平台有差异的信息(struct exec-domain*exec-domain;)。
7.描述进程执行的程序属于何种 UNIX 平台的个性信息(unsigned long personality;)。
8.指向进程所属的全局执行文件格式结构(structlinux-binfmt *binfmt;)。共有 a.out,
script,elf 和 java 四种。
9.引起进程退出的返回代码和引起错误的信号名(int exit-code,exit-signal;)。
10.出错时是否可以进行 memory dump(int dumpable:1;1 表示可以)。
11.区别进程正在执行旧的程序代码还是由执行 execue 装入的新代码(int did-exec:1;)。
12.进程显示终端所在的组标识(struct tty-old-pgrp;)。
13.指向进程所在的显示终端的信息(struct tty-struct*tty;)。若进程不需要显示终端(如
0 号进程)时,页面指针为空。
14.父进程因 wait 等待子进程结束所处的睡眠队列(strcut wait-queue*wait-childexit;)。
(12)进程队列的全局变量数据成员
1.当前正在运行进程的指针(current)。
2.0 号进程 PCB 始终保持初值INIT-TASK(structtask_struct init-task;)。
3. 进 程 队 列 数 组 , 表 示 系 统 可 同 时 运 行 的 最 大 进 程 数 ( struct
task_struct*task[NR-TASKS];)。
4.Linux 的基准时间(unsigned long volatilejiffies;)。系统初始化时清 0,以后每个 10ms
由时钟中断服务程序增 1。
5.重新调度标志位(int need-resched;置位,写上重新调度)。
6.记录中断服务程序的嵌套层次(unsigned longsitr-count;)。正常运行时该值为 0,当
该值非 0 时不允许重新调度。
2.3.3 Linux中进程的状态及其转换
Linux 中进程有六种状态,它们是:
(1)可运行状态(TASK_RUNNING)。相当于进程三种基本状态中的执行状态和就绪状态,所以是正在运行或准备运行的进程处于这种状态,处于这种状态的进程实际参与进程的调度。
(2)可中断阻塞状态(TASK_INTERRUPTIBLE)。处于这种阻塞状态中的进程,通常只要阻塞的原因解除,比如请求资源未能满足而阻塞,一旦资源满足后,就可以被唤醒到就绪状态,也可以由其他进程通过信号或定时中断唤醒,并进入就绪队列。这种中断阻塞状态类似于一般进程的阻塞状态。
(3)不可中断阻塞状态(TASK_UNINTERRUPTIBLE)。处于这种阻塞状态的进程,只能由资源请求得到满足时唤醒到就绪队列,不能通过信号或定时中断唤醒。
(4)僵死状态(TASK_ZOMBIE)。处于这种状态的进程已经结束运行,离开 CPU,并归还所占用的资源,只是进程控制块 PCB 结构还没有归还释放。
(5)暂停状态(TASK_STOPPED)。处于这种状态的进程被暂停执行而阻塞,通过其他进程的信号才能唤醒。导致暂停的原因有两点:一是收到暂停信号 SIGSTOP,SIGSTP,SIGTTIN和 SIGTTOU;二是受其他进程的跟踪系统调用的控制而暂时把 CPU 交出给控制进程,处于暂停状态。
(6)交换状态(TASK_SWAPPING)。处于这种状态时,进程的页面可以从内存换出。
这六种状态不是固定不变的,它们随着条件的变化而转换,转换情况如图 2 所示:
图2 Linux进程状态及其转化
用户进程执行 do_fork()函数时创建一个新的子进程,该子进程插入就绪队列,处于可运行态。创建一个子进程时,进程状态为不可中断阻塞状态,在创建子进程的工作结束前把父进程唤醒为就绪状态,即可运行状态。处于可运行状态的进程插入就绪队列中,在适当的时候被调度选中,可以获得 CPU。占有 CPU 的进程,当分给它的时间片(10ms 的整数倍)用完时,由时钟中断触发重新调度,使该进程又回到就绪状态,并挂到就绪队列队尾。已经占有 CPU 并正在运行的进程,若申请资源不能满足,则睡眠阻塞。若调用 sleep_on(),睡眠状态变为不可中断阻塞状态。若调用 interrupt_sleep_on(),睡眠状态变为可中断阻塞状态。一旦进程变为阻塞状态,其释放的 CPU 会马上被调度程序重新调度一个就绪进程去占用,而阻塞的进程插入相应的等待队列。处于可中断阻塞状态的进程可由资源满足要求或信号量或者定时中断将其唤醒为运行状态。而处于不可中断阻塞状态的进程只能由所请求的资源得到满足而唤醒,不能由信号量或定时中断唤醒,唤醒后插入就绪队列。当进程执行系统调用 sys_exit()或收到 SIG_KILL 信号(取消进程)而调用 do_exit()结束时,进程变为僵死状态。此时,归还它所占有的资源,同时启动进程调度系统程序schedule()重新调度,让其他就绪者占有CPU。如果进程通过系统调用设置跟踪标志,则在系统调用返回前,进入系统调用跟踪(syscall_trace()),进程状态就变为暂停状态。CPU 经重新调度给其他就绪进程,仅当其他进程发出暂停进程信号(SIG_KILL)或 SIG_CONT 时,才能把暂停状态唤醒,重新插入就绪队列。
2.3.4 Linux中的进程调度
1.调度的时机
调度的时机是指何时进行重新调度,即重新分配 CPU 资源的问题。Linux 调度时机主要有以下几种。
(1)当正在 CPU 执行的进程结束,或因某种原因阻塞睡眠时,要重新调度。具体说就是正在运行的进行执行 exit()函数或 sleep()函数时,这些函数主动启动进程调度函数,重新分配 CPU。
(2)当就绪队列中增加一个新进程时,要重新调度。也就是说,正在执行的进程每当调用函数 add-runqueue()时,要重新分配 CPU 资源。 在此过程中,比较新加入的进程和当前正在执行进程的 counter 值,如果符合一定条件(比如新进程的 counter 减去当前进程的 counter大于 3),将调度标志need-resched 置为 1。当内核校验到调度标志为 1 时便执行调度程序,重新调度。
(3)当正在执行的进程分到的时间片用完时,要重新调度。此种情况下,调度执行的启动是由时钟中断引发的。
(4)当进程从执行系统调用返回到用户态时,要重新调度。在系统调用返回时,一般要调用返回函数 ret-from-call(),由此函数检测调度状态,若是 1,则启动调度程序。
(5)当内核结束中断处理返回用户态时,要重新调度。此种情况也是通过执行返回函数检测调度标志的。有时,对于那些经常响应和及时处理的中断,为了节省开销,并不调用返回函数,这时返回的是被中断的进程。
(6)直接执行调度程序。
2.进程调度的算法
Linux 进程调度采用的是时间片轮转法,但同时又要保证高优先级的进程及时运行且运
行的时间较长。具体做法介绍下:
在进程控制块 fast-struct 结构中有四个成员:policy,priority,counter 和 rt-priority。其中 policy是用来区分进程类型是实时进程还是普通进程,实时进程优先于普通进程运行。对于普通进程,Linux 采用的是动态优先数法进行调度,选择进程的依据是进程的 counter 值的大小。在进程创建时给优先级 priority 赋一个初值(如 0~70 之间),这个值同时也是counter 的初值,即进程创建时counter 的值就是 priority 的初值。实际上 priority 代表的是分给这个进程的时间片值,而 counter 代表的是该进程剩余的时间片,在进程的执行过程中,counter 的值不断减少,而 priority 的值是不变的。当 counter 的值减少为 0 时(最低时)表示进程用完了所分配的时间片,这时就要放弃 CPU。这时要再一次重新赋值,只有这样普通进程才能有重新被调度的可能。某进程的counter 值为 0 时,会完全放弃对 CPU 的使用,其他进程运行的机会就会增加,所以称之为动态优先数法。Linux 中,一个“时钟滴答”为 10ms,意味着每秒钟有 100次时钟中断。时间片的大小就是指“时钟滴答”的个数。当 priority 为 20 时,表示分配给该进程的时间片为 10ms×20=200ms。这个值的大小可由用户自己决定,内核在新创建进程时分给进程的时间片值默认为 200ms,用户可通过系统调用改变这个值。
对于实时进程,Linux 采用的是先进先出和时间片轮转算法。平时,只要符合调度时机的条件就重新调度,当符合调度条件时一个进程也不能超过 200ms 的运行时间,一旦超过便重新调度。
在实时进程调度时,counter 只是用来表示该进程的剩余时间片,不作为它是否能执行的衡量标准,这一点和普通进程中 counter 的含义是不一样的。
3.就绪(可运行)队列
Linux 中所有的就绪进程排列成一个双向循环链表,叫做 runing queue 队列,它是调度程序直接操作的对象。指针 next-run 和 prev-run 放在 task_struct 结构中,分别为链表的前后指针,链表的头和尾都是 0 号进程,也叫空进程,即 idle-task,它是队列的一个标志,另一个队列的标志是队列的长度(也就是队列中可执行进程的个数)。有两个特殊的进程,一个是空进程,一个是当前进程,它总在可执行队列中。这里所说的当前进程是指由 current 指针所指的进程(也就是某进程被调度到的时候,current 才指向当前进程)。空进程是只有系统中无就绪进程时才投入运行队列的进程,从 idle-task→next 开始,到 idle-task结束。新增加的进程可插入到idle-task→next 处。队列长度用/kernel/fork.c 中定义的全局型变量 nr-running 表示,格式为:int nr-running=1;,当 nr-running 为 0 时,表示队列中只有空进程。
4.进程的可运行度量函数
进程调度时,要选出最值得调度的那个进程去执行,用什么衡量哪一个就绪进程最值得
调度呢?Linux 用一个可运行度量函数来衡量哪一个就绪进程最值得调度,这个函数是goodness()。它的工作过程如下。
(1)根据进程的调度策略policy 区分进程是实时进程还是普通进程。
(2)如果是实时进程,就用时间片轮转法或先进先出法去调度。如果是普通进程,就只考虑 counter。如果是实时进程被调度的权值为实时优先级(rt-priority)+数值 1000,而普通进程的权值只是 counter 的值,所以它的权值总小于实时进程的权值。也正是返回的这个权值(weight)就是衡量哪一个进程最值得调度的依据。
5.调度程序所作的工作(schedule)
调度程序所作的工作可分为以下几个部分,它们是:
(1)检查是否有中断正在进行(intr-count=1?)。若有,则不允许调度程序执行。
(2)处理内核例程。
(3)把当前进程放在轮转的队尾(如果是轮转法)。若该进程是可中断的,并收到信号,
就将调度标志置 1;若当前的 counter 值为 0,调度标志也是为 1;若当前进程正在运行,则继续保持;若既不可中断也不运行,不作为调度选择对象。
(4)用 goodness()函数选择一个最值得调度的进程,准备把 CPU 让给它。
(5)进程切换。若最佳的调度进程不是当前进程(交换新旧程序状态字),使 current 指
向选中的进程并恢复选中的进程的现场,保留当前进程的现场。
3. Linux与Minix中进程实现的异同
(1)进程控制块内容:
相同点:
其中二者都包含了进程管理、文件管理、以及一些标志位的信息;
不同点:
Linux中所包含的信息明显是比较复杂的,支持虚拟存储器,而且有虚拟文件系统,Minix作为一款教学用的操作系统并不具备这些特性。
(2)进程间的通信:
相同点:
二者均有消息和信号来实现进程间的通信。
不同点:
Linux除了上述二者,还有管道和有名管道、共享内存、套接字来实现进程间的通信
(3)调度:
相同点:
Linux和Minix也不例外。Linux和Minix都不是只简单了使用了一种调度算法,而且其调度思想里都包含了时间片轮转和优先级调度。Linux和Minix的进程中都有一个时间片,以表示进程能够运行的最大时间间隔,以保证当进程出现问题的时候不会一直占据CPU。同时,Linux和Minix都设定了优先级的概念,以表达进程的重要程度,并且进程的优先级是可以变化的。
Linux和Minix都是抢占式调度,即更高优先级的进程可以抢占当前进程的CPU使用权。由于Linux和Minix在实现优先级的细节上并不相同,所以这种抢占的具体形式还是有差异的。Linux和Minix都对进程进行了分类,这种分类并不是简单的优先级划分,但是和优先级有非常紧密的联系。Linux把进程分为实时进程和普通进程。Minix对进程的划分更细,有系统进程、时钟进程、驱动程序进程、服务器进程、用户进程和IDLE进程。不同进程在优先级上已经有了先天的不同,比如Linux的实时进程比普通进程的优先级要高,Minix的系统和时钟进程比驱动程序进程的优先级要高,而驱动程序进程和服务器进程比用户进程的优先级要高。
不同点:
Linux在逻辑上只有一个就绪队列,而Minix默认情况下有16个就绪队列。Linux进程的优先级划分不依赖于队列(因为它本来就只有一个就绪队列),而Minix进程的优先级则基本完全依赖于它所处的队列。
Linux和Minix对于优先级的调整方式也有所不同。Linux进程的优先级一般是固定的,虽然也可以调整,但更多的是通过权重中的其他影响因素来中和优先级所带来的巨大影响,比如进程已经运行的时间。Minix进程的优先级也比较固定,但是一般会在一个固定的范围内进行调整,也就是在几个相邻的队列中移动,其调整相对于Linux来说要更频繁一些,调整的机制也更多。
Linux的不同种类的进程使用不同的调度策略,比如实时进程使用FIFO和RR,而普通进程则使用分时调度策略,即集合优先级的时间片轮转。Minix虽然进程的种类更多,但是不同种类的进程的区别主要体现在优先级上(还有一部分体现在时间片上),所有进程共享一套调度策略。
Minix特别照顾了I/O密集型的进程,对于因为I/O阻塞而转入非就绪态的进程,当该进程再次转为就绪态时(I/O操作完成),直接插入到对应优先级队列的队首,并且拥有上次退出CPU时剩下的时间片。也就是说,这类进程在Minix中能够更快地被调度。
4.总结
本文通过分别详细介绍Linux与Minix下的进程控制块以及实现机制、进程间的通信、以及进程间的调度策略,得出了二者的异同点。Linux是一款在商业上主流的操作系统,而Minix是一款教学用的操作系统,一个采用的独立内核,一个采用是微内核,在进程的实现上有着诸多的不同,但是二者都是多任务,多用户的操作系统。本文算是简单介绍了二者在进程实现方面的异同点。
参考文献
[1] 范磊编著,《Linux 内核源代码》,人民邮电出版社,2002.1
[2] [美] Andrew S. Tanenbaum,Albert S. Woodhull 著,向勇等译,《操作系统设计与实现》,第三版,电子工业出版社2011.6.
[3] http://www.ibm.com/developerworks/cn/linux/l-linux-kernel/