1 操作系统原理中的进程模型
1.1进程的概念
进程的定义很多,本文将进程描叙为一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。进程具有动态性、独立性、并发性和结构化等特征。动态性是指进程具有动态的地址空间,地址空间的大小和内容都是动态变化的。地址空间的内容包括代码(指令执行和处理器状态的改变)、数据(变量的生成和赋值)和系统控制信息(进程控制块的生成和删除)。独立性是指各进程的地址空间相互独立,除非采用进程间通信手段,否则不能相互影响。并发性也称为异步性,是指从宏观上看,各进程是同时独立运行的。结构化是指进程地址空间的结构划分,如代码段、数据段和核心段划分。
进程和程序是两个密切相关的不同概念,它们在以下几个方面存在区别和联系:
进程是动态的,程序是静态的。程序是有序代码的集合;进程是程序的执行。进程通常不可以在计算机之间迁移;而程序通常对应着文件、静态和可以复制。
进程是暂时的,程序是永久的。进程是一个状态变化的过程;程序可长久保存。
进程与程序的组成不同:进程的组成包括程序、数据和进程控制块(即进程状态信息)。
进程与程序是密切相关的。通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序。进程可创建其他进程,而程序并不能形成新的程序。
进程是程序代码的执行过程,但并不是所有代码执行过程都从属于某个进程。
1.2线程的概念
是进程中的一个实体,是被系统莫道不消魂独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
用户级线程(User-Level Threads):这类线程不依赖于内核,只是存在于用户级中,对它的创建、撤销和切换都不是通过系统调用来实现,因此这种线程与核心无关。核心只需实现对进程的调度。在系统调用时,由于核心并不知道用户级线程的存在,所以将系统调用当成是整个进程的行为,致使该线程所隶属的进程等待,而调度另一个进程执行。
核心级线程(Kernel-Supported Threads):这种线程依赖于内核。无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤销和切换都用通过核心实现。核心级线程的调度和切换与进程的调度和切换很相似。
1.3进程的五状态模型
图1 进程的五状态模型
在图1五状态进程模型中,进程状态被分成下列五种状态。进程在运行过程中主要是在就绪、运行和阻塞三种状态间进行转换。创建状态和退出状态描述进程创建的过程和进程退出的过程。
运行状态(Running):进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
就绪状态(Ready):进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。
阻塞状态(Blocked):当进程由于等待I/O操作或进程同步等条件而暂停运行时,它处于阻塞状态。
创建状态(New):进程正在创建过程中,还不能运行。操作系统在创建状态要进行的工作包括分配和建立进程控制块表项、建立资源表格(如打开文件表)并分配资源、加载程序并建立地址空间表等。
退出状态(Exit):进程已结束运行,回收除进程控制块之外的其他资源,并让其他进程从进程控制块中收集有关信息(如记帐和将退出代码传递给父进程)。
2 Minix的进程模型
2.1进程的创建
在Minix中,进程创建采用两阶段的模式,父进程调用fork创建子进程,创建后子进程跟父进程拥有同样的内存映像,打开的文件,以及环境变量等,但拥有各自的地址空间,尽管的地址空间是父进程的地址空间的一个拷贝。因此父、子进程中任何一个改变自己的地址空间对另一个是不可见的。通常在fork子进程后,子进程会调用execve或者是类似的系统调用加载一段新的程序。
创建进程的时机:
系统初始化
运行中的进程调用创建进程系统调用
用户请求创建进程
批量作业的执行
2.2进程的调度和运行
内核调度的单位是进程,Minix3调度器采用优先级和改进的轮转算法调度进程,使用一个多级排队系统。一共定义了16个队列,但是重新编译以定义更多或更少的队列也是可以的。进程被调度后,开始运行。
2.3进程的终止
刚创建的新进程,被调度程序调度后,便开始运行。完成任务或者遇到错误或者被其他进程杀死后进程退出。在Minix中进程正常退到调用exit系统调用,杀死一个进程调用kill系统调用。
2.4进程的层次结构
在Minix系统中,被编译进内核的任务例如CLOCK和SYSTEM任务,只在内核中可见,不构成任何一棵进程树。Process manager进程是第一个在用户空间运行的进程,其PID为0但不是任何一个进程的父进程或子进程。系统中的其他进程包括系统进程和用户进程,构成一棵以reincarnation server为根节点的树,但树结构中各节点进程之间的启动顺序是不确定的。
系统启动时,boot image 里的reincarnation server和其他进程启动, reincarnation serve 是所有从boot image中创建的进程的父进程。init进程是第一个用户进程,是所有用户进程的父节点,其PID为1。Init执行/etc/rc脚本,发射一些命令给reincarnation serve去创驱动和服务,然后为每个终端启动一个getty进程,getty进程execs一个登录进程,密码正确后登录进程加载用户shell进程。Shell进程fork用户进程执行用户命令。
2.5进程的状态模型
进程的三种状态:
(1)运行态(在该时刻实际占用处理机)
(2) 就绪态(可运行,因为其他进程正在运行而暂时地被挂起)
(3)阻塞态(除非某种外部事件,否则不能运行)
前两种状态在逻辑上很类似,这两种状态下的进程都希望运行,只是在后者中,暂时没有CPU分配给它,第三种状态与前两种状态不同,该状态的进程不能运行,即使CPU空闲也不行。
3 Linux2.6的进程模型
3.1 Linux下的进程和线程
Linux是一个多任务,多用户的操作系统,支持多线程技术,但其实现的线程机制非常独特,从内核的角度来说,它并没有线程这个概念。Linux把所有线程都当作进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程和进程都有属于自己的task_struct,在内核看来两者没什么区别。
3.2进程的创建
Linux在传统Unix两阶段创建进程的方式上进行了改进,采用了写时拷贝(copy-on-write)技术。创建新进程时,不是复制父进程的整个地址空间,而是让父进程与子进程共享同一个拷贝。当其中一进程写数据时,数据才会被复制,从而使各个进程拥有各自的拷贝。
当子进程需要执行另一段程序时,可以调用exec()一族的系统调用。在一般的情况下,进程创建后都会马上运行一个可执行文件,这种优化可以避免拷贝大量根本就不会被使用的数据。
3.3进程的调度和运行
Linux内核的调度单位是进程task_struct,主要的调度算法有:
时间片轮转调度算法(round-robin):SCHED_RR,用于实时进程。系统使每个进程依次地按时间片轮流执行的方式。
优先权调度算法:SCHED_NORMAL,用于非实时进程。系统选择运行队列中优先级最高的进程运行。Linux 采用抢占式的优级算法,即系统中当前运行的进程永远是可运行进程中优先权最高的那个。
FIFO(先进先出) 调度算法:SCHED_FIFO,实时进程按调度策略分为两种。采用FIFO的实时进程必须是运行时间较短的进程,因为这种进程一旦获得CPU 就只有等到它运行完或因等待资源主动放弃CPU时其它进程才能获得运行机会
3.4进程的运行和终止
刚创建的进程被调度后,进入运行。当进程调用exit(),或者进程接收到它不能处理也不能忽略的信号或异常时,进程终结处于TASK_ZOMBIEZ状态,与进程相关联的所有资源都被释放掉,此时进程存在的惟一目的就是向它的父进程提供信息,当父进程调用wait()系统调用后,子进程的所有剩余内存被释放。
3.5进程的进程树
在Linux系统中所有进程都是PID为1的init进程的后代,它们一起构成系统的进程树。
3.6进程状态模型
Linux的进程分为五状态,状态及状态转换如下:
TASK_RUNNING(运行):进程是可执行的,它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行惟一可能的状态,可以应用到内核空间中正在执行的进程。
TASK_INTERRUPTIBLE(可中断):进程正在睡眠,等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并投入运行。
TASK_UNINTERRUPTIBLE(不可中断):除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可中断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不作响应,所以没可中断状态使用广。
TASK_ZOMBIE(僵死):该进程已经结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用wait4(),进程描述符就会释放。
TASK_STOPPED(停止):进程停止运行,进程在投入运行
图2 进程状态转化图
3.7线程在Linux中的实现
Linux内核不区分线程跟进程,线程跟进程的区别就是线程跟别的进程共享用户空间,Linux的用户线程可以通过线程库实现。Linux在核外采用1:1线程模型,即用一个核心进程对应一个用户线程,将线程调度等同于进程调度,交给核心完成,而其它诸如线程取消、线程间的同步等工作,都是在核外线程库中完成的。因此可以把进程看作一组线程,这组线程拥有相同的线程组号(TGID),这个TGID就是这组线程序所附属的进程的ID号。
4 Solaris 10的进程模型
4.1 Solaris10进程,线程,LWP
Solaris10内核的进程跟传统UNIX进程的概念类似,进程是系统资源的分配,包括执行所需的所有抽象(例如 硬件状态,打开文件等)。但Solaris10扩展了传统的UNIX对进程支持的实现,它支持在一个进程内执行多条线程,这些线程共享进程的资源,并可以独立与其他线程在处理器上调度和执行。进程在Solaris内核中的数据结构为proc_t。
用户线程:在进程内由用户创建的执行单元,其在内核中的数据结构为ulwp_t.
内核线程:内核种运行的线程,操作系统的任务都是以内核线程的形式执行,它是内核调度和执行的基本单位,在内核中的数据结构为kthread_t
轻量级进程(LWP):由内核支持的控制流程,称为轻进程。LWP可被当作一个执行代码或系统调用的虚拟CPU。它使用户线程可以独立与同一进程中的其他线程而执行和进入内核,在内核中的数据结构为klwp_t。
进程,用户线程,LWP,内核线程之间的对应关系如下图:
图3 进程,用户线程,LWP,内核线程之间的对应关系图
4.2进程的创建
Solaris10内核的进程跟传统UNIX进程的概念类似,进程是系统资源的分配,包括执行所需的所有抽象(例如 硬件状态,打开文件等)。但Solaris10扩展了传统的UNIX对进程支持的实现,它支持在一个进程内执行多条线程,这些线程共享进程的资源,并可以独立与其他线程在处理器上调度和执行。
Solaris10通过fork一族系统调用来创建进程,fork先将父进程的地址空间的内容复制到子进程的地址空间,然后在新创建的进程内创建新的LWP和内核线程,但只在新进程中复制调用的线程。如果需要复制所有线程可以调用forkall()系统调用。当新创建的进程/LWP/kthread基础结构就绪后,大部分应用程序会调用exec一族系统调用,它用新的可执行映像覆盖调用程序。
4.3 进程调度和运行
Solaris内核调度和执行的单位是内核线程,Solaris内核为内核线程实现了全局的线程优先级模型。内核调度程序使用这个模型选择哪个内核线程作为许多潜在的可运行内核线程中的下一个执行。Solaris调度程序实现了多个调度类,允许不同的调度策略被用于线程。三个基本的调度类是TS(IA是加强的TS)、SYS和RT。
4.4进程的终止
有三种可能的事件会导致进程的终止:
当进程显示调用exit()一族系统调用会使多线程进程中的所有线程终止
进程执行完成,退出main函数也会终止进程。
信号的产生和传递都会导致进程的终止。
不管哪种事件导致进程的终止最终都要执行内核退出函数来释放系统资源。进入内核函数exit()后,会立即调用eixtlwaps()函数。exitlwps()负责终止进程中的所有的LWP,只保留其中一个,当每个LWP/kthread的清理工作完成后,LWP/kthread被放进进程的僵死LWP链表。如何exit()函数进行其他的清理工作。一旦exit()执行完毕,进程进入僵死状态,只占用进程表项和PID数据结构。当父进程执行wait系统调用时,处于僵死状态的进程剩余的资源被回收。
4.5进程的进程树
与Linux和传统的Unix类似。
4.6进程的状态
Solari中的调度的基本单位是内核线程而不是进程,对于非线程的进程,进程的状态本质上是其内核线程的状态,而对于多线程进程,属于同一进程的多个内核线程可能处与不同的状态,因此进程的状态用处不大。进程的状态定义如下:
SIDL:空闲状态,在fork创建时的状态
SRUN:可运行状态
SONPROC:在处理器上运行的状态
SSLEEP:睡眠(阻塞)状态
SSTOP:停止状态
SZOMB:僵死状态
内核对进程状态进行操作的地方也很少,当检查进程时设置进程状态。在fork创建进程过程中设置进程状态为空闲状态(SIDL);当创建完成并可运行时,其状态被设置为可运行态;在进程退出时,其状态被设置为僵死状态。除此之外,进程生命周期中的所有其他状态改变发生在其内核线程上。
4.7线程模型
Solaris10的线程模型相当与之前版本发生了很大的变化。Solaris最初的设计意图是在进程内支持成百上千的线程,而不需要进入内核进行多个线程管理任务,例如创建和销毁线程。采用的线程模型是多级别的M×N模式,在这种模式下用户线程是与LWP分离的且不相同的。在线程库中实现的用户线程调度程序把M个用户线程复用到N个LWP,N可能比M小。在新的线程模型中,采用的是1:1的模型,没一个用户线程创建的同时,都对应创建一个LWP和相关的内核线程。
改进后的优点如下:
改进的性能、可扩展性和可靠性。库源代码的大小和复杂性随着单层模型的发展被充分地减小。库级的调度程序需要的内部库锁被废除。
可靠的信号行为。用户线程和LWP之间的信号屏蔽同步问题不复存在;异步的信号传递是可靠和一致的。
改进的自适应互斥信号锁实现。互斥信号(mutex)锁是用于多线程程序保护数据被多线程同时访问的同步原语。自适应互斥信号提供优化,因此希望获得被占用的锁的线程将动态决定自旋等待锁,或者睡眠并依靠唤醒机制在锁被释放的时候再试一次。使用新模型,自适应互斥信号已经被优化。
新的线程模型图如下图:
图4 1:1线程模型图
5 Minix, Linux2.6, Solaris 10进程模型对比
5.1进程的创建和终止
Minix3,linux2.6,Solaris10的进程创建和终止的处理思想都继承于传统的Unix,但因各自的进程模型不同实现差别很大。Solaris支持用户级多线程,系统中具有真正的线程数据结构。为了实现多线程,其在进程的创建和终止处理时,需要创建和清理进程里的用户线程,以及用户线程对应的LWP/内核线程。而linux内核中进程的创建和终止,只需处理进程相关的资源分配和回收,相对Solaris的进程创建和终止的开销小。Minix3作为教学目的,不支持线程,进程的创建和终止也比较简单。
5.2进程调度
Minix3,linux2.6,Solaris10内核调度的基本单位不同,Minix调度单位是进程,linux调度单位是task_struct可能是进程也可能是线程。Solaris调度单位是内核线程。实现的调度算法各异,最大的区别是Solaris同时支持多个调度器。
5.3进程的进程树
Minix3的init进程只是所有用户进程树的根节点,而在linux和Solaris系统中,init是所有系统进程树的根结点。
5.4进程的状态
Minix3,linux2.6,Solaris10都类似,但因各自的设计目标不同实现各异。其中Minix3作为教学目的,使用的就是五状态进程模型。Linux把阻塞状态分成了可中断和不可中断状态。而在Solaris中,系统调度和执行的单位是内核线程,进程的状态基本上成为了摆设,只是在检查和分配系统资源时使用。
5.5对线程的支持
Minix3不支持线程机制。linux内核不区分线程和进程,可以通过线程库实现用户级线程,Linux内核只提供了轻量进程的支持,限制了更高效的线程模型的实现,但Linux着重优化了进程的调度开销,一定程度上也弥补了这一缺陷。Solaris10作为商业级操作系统内核既支持进程也支持线程,采用1:1的模型,每一个用户线程创建的同时,都对应创建一个LWP和相关的内核线程,内核线程是系统调度和执行的单位。内核支持线程使得Solaris的线程模型更高效。
参考文献:
[1]《操作系统:设计与实现》第二版,电子工业出版社,Andrew S.Tanenbaum,
Albert S.Woodhull著,王鹏,尤晋元 朱鹏 敖青云等译
[2]《Linux内核设计与实现》第二版,机械工业出版社,(美)Robert Love著,陈莉君 康华 张波等译
[3]《Solaris10内核结构》第二版,机械工业出版社,(美)Richard McDougall, Jim Mauro著,Sun中国工程研究院译



