Linux操作系统——进程管理

一、进程

1.1程序的顺序执行与并发执行

程序的顺序执行:

程序的各操作步骤之间依序执行,程序与程序之间串行执行,称为顺序执行。顺序执行时单道程序系统中的程序的运行方式。

特点:

(1)顺序性:一个操作结束,下一个操作才能开始执行。多个程序要运行时,仅当一个程序全部执行结束,另一个程序才能开始。

(2)封闭性:程序在封闭环境中运行,即程序运行时独占所有资源,程序的执行过程不受外界影响,结果只取决于程序自身。

(3)可再现性:程序执行的结果与运行的时间和速度无关,结果都是可再现的,重复执行该程序结果相同。

总的来说,顺序执行的方式简单,便于调试,但系统资源利用率很低。

程序的并发执行:

若干个程序或程序段同时运行。它们在执行时间上是重叠的,即同一程序或不同程序的程序段可以交叉执行。

特点:

(1)间断性:并发程序之间因竞争资源而相互制约,导致程序运行过程的间断。例如,在只有一个CPU的系统中,多个程序需要轮流占用CPU运行,未获得CPU使用权限的程序必须等待。

(2)失去封闭性:当多个程序共享系统资源时,一个程序的运行收到其他程序的影响,其运行过程和结果不完全由自身决定。例如,一个程序计划在某一时间段执行一个操作,但很可能在那个时刻到来时它没有获得CPU的使用权限,因而也五大完成该操作。

(3)不可再现性:由于没有了封闭性,并发程序执行结果与执行的时机及执行的速度有关,结果往往不可再现。

并发执行程序虽然可以提高系统资源的利用率和吞吐量,但程序的行为变得复杂和不确定,使程序难以调试,若处理不当还会带来许多潜在问题。

1.2进程的概念:

进程(process)是一个可并发执行的程序在某数据集上的一次运行。

程序是进程的一个组成部分,是进程的执行文本,而进程是程序的执行过程。

1.3进程的特性:

(1)动态性:进程由“创建”而产生,由“撤销”而消亡,因“调度”而运行,因“等待”而停顿。进程由创建到消失的过程称为进程的生命周期。

(2)并发性:在同一时间段内有多个进程在系统内活动。在宏观上是并发运行,而在微观上是在交替运行。

(3)独立性:进程是可独立运行的基本单位,是操作系统分配资源和调度资源管理的基本对象。因此,每个进程都是独立地拥有各种必要的资源,独立地占有CPU并独立的运行。

(4)异步性:每个进程都独立地执行,各自按照不可预知的速度向前推进。进程之间的协调运行由操作系统负责。

1.4进程的基本状态

在多道系统中,进程的个数总是多于CPU的个数,因此它们需要轮流占用CPU。宏观上看,所有进程同时都在向前推进,而在微观上,这些进程是在走走停停之间完成整个运行的过程。

进程有3个基本的状态:

(1)就绪态:进程已经分配到了除CPU之外的所有资源,这时的进程状态称为就绪态。处于就绪态的进程一旦获得了CPU便可立即执行,系统中常会有多个进程处于就绪态,他们拍成一个就绪队列。

(2)运行态:进程已经获得CPU,正在运行,这时的进程状态称为运行态。在CPU系统中,任何时刻只能有一个进程处于运行态。

(3)等待态:进程因某种资源不能满足,或希望的某事件尚未发生而暂停执行时,称为等待态。系统中常常会有多个进程处于等待态,它们按等待事件分类,排成多个等待队列。

1.5进程状态的转换

进程诞生之初是处于就绪状态,在其后的生存期间内不断地从一个状态转换到另一个状态,最后在运行状态结束。

 运行态→等待态:正在执行的进程因为等待某事件而无法执行下去,比如,进程申请某种资源,而该资源恰好被其他进程占用,则该进程将交出CPU,进入等待状态。

等待态→就绪态:处于等待状态的进程,当所申请的资源得到满足,则系统将资源分配给它,并将其状态变为就绪态。

运行态→就绪态:正在执行的进程的时间片用完了,或者有更高优先级的进程到来,系统会暂停该进程的运行,使其进入就绪态,然后调度其他进程运行。

就绪态→运行态:处于就绪状态的进程,当被进程调度程序选中后,即进入CPU运行。此时该进程的状态变为运行态。

二、进程控制块

进程由程序、数据和进程控制块三部分组成,其中程序是进程执行的可执行代码,数据是进程所处理的对象,进程控制块记录进程的所有信息。它们存在于内存,其内容会随着执行过程的进展不断变化,在某个时刻的进程内容被称为进程映像。

2.1进程控制块

Process Control Block,PCB 是系统管理进程设置的一个数据结构,用于记录进程的相关信息。PCB是系统感知和控制进程的一个数据实体。当创建一个进程时,系统为他生成PCB;进程完成后,撤销它的PCB。PCB是进程的代表,PCB存在进程存在,PCB消失进程结束。进程的生存周期中,系统通过PCB来了解进程的活动情况,对进程实施控制和调度,因此PCB是操作系统中的最重要数据结构之一。

2.2进程控制块的内容

PCB记录了有关进程的系统所关心的所有信息,主要包括以下4方面:

(1)进程描述信息

进程描述信息用于记录一个进程的特征和基本情况,通过这些信息可以识别该进程,了解该进程的归属信息,确定这个进程和其他进程之间的关系。

系统为每个进程分配了一个唯一的整数作为进程标识号PID,通过这个PID来标识这个进程。操作系统、用户及其他进程都是通过PID来识别进程的。此外,还要描述进的家族关系,即父进程和子进程的信息。

(2)进程控制和调度信息

进程是系统运行调度的基本单位,进程控制块记录进程的当前状态、调度信息、计时信息等。系统根据这些信息确定进程的状态,实施进程调度和控制。

(3)资源信息

系统以进程为单位分配资源,并将资源信息记录在进程的PCB文件中。资源包括改进程使用的存储空间,打开的文件及设备等。通过这些信息,进程可以的到运行需要的相关程序段、数据段、使用文件、设备等资源。

(4)现场信息

现场信息一般包括CPU的内部寄存器和系统堆栈等,它们的值刻画了进程的运行状态。退出CPU的进程必须保存好这些现场状态,以便在下一次被调度时继续运行。当一个进程被重新调度运行时,要用PCB中的现场信息来恢复CPU的运行现场。现场一旦切换,下一个指令周期CPU将精准地接着上一次运行的断点处继续执行下去。

三、进程的组织

管理进程就是管理进程的PCB。一个系统中通常有数百上千个进程,为了有效管理,系统需要采用适当的方式将他们组织在一起。所有的PCB都存放在内存中,通常采用的组织机构有数组、索引和链表3种方式。

数组方式是将所有PCB顺序存放在一个一维数组中,这种方式比较简单,但操作起来效率低。

索引方式是通过在PCB数组上设置索引表或散列表,以加快访问速度。

链表方式是将PCB链接起来,构成链式队列或链表。例如,所有就绪的PCB链成一个就绪队列;所有等待的PCB按等待事件链成多个等待队列。这样,在进程调度时只要扫描就绪队列即可,等待的事件发生时只要扫描相应的等待队列即可。当进程状态发生转换时,链式结构允许方便的向队列插入和删除一个PCB。
 

四、Linux系统中的进程

在Linux系统中进程也被称为任务(task),两者的概念是一致的。

4.1Linux进程的状态

Linux的进程共有5种基本状态,包括:运行、就绪、睡眠(分为可中断与不可中断)、暂停和僵死。

Linux将这些基本状态归结为4种并加以命名和定义:

(1) 可执行态:包括运行和就绪两种状态。处于可执行态的进程均已具备运行条件。

(2)睡眠态:即等待态。进程在等待某个事件或资源。可细分为可中断的(interruptible)和不可中断的(uninterruptible)。

不可中断:睡眠过程中进程会忽略信号

可中断:如果收到信号会被唤醒而进入可执行状态,待处理完信号后再次进入睡眠状态。

(3)暂停态:处于暂停态的进程一般是由运行态转换而来的,等待某种特殊处理。比如,调试跟踪的程序每执行到一个断点,就转入暂停态,等待新的输入信号。

(4)僵死态:进程运行结束或因某些原因被终止时,它释放除PCB以外的所有资源,这种占有着PCB但已经无法运行的进程就处于僵死态。

4.2 Linux进程的状态转换过程

Linux进程的状态转换过程是:新创建的进程处于可执行的就绪态,等待调度执行。

处于可执行态的进程在就绪态和运行态之间轮回。就绪态的进程一旦被调度程序选中,就进入运行态。等待时间片耗尽之后,退出CPU,转入就绪态等待下一次的调度。处于此轮回的进程在运行与就绪之间不断告诉切换。

运行态、睡眠态和就绪态形成一个回路。处于运行态的进程,有时需要等待某个事件或某种资源的发生,这是已经无法占有CPU继续工作,于是退出CPU,转入睡眠态。当等待的事件发生后,进程被唤醒,进入就绪态。

运行态、暂停态和就绪态也构成一个回路。当处于运行态的进程接受到暂停执行信号时,它放弃CPU,进入暂停态,当暂停的进程获得恢复执行信号时,就转入就绪态。

处于运行态的进程调用退出函数exit后,进入僵死态。父进程对该进程进行处理后,撤销其PCB。此时,这个进程就完成了它的使命,从僵死态走向消失。

4.3 Linux的进程控制块

Linux系统的PCB用一个称为task_struct的结构体来描述。系统中每创建一个新的进程,就给他分配一个task_struct的结构体,并填入进程的控制信息,task_struct主要包括以下内容:

(1)进程标识号(PID):PID是标识该进程的一个整数,系统通过这个标识号来唯一表示一个进程。

(2)用户标识(UID)和组标识(GID):描述进程的归属关系,即进程的属主和属组的标识号。系统通过这两个标识号判断进程对文件和设备的访问权限。

(3)链表信息:用指针的方式记录进程的父进程、兄弟进程、子进程的位置(即PCB的地址)。系统通过链接信息确定进程的家族关系以及其在整个进程链中的位置。

(4)状态:进程当前的状态。

(5)调度信息:与系统调度相关的信息,包括优先级、时间片、调度策略。

(6)记时信息:包括时间和定时器。时间记录进程建立的时间以及进程占用CPU的时间统计,时进程调度、统计和监控的依据。定时器用于设定一个时间。时间到时,系统会发定时信号通知进程。

(7)通信信息:记录有关进程间信号量通信及信号通信的信息。

(8)退出码:记录进程运行结束后的退出状态,供父进程查询用。

(9)文件系统信息:包括目录,当前目录,打开文件以及文件创建掩码等信息。

(10)内存信息:记录进程的代码映像和堆栈地址,长度等信息。

(11)进程现场信息:保存进程放弃CPU时所有CPU寄存器及堆栈的当前值。

4.4 查看进程的信息

Linux系统中,查看进程的信息可以使用ps(process status),可查看记录在进程PCB中的几乎所有进程信息。

ps命令

【功能】查看进程的信息

【格式】ps[选项]

【选项】-e 显示所有进程

-f 已全格式显示

-r 只显示正在运行的进程

-o 以用户定义的格式显示

a 显示所有终端上的所有进程

u 以面向用户的格式显示

x 显示所有不控制终端的进程

【说明】

(1)默认时只显示在文本终端上运行的进程,除非指定了-e、a、x等选项

没有指定显示格式时,采用以下省略格式分PID

PIDTTYTIMECMD
进程标识号

进程对应的终端;

?表示进程不占用终端

进程累计使用的CPU时间进程执行的命令

(2)指定-f选项时,以全格式,分8列显示

UIDPIDPPIDCSTIMTTYTIMECMD
进程属主的用户名进程标识号父进程标识号进程最近使用的CPU时间进程开始时间

进程对应的终端;

?表示进程不占用终端

进程累计使用的CPU时间进程执行的命令

(3)指定u选项时,以用户格式,分11列显示:

USERPID%CPU%MEMVSZRSSTTYSTATSTARTTIMECOMMAND
同UID进程标识号进程占用CPU的时间与进程总运行时间之比进程占用的内存与总内存之比进程虚拟内存的大小,以KB为单位占用实际内存的大小,单位KB..当前进程状态同STIM..同CMD

STAT:用字母表示

R——执行态;

S——睡眠态;

D——不可中断睡眠态;

T——暂停态;

Z——僵尸态

4.5 Linux进程的组织

Linux系统采用了多种方式来组织PCB,主要用一下几种:

(1)进程链表

系统将所有的PCB链成一个双向循环列表,进程通过PCB中的list_head字段链入进程链表。遍历列表即可顺序的找到每个进程。

(2)进程数链表

Linux系统中,进程之间存在着父子关系。除init进程外,每个进程都有一个父进程,即创建了此进程的进程。一个进程可以创建多个进程,称为他的子进程。具有相同的父进程的进程称为兄弟进程。系统中所有的进程形成了一棵进程树,每个进程都是树种的一个节点,树的根是init进程。

在PCB中设置有父进程指针parent、子进程指针childr和兄弟进程指针sibling,它们构成了进程树的结构。进程可以通过这些指针直接找到他们的家族成员。

(3)可执行队列

为了方便进程的调度,系统把所有处于可执行状态的PCB组成一个可执行队列,处于可执行状态的进程通过PCB中的run_list字段链入队列。可执行队列中设置了一个curr指针,它指向正在使用CPU的进程,用来区别就绪态和运行态的进程,在切换进程时,进程调度程序从可执行队列中选择一个让其运行,并将curr指针指向它。

(4)等待队列

进程因不同的原因睡眠,系统将睡眠的进程分类管理,每类对应一个特定的事件,用一个等待队列链接。等待队列是一个双向循环链表,链表的节点中包含了指向进程PCB的指针。当某一事件发生时,内核会唤醒相应的队列中满足等待条件的队列,将唤醒的进程从队列中删除,加入到可执行队列。

五、进程的运行模式

5.1 操作系统的内核

完整的操作系统由一个内核和一些系统服务程序构成。内核(kernel)是操作系统的核心,负责基本的资源管理和控制工作,为进程提供良好的运行环境。

 Linix系统的层次体系结构,分为三层:

最底层:系统硬件

核心层:运行程序和管理基本硬件得到核心程序

用户层:系统的核外程序和用户程序组成,它们都是以用户进程的方式运行在核心之上。

        内核在系统引导时载入并常驻内存,形成对硬件的第一层包装。启动了内核的系统具备了执行进程的所有条件,使进程可以被正确地创建、运行、控制、和撤销。因此,内核应具备支撑进程运行的所有功能,包括对进程本身的控制及进程要使用的资源的管理。

        Linux系统的内核主要由以下成分构成:

(1)进程控制子系统:负责支持、管理和控制进程的运行,包含以下模块:

        a.进程调度模块:负责调度进程的运行

        b.进程通信模块:实现进程间的本地通信

        c.内存管理模块:管理进程的地址空间

(2)文件子系统:为进程提供I/O环境,包括以下模块和成分:

        a.文件系统模块:管理文件和设备

        b.网络接口模块:实现进程间的网络通信

        c.设备驱动程序:驱动和控制设备的运行

        1.系统调试接口:提供进程与内核的接口,进程通过此接口调用内核的功能

        2.硬件控制接口:是内核与硬件的接口,负责控制硬件并响应和处理中断事件

5.2 中断与系统调用

由上图可以看出,内核与外界的接口是来自用户层的系统调用和来自硬件层的中断,而系统调用本身也是一种特殊的中断,因此可以说内核是中断的驱动,它的主要作用就是提供系统调用和中断的处理。 

5.2.1 中断

        早期计算机系统中,CPU与各种设备时串行工作。当需要设备传输数据时,CPU向设备发出指令,启动设备执行数据传输操作,CPU不断测试设备的状态,知道它完成操作。在这期间,CPU处于原地踏步的循环中,对CPU资源是极大的浪费。

        中断的技术出现改变了计算机系统的操作模式。现在,CPU与各种设备时并发工作的。在中断方式下,CPU启动设备操作后,它不是空闲等待,而是继续执行程序。当设备完成I/O操作后,向CPU发出特定的中断信号,打断CPU的运行。CPU响应中断后暂停正在执行的程序,转去执行专门的中断处理程序,然后再返回原来的程序继续执行。

        中断的概念是因实现CPU与设备并行操作而引入的。然而,这个概念后面被打达地扩大了。现在,系统中所有的异步发生的事情都是通过中断机制来处理的,包括I/O设备中断、系统时钟中断、硬件故障中断、软件异常中断等。这些中断分为硬件中断和软件中断(异常)两个大类,每个中断都对应一个中断处理程序,中断发生后,CPU通过处理程序来处理中断事件。

5.2.2系统调用

         系统调用是系统内核提供的一组特殊函数,用户进程通过系统调用来访问系统资源。与普通函数不同之处在于,普通函数是由用户或函数可提供的程序代码,它们的运行会受到系统的限制,不能访问系统资源,系统调用是内核中的程序代码,他们具有访问系统资源的权限。当用户进程需要执行涉及系统资源的操作时,需要通过系统调用,由内核来完成。

        系统调用是借助中断机制来实现的,他是软中断的一种,称为“系统调用”中断。当进程执行到一个系统调用时,就会产生一个系统调用中断。CPU将响应此中断,转入到系统调用入口程序,然后调用内核中相应的系统调用处理函数,执行该系统调用对应的功能。

5.3 进程的运行模式

5.3.1 CPU的执行模式

        CPU的基本功能就是执行指令。通常,CPU指令集中的指令可以划分为两类:特权指令和非特权指令。

特权指令:指具有特殊权限的指令,可以访问系统中所有的寄存器和内存单元,修改系统的关键设置。比如清理内存、设置时钟、执行I/O操作等都是由特权指令完成的。

非特权指令:指一般性的运算和处理的指令,这些指令只能访问用户程序直接的内存地址空间

5.3.2 进程的运行模式

        进程在运行期间常常被中断或系统调用打断,因此CPU也进程在用户态与核心态之间切换。在进行通常的计算和处理时,进程运行在用户态;执行系统调用或中断处理程序时进入核心态,执行内核代码。调用返回后,回到用户态继续运行

        A期间,进程运行在用户态,执行用户程序代码。运行到某一时刻时发生了中断,进程随即“陷入”核心态运行。在B期间,CPU运行在核心态,执行的时内核程序代码。此时有两种情况:

1.如果程序时被中断打断的,则B期间执行的时中断处理程序,它时随机插入的,与进程本身无关;

2.如果进程时因调用了系统调用而陷入内核空间的,则B执行的是内核的系统调用程序代码,他是作为进程的一个执行环节,由内核代理用户进程继续执行的,在中断或系统调用返回后的C期间中,进程在用户继续运行。

六. 进程控制

        进程控制是指对进程的生命周期进行有效的管理,实现进程的创建、撤销以及进程各状态之间的转换等控制功能。进程控制的目标是使多个进程能平稳地并发执行,充分共享系统资源。

6.1 进程控制的功能

        进程控制的功能使控制进程在整个生命周期中各种状态之间的转换(不包括就绪态与运行态之间的转换,它们是由进程调度来实现的)。为此,内核提供了几个原子性的操作函数,称为原语。他与普通函数的区别使它的各个指令的执行是不可分割的,要么全部完成,要么一个也不做,因此可以看作是一条广义的指令。用于进程控制的原语主要有创建、终止、阻塞和唤醒等。

(1)创建进程

        创建原语的主要任务是根据创建者提供的有关参数(包括进程名,进程优先级,进程代码起始地址,资源清单等信息),建立进程的PCB。具体的操作是:先申请一个空闲的PCB结构,调用资源的分配程序为它分配所需的资源,将有关信息填入PCB,状态设置为就绪态,然后把他插入就绪队列中。

(2)撤销进程

        撤销原语用于在一个进程运行终止时,撤销这个进程并释放进程占用的资源。撤销的操作过程是:找到被撤销的进程的PCB,将它从所在队列中摘出,释放进程所占用的资源,最后消去进程的PCB。

(3)阻塞进程

        阻塞原语用于完成从运行态到等待态的转换工作。当正在运行的进程需要等待某一事件而无法执行下去时,它就调用阻塞原语把自己转入等待态,插入到相应的等待队列中;最后调用进程调度程序,从就绪(可执行)队列中选择一个进程投入运行。

(4)唤醒进程

        唤醒原语用于完成等待态到就绪态的转换工作。当处于等待态的进程所等待的事件出现时,内核会调用唤醒原语唤醒被阻塞的进程。操作过程是:在等待队列中找到该进程,置进程的当前状态为就绪态,然后将他从等待队列中撤出并插入到就绪队列中。

6.2 Linux系统的进程控制

        在Linux系统中,进程控制的功能是由内核的进程控制子系统实现的,并以系统调用的形式提供给用户进程或其他系统进程使用。

6.2.1 进程的创建与映像更换

        系统启动时执行初始化程序,启动进程号为1的init进程运行。系统总所有的其他进程都是由init进程衍生而来的。除init进程外,每个进程都是由另一个进程创建的。新建的进程称为子进程,创建子进程的叫父进程。

        Unix/Linux系统创建进程的方式与众不同。它不是一步构造出新的进程,而是采用先复制再变身两个步骤,即先按照父进程创建一个子进程,然后再更换进程映像开始执行

(1)创建进程

        创建一个进程的系统调用是fork()

        创建进程采用的方法是克隆,即父进程复制一个子进程。做法是:先获得一个空闲的PCB,为子进程分配一个PID,然后将父进程的PCB中的代码即资源复制给子进程的PCB,状态置为可执行状态。建好PCB后将其链接入进程链表和可执行队列中。此后,子进程和父进程并发执行。父子进程执行的是同一个代码,使用的是同样的资源,它与父进程的区别仅仅在于PID(进程号)、PPID(父进程号)和子进程运行相关的属性(如状态,累计运行时间等),而这些是不能从父进程那里继承来的。

fork()系统调用

【功能】创建一个新的子进程

【调用格式】 int fork();

【返回值】0 向子进程返回的返回值,总为0

>0 向父进程返回的返回值,它是子进程的PID

-1 创建失败

【说明】若fork()调用成功,则它向父进程返回子进程的PID,并向新建的子进程返回0。

        从图中可以看出,当一个进程成功执行了fork()后,从调用点之后分裂成了两个进程:一个是父进程,从fork()后的代码从继续运行;另一个是新建的子进程,从fork()后的代码处开始运行。

        与一般函数不同,fork()是“一次调用,两次返回”,因为在调用成功后,已经是两个进程了。由于子进程是从父进程那里复制的代码,因此父子进程执行的是同一个程序,它们在执行时的区别只在于得到的返回值不同。父进程得到的返回值时子进程的PID;子进程得到的返回值是0。

        若不考虑fork()的返回值,则父子进程的行为就完全一样了,但创建一个子进程的目的是想让它做另一件事。所以,通常的做法是:在fork()调用后,通过判读fork()的返回值,分别为父进程和子进程设计不同的执行分支。这样父子进程虽是同一个代码,执行路线却分道杨彪。

例: 一个简单的fork_test程序:

#include <stdio.h>

int main()
{
        int rid;
        rid = fork();
        if(rid < 0){
                printf("fork error!");
                return;
        }
        if(rid > 0){
                printf("I am parent,my rid is %d,my PID is %d\n",rid,getpid());
        }else{
                printf("I am child,my rid is %d,my PID is %d\n",rid,getpid());
        }

        return 0;
}

注:程序中的getpid()是一个系统调用,他返回本教程的教程表示号PID。

        fork_test程序运行时,父子进程将会输出不同的信息

 由于两进程时并发的,它们的输出信息的先后次序不确定,有可能父先子后,也可能相反。

(2)更换进程映像

        进程映像是指进程所执行的程序代码及数据。fork()是将父进程的执行映像拷贝给子进程,因而子进程实际上是父进程的克隆体。单用户通常需要的是创建一个新进程,它执行的是一个不同的程序。Linux系统的做法是,先用fork()克隆一个子进程,然后再子进程中调用exec(),使其脱胎换骨,变为一个全新的进程。

        exec()系统调用的功能是根据参数指定的文件名找到程序文件,把他装入内存,覆盖原来进程的映像,从而形成一个不同于父进程的全选的子进程。除了进程映像被更换外,子进程的PID及其他PCB属性保存不变,实际上是一个新的进程“借壳”原来的子进程开始运行。

        exec()系统调用

【功能】改变进程的映像,使其执行另外的程序

【调用格式】exec()是一系列系统调用,共有6种调用格式,其中 execve() 才是真正的系统调用,其余是对其包装后的C库函数。

        int execve(char *path,char *argc[],char *envp[]);

        int execl(char *path,char *arg0,char *arg1,...,char *argn,0);

        int execle(char *path,char *arg0,char *arg1,...,char *argn,0,char *exvp[]);

【参数说明】path为要执行的文件的路径名,argv[]为运行参数数组,envp[]为运行环境数组。arg0为程序的名称,arg1~argn为程序的运行参数,0表示参数结束。

例如:

        execl("/bin/echo","echo","hello!",0);

        execle("/bin/ls","ls","-l","/bin",0,NULL);

        前者表示更换进程映像为/bin/echo文件,执行的命令行是“echo hello!”

        后者表示更换进程映像为/bin/ls文件,执行的命令行是“ls -l /bin”

【返回值】调用成功后,不返回,调用失败后,返回-1。

        与一般函数不同,exec()是“一次调用,零次返回”,因为调用成功后,进程的映像已经被替换,无处可以返回。下图描述了用exec()系统调用更换进程映像的流程。子进程开始运行后,立刻调用exec(),变身成功后即开始执行性的程序了。

例:一个简单的fork-exec_test程序:

#include <stdio.h>
#include <unistd.h>

int main()
{
        int rid;
        rid = fork();
        if(rid > 0){
                printf("I am parent\n");
        }else{
                printf("I am child,l'll change to echo!\n");
                execl("/bin/echo","echo","hello!",(char *)0);
        }
        return 0;
}

        fork返回后,父子进程分别执行各自的分支,父进程输出信息“I am parent”.子进程输出信息“I am child,l'll change to echo!”,然后调用exec(),变换为echo程序。echo随即开始执行并输出字符串“hello”。

6.2.2 进程的终止与等待

(1)进程的终止与退出状态

        导致一个进程终止运行的方式有两种:一是程序中使用退出语句主动终止运行,我们称其为正常终止;另一种是被某个信号杀死(例如:在程序运行时按Ctrl+C终止其运行)称为非正常终止。

用C语言编程时,我们可以通过以下4种方式主动退出:

        (1)调用exit(status)函数来结束程序;

        (2)在main()函数种调用return status语句结束;

        (3)在main()函数中调用return语句结束;

        (4)main()函数结束。

        以上4种情况都会使进程正常终止,前3种为显示地终止程序的运行,后一种为隐式地终止。正常终止的进程可以返回给系统一个退出状态,即前2种语句中的status。通常的约定是:0表示正常状态;非0表示异常状态,不同取值表示异常的具体原因。例如对一个计算程序,可以约定退出状态为0表示计算成功,为1表示运算数出错,为2表示运算符出错等。如果程序结束时没有指定的退出状态(如后两者),则他的退出状态时不确定的。

        设置退出状态的作用时通知父进程有关此次运行的状况,以便父进程做出相应的处理。因此,显示地结束程序并返回退出状态时一个好的Linux/Unix编程习惯,这样的程序可以将自己的运行状况告知系统,因而能更好地与系统和其他程序合作。

(2)终止进程

        进程无论以那种方式结束,都会调用一个exit()系统调用,通过这个系统调用终止自己的运行,并及时通知父进程回收本进程。exit()系统调用完成以下操作:释放进程除PCB外的几乎所有资源;向PCB写入进程退出状态和一些统计信息;置进程态为“僵死态”;向父进程发送"子进程终止"信号;调用进程调度程序切换CPU的运行进程。

        至此,子进程已变为"僵尸进程",它不再具备任何执行条件,只是PCB还在。保留PCB的目的时为了保存有关该进程运行的重要信息,比如这个进程的退出状态、运行时间的统计、收到信号的数目等。子进程的最后回收工作由父进程负责。父进程收集子进程的信息后将其PCB撤销。如果某一个进程由于某种原因先于子进程终止,有它创建的子进程就会变成"孤儿进程"。当系统中出现孤儿进程时,init进程将会发现并收养它,成为它的父进程。由于init进程不会退出,所以所有的进程都会被收养,最后,在系统关机之前,init进程要负责结束所有的进程。

exit()系统调用

        【功能】使进程主动终止

        【调用格式】void exit (int status);

        【参数说明】status是要传给父进程的一个整数,用于父进程通报进程运行的结构状态。status的含义通常是:0表示正常终止;非0表示运行有错,异常终止。

(3)等待与收集过程

        在并发执行的环境中,父子进程的运行速度是无法确定的。在许多情况下,我们希望父子进程的进展能有某种同步关系。比如,父进程需要等待子进程的运行结果才能继续执行下一步计算,或父进程要负责子进程的回收工作,他必须在子进程结束后才能退出。这时就需要通过wait()系统调用来阻塞父进程,等待子进程结束。

        当父进程调用wait()时,自己立即被阻塞,有wait()检查是否有僵死子进程,如果找到就收集它的信息,然后撤掉它的PCB;否则就阻塞下去,等带子进程发来的终止信号。父进程被信号唤醒后,执行wait(),处理子进程的回收工作,经wait()收集后,子进程才真正的消失。

wait()系统调用

        【功能】阻塞进程直到子进程结束;收集子进程。

        【调用格式】int wait(int *statloc)

        【参数说明】*statloc保存了子进程的一些状态。如果是正常退出,则字节莫为0,第2字节为退出状态;如果是非正常退出(即被某个信号终止),则其末字节不为0,末字节的低7位为导致进程终止的信号的信号值,若不关心子进程是如何终止的,可以用NULL作参数,即wait(NULL)。

        【返回值】>0 子进程的PID;

        -1 调用失败;

        0 其他;

        下图描述了wait()系统调用等待子进程的流程

例:一个简单的wait-exit_test程序:

#include <stdio.h>
#include <stdlib.h>

int main()
{
        int rid,cid,status;
        rid = fork();
        if(rid < 0){
                printf("fork error!\n");
                exit(-1);
        }if(rid == 0){
                printf("I am child.I will sleep a while.\n");
                sleep(10);
                exit(0);
        }

        cid = wait(&status);
        printf("I catched a child with PID of %d\n",cid);

        if((status & 0377) == 0){
                printf("It exited normoally with status of %d\n",status>>8);
        }else{
                printf("It is terminated by signal %d\n",status&0177);
                exit(0);
        }

        return 0;
}

         执行过程为:父进程在创建子进程失败时会用exit(-1)退出。成功创建子进程后,父进程会调用wait()阻塞自己;子进程运行,先输出信息,睡眠10秒后调用exit(0)退出父进程发信号,高中自己结束。父进程被唤醒后,从wait()返回,根据获得的子进程的PID和退出状态判断子进程的运行情况并输出相应的信息,然后用exit(0)退出。

6.3 Shell命令的执行过程

        Shell程序的功能就是执行Shell命令,执行命令的主要方式就是创建一个子进程,让这个子进程来执行命令的映像文件。因此,Shell进程是所有在其下执行的命令的父进程。下图所示是Shell执行命令的大致过程,从中可以看到一个进程从诞生到消失的整个过程。

         Shell进程初始化完成后,在屏幕上显示命令提示符,等待命令行输入。接收到一个命令行后,Shell对其进行解析,确定要执行的命令及其选项和参数,以及命令的执行方式,然后创建一个子Shell进程。

        子进程诞生后立即更换进程映像为要执行的命令的映像文件,运行该命令直至结束。如果命令行后面没有带有后台运行符“&”,则子进程在前台开始运行。此时,Shell阻塞自己,等待命名执行结束。如果命令行后带有“&”,则子进程在后台开始运行,同时Shell也继续执行下去。它立即显示命令提示符,接受下一个命令。命令子进程执行结束后,向父进程Shell进程发送信号,由Shell对子进程进行回收处理。

7.进程调度

7.1进程调度的基本原理

7.1.1进程调度的功能

        进程调度的功能是按照一定的策略把CPU分配给就绪进程,使他们轮流地使用CPU。进程调度实现看教程的就绪态与运行态之间的装换。调度工作包括:

        (1)当正运行的教程因某种原因放弃CPU时,为该进程保留现场信息。

        (2)按一定的调度算法,从就绪进程中选一个进程,把CPU分配给它。

        (3)为被选中的进程恢复现场,使其运行。

7.1.2进程调度算法

        进程调度算法是系统效率的关键,它确定了系统对资源,特别是对CPU资源的分配策略,因而直接决定着系统最本质的性能指标,如响应速度、吞吐量等。进程调度算法的目标首先是要充分发挥CPU的处理能力,满足进程对CPU的需求。此外还要尽量做到公平对待每个进程,是他们都能得到运行机会。

        常用的调度算法:

        (1)先进先出法:按照进程在就绪队列中的先后次序来调度。这是最简单的调度法,缺点是对一些紧迫任务的响应时间过长

        (2)短进程优先发:有先调用短进程运行,以提高系统的吞吐量,但对长进程不利。

        (3)时间片轮转法:进程按规定的时间片轮流使用CPU。这种方法可满足分时系统对用户响应时间的要求,有很好的公平性。时间片长度的选择应适当。过短会引起频繁的进程调度,过长则对用户的响应较慢。

        (4)优先调度法:为每个进程设置优先级,调度时先选择优先级高的进程运行,使紧迫的任务可以优先得到处理。静态优先级是指预先指定的,动态优先级则随进程运行时间的长短而降低或升高。两种优先级组合调度,即可保证对高优先级进程的响应,也不会忽略低优先级的基础。

7.2 Linux系统的进程调度

7.2.1进程的调度信息

        在Linux系统中,进程的PCB中记录了与基础调度相关的信息,主要有:

        (1)调度策略(policy):对进程的调度算法。决定了调度程序应如何调度该进程。Linux

系统将进程分为实时进程与普通(非实时)进程。实时进程是那些对响应时间要求很高的进程,如视频与音频应用、过程控制和数据采集等,系统优先响应它们对CPU的要求;普通进程则采用优先级+时间片轮转的调度策略,以兼顾系统的响应速度,公平性和整体效率。

        (2)实时优先级(rt_priority):实时进程的优先级,标志实时进程优先权的高低,取值范围为1(最高)~99(最低)

        (3)静态优先级(static_prio):进程的基本优先级。进程在创建指之初被赋予了一个表示优先度的“nice数”,它决定了进程的静态优先级。静态优先级的取值返回为100(最高)~139(最低),它是计算时间片的依据。

        (4)动态优先级(prio):普通进程的实际优先级。它是对静态优先级的调整,随进程的运行状况而变化,取值范围是100(最高)~139(最低)

        (5)时间片(time_slice):进程当前剩余的时间片。实际片的初始大小却决于进程的静态优先级,优先级越高则时间减为0的基础将不会被调度,直达它再次获得新的时间片。

        基础的调度策略和优先级等是在进程创建时从父进程那里继承来的,不过用户可以通过系统调用它们。

        setpriority()和nice()用于设置静态优先级;

        sched_setparam()用于设置实时优先级;

        sched_setscheduler()用于设置调度策略和参数。

7.2.2调度函数和队列

        Linux系统中用于实现进程调度的程序时内核函数schedule()。该函数的功能时按照预定的策略在可执行进程中选择一个进程,切换CPU现场使之运行。调度程序中最基本的数据结构是可执行队列runqueue。每个CPU都有一个自己的可执行队列,它包含了所有等待该CPU的可执行进程。runqueue结构中设有一个curr指针,指向正在使用CPU的进程。进程切换时,curr指针也跟着变化。

        旧版本的调度程序(2.4版内核)在选择进程时需要遍历整个可执行队列,用的时间随进程数量的增加而增加,最坏时可能达到O(n)复杂度级别。新内核(2.6版内核)改进了调度的算法和数据结构,使算法的复杂度达到O(1)(最优级别),故称为O(1)算法。
  新内核的
runqueue队列结构中实际包含了多个进程队列,它们将进程按优先级划分,相同优先级的链接在一起,成为一个优先级队列。所有优先级队列的头地址都记录在一个优先级数组中,按优先级顺序排列。实时进程的优先级队列在前(199),普通进程的优先级队列在后(100139)。当进程调度选择进程时,只需在优先级数组中选择当前最高优先级队列中的第1个进程即可。无论进程的多少,这个操作总可以在固定的时间内完成,因而是O(1)级别的。

        影响调度算法效率的另一个操作是为进程重新计算时间片。旧算法中,当所有进程的时间片用完后,调度程序遍历可执行队列,逐个为它们重新赋予时间片,然后开始下一轮的执行。当进程数目很多时,这个过程会十分耗时。为克服这个弊端,新调度函数将每个优先级队列分为两个:活动队列和过期队列。活动队列包含了那些时间片未用完的进程,过期队列包含了那些时间片用完的进程。相应地,在runqueue中设置了两个优先级数组,一个是活动数组active,它记录了所有活动队列的指针;另一个是过期数组expired,它记录了所有过期队列的指针。当一个进程进入可执行态时,它被按照优先级放入一个活动队列中;当进程的时间片耗完时,它会被赋予新的时间片并转移到相应的过期队列中。当所有活动队列都为空时,只需将activeexpired数组的指针互换,过期队列就成为活动队列。这个操作也是O(1)级别的。
  可以看出,新调度的实现策略是用复杂的数据结构来换取算法的高效率的。

7.2.3 Linux的进程调度策略

        进程调度在选择进程时,首先在可执行队列中寻找优先级最高的进程。由于实时进程的优先级(199)总是高于普通进程(100139),所以实时进程永远优先于普通进程。选中进程后,根据PCBpolicy的值确定该进程的调度策略来进行调度。在schedule()函数中实现了3种调度策略,即先进先出法,时间片轮转法和普通调度法。

1)先进先出法

        先进先出(FIFOFirst In First Out)调度算法用于实时进程,采用FIFO策略的实时进程就绪后,按照优先级rt_priority加入到相应的活动队列的队尾。调度程序按优先级依次调度各个进程运行,具有相同优先级的进程采用FIFO算法。投入运行的进程将一直运行,直到进入僵死态、睡眠态或者是被具有更高实时优先级的进程夺去CPU
  
FIFO算法实现简单,但在一些特殊情况下有欠公平。比如,一个运行时间很短的进程排在了一个运行时间很长的进程之后,它可能要花费比运行时间长很多倍的时间来等待。

2)时间片轮转法

        时间片轮转(RRRound Robin)算法也是用于实时进程,它的基本思想是给每个实时进程分配一个时间片,然后按照它们的优先级rt_priority加入到相应的活动队列中。调度程序按优先级依次调度,具有相同优先级的进程采用轮换法,每次运行一个时间片。时间片的长短取决于其静态优先级static_prio。当一个进程的时间片用完,它就要让出CPU,重新计算时间片后加入到同一活动队列的队尾,等待下一次运行。RR算法也采用了优先级策略。在进程的运行过程中,如果有更高优先级的实时进程就绪,则调度程序就会中止当前进程而去响应高优先级的进程。
  相比
FIFO来说,RR算法在追求响应速度的同时还兼顾到公平性。

3)普通调度法

        普通调度法(NORMALNormal Scheduling)用于普通进程的调度。每个进程拥有一个静态优先级和一个动态优先级。动态优先级是基于静态优先级调整得到的实际优先级,它与进程的平均睡眠时间有关,进程睡眠的时间越长则其动态优先级越高。调整优先级的目的是为了提高对交互式进程的响应性。

        NORMAL算法与RR算法类似,都是采用优先级+时间片轮转的调度方法。进程按其优先级prio被链入相应的活动队列中。调度程序按优先级顺序依次调度各个队列中的进程,每次运行一个时间片。一个进程的时间片用完后,内核重新计算它的动态优先级和时间片,然后将它加入到相应的过期队列中。与RR算法的不同之处在于,普通进程的时间片用完后被转入过期队列中,它要等到所有活动队列中的进程都运行完后才会获得下一轮执行机会。而RR算法的进程始终在活动队列中,直到其执行完毕。这保证了实时进程不会被比它的优先级低的进程打断。可以看出,RR算法注重优先级顺序,只在每级内采用轮转;而NORMAL算法注重的是轮转,在每轮中采用优先级顺序。

7.2.4 进程调度的时机

        当需要切换进程时,进程调度程序就会被调用。引发进程调度的时机有下面几种:
  
(1) 当前进程将转入睡眠态或僵死态。
  
(2) 一个更高优先级的进程加入到可执行队列中。
  
(3) 当前进程的时间片用完。
  
(4) 进程从核心态返回到用户态。

        从本质上看,这些情况可以归结为两类时机,一是进程本身自动放弃CPU而引发的调度,这是上述第1种情况。这时的进程是主动退出CPU,转入睡眠或僵死态。二是进程由核心态转入用户态时发生调度,包括上述后3种情况。这类调度发生最为频繁。当进程执行系统调用或中断处理后返回,都是由核心态转入用户态。时间片用完是由系统的时钟中断引起的中断处理过程,而新进程加入可执行队列也是由内核模块处理的,因此也都会在处理完后从内核态返回到用户态。

        Linux系统是抢占式多任务系统,上述情况除了第1种是进程主动调用调度程序放弃CPU的,其他情况下都是由系统强制进行重新调度的,这就是CPU抢占(preemption)。在必要时抢占CPU可以保证系统具有很好的响应性。为了标志何时需要重新进行进程调度,系统在进程的PCB中设置了一个need_resched标志位,为1时表示需要重新调度。当某个进程的时间片耗尽,或有高优先级进程加入到可执行队列中,或进程从系统调用或中断处理中返回前,都会设置这个标志。每当系统从核心态返回用户态时,内核都会检查need_resched标志,如果已被设置,内核将调用调度函数进行重新调度。

8.进程的互斥与同步

        多个进程在同一系统中并发执行,共享系统资源,因此它们不是孤立存在的,而是会互相影响或互相合作。为保证进程不因竞争资源而导致错误的执行结果,需要通过某种手段实现相互制约。这种手段就是进程的互斥与同步。

8.1 进程的互斥与同步

        并发进程彼此间会产生相互制约的关系。进程之间的制约关系有两种方式:一是进程的同步,即相关进程为协作完成同一任务而引起的直接制约关系;二是进程的互斥,即进程间因竞争系统资源而引起的间接制约关系。

8.1.1 临界资源与临界区

        临界资源(critical resource)是一次仅允许一个进程使用的资源。例如,共享的打印机就是一种临界资源。当一个进程在打印时,其他进程必须等待,否则会使各进程的输出混在一起。共享内存、缓冲区、共享的数据结构或文件等都属于临界资源。
  临界区
(critical region)是程序中访问临界资源的程序片段。划分临界区的目的是为了明确进程的互斥点。当进程运行在临界区之外时,不会引发竞争条件。而当进程运行在临界区内时,它正在访问临界资源,此时应阻止其他进程进入同一资源的临界区。

8.1.2 进程的互斥与同步

        进程的互斥(mutex)就是禁止多个进程同时进入各自的访问同一临界资源的临界区,以保证对临界资源的排它性使用。

        因共享临界资源而发生错误,其原因在于多个进程访问该资源的操作穿插进行。要避免这种错误,关键是要用某种方式来阻止多个进程同时访问临界资源,这就是互斥。以停车场车位计数器为例,当进程A运行在它的(A)临界区内时,进程B不能进入它的(B)临界区执行,进程B必须等待,直到A离开A的临界区后,B才可进入B的临界区运行。

        进程的同步(synchronization)是指进程间为合作完成一个任务而互相等待、协调运行步调。

        例如:两个进程合作处理一批数据,进程A先对一部分数据进行某种预处理,然后通过缓冲区传给进程B做进一步的处理。这个过程要循环多次直至全部数据处理完毕。
  访问缓冲区是一个典型的进程同步问题。缓冲区是两进程共享的临界资源,当一个进程存取缓冲区时,另一个进程是不能同时访问的。但两进程之间并不仅仅是简单的互斥关系,它们还要以正确的顺序来访问缓冲区,即必须
A进程写缓冲区在前,B进程读缓冲区在后,且读与写操作必须一一交替,不能出现连续多次地读或写操作。比如,当A进程写满缓冲区后,即使B进程因某种原因还没有占用缓冲区,A也不能去占用缓冲区再次写数据,它必须等待B将缓冲区读空后才能再次写入。
  可以看出,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。广义地讲,互斥与同步实际上都是一种同步机制。

8.2 信号量与P、V操作

        实现进程互斥与同步的手段有多种,其中,信号量是最早出现的进程同步机制。因其简洁有效,信号量被广泛地用来解决各种互斥与同步问题。

8.2.1 信号量与P、V操作

        信号量(semaphore)是一个整型变量s,它为某个临界资源而设置,表示该资源的可用数目。s大于0时表示有资源可用,s的值就是资源的可用数;s小于或等于0时表示资源已都被占用,s的绝对值就是正在等待此资源的进程数。

        信号量是一种特殊的变量,它仅能被两个标准的原语操作来访问和修改。这两个原语操作分别称为P操作和V操作。
  
P(s)操作定义为:s=s-1; if (s<0) block(s);
  V(s)操作定义为:s=s+1; if (s<=0) wakeup(s);
  PV操作是原语,也就是说其执行过程是原子的,不可分割的。PV操作中用到两个进程控制操作,其中,block(s)操作将进程变换为等待状态,放入等待s资源的队列中。wakeup(s)操作将s的等待队列中的进程唤醒,将其放入就绪队列。这两种操作后都会调用schedule()函数,引发一次进程调度。

        P(s)操作用于申请资源sP(s)操作使资源的可用数减1。如果此时s是负数,表示资源不可用(即已被别的进程占用),则该进程等待。如果此时s0或正数,表示资源可用,则该进程进入临界区运行,使用该资源。
  
V(s)操作用于释放资源sV(s)操作使资源的可用数加1。如果此时s是负数或0,表示有进程在等待此资源,则用信号唤醒等待的进程。如果此时s是正数,表示没有进程在等待此资源,则无须进行唤醒操作。

        使用信号量与PV操作可以正确实现进程间的各种互斥与同步。信号量的作用类似于人行道上的红绿灯:行人过街时先按下按钮(执行P操作),车行道上的红灯亮起,来往车辆见到信号即停止;行人过街后,按另一个按钮(执行V操作),使绿灯亮起,车辆放行。

8.2.2 用P、V操作实现进程互斥

        设进程A和进程B都要访问临界资源C,为实现互斥访问,需要为临界资源C设置一个信号量s,初值为1。当进程运行到临界区开始处时,先要做P(s)操作,申请资源s。当进程运行到临界区结束处时,要做V(s)操作,释放资源s。进程A和进程B执行过程如图所示。

        由于s的初值是1,当一个进程执行P(s)进入临界区后,s的值变为0。此时若另一个进程执行到P(s)操作时就会被挂起(s的值变为-1),从而阻止了其进入临界区执行。当一个进程退出其临界区时执行V(s)操作,若此时s=1表示没有进程在等待此资源,若此时s=0表示有一个进程在等待此资源,系统将唤醒该进程,使之可以进入临界区运行。这样就保证了两个进程总是互斥地访问临界资源。

8.2.3 用P、V操作实现进程同步 

        设两进程为协作完成某一项工作,需要共享一个缓冲区。先是一个进程C往缓冲区中写数据,然后另一个进程D从缓冲区中读取数据,如此循环直至处理完毕。缓冲区属于临界资源,为使这两个进程能够协调步调,串行地访问缓冲区,需用PV操作来同步两进程。这种工作模式称为“生产者-消费者模式”。同步的方法介绍如下。
  设置两个信号量:
  “缓冲区满”信号量
s1s1=1时表示缓冲区已满,s1=0时表示缓冲区未满。初值为0
  “缓冲区空”信号量
s2s2=1时表示缓冲区已空,s2=0时表示缓冲区未空。初值为1
  进程
C和进程D执行过程如图所示。

        由于s1的初值是0s2的初值是1,最初进程C执行P(s2)可以进入临界区,向缓冲区写入,而进程D在执行P(s1)时就会被挂起,因此保证了先写后读的顺序。此后,两者的同步过程是:当C写满缓冲区后,执行V(s1)操作,使D得以进入它的临界区进行读缓冲区操作。在D读缓冲区时,C无法写下一批数据,因为再次执行P(s2)时将阻止它进入临界区。当D读空缓冲区后,执行V(s2)操作,使C得以进入它的临界区进行写缓冲区操作。在C写缓冲区时,D无法读下一批数据,因为再次执行P(s1)时将阻止它进入临界区。这样就保证了两个进程总是互相等待,串行访问缓冲区。访问的顺序只能是“写、读、写、读、……”,而不会出现“读、写、读、写、……”或“读、读、写、……”、“写、写、读、……”之类的错误顺序。 

8.3 Linux的信号量机制

        Linux系统中存在两种信号量的实现机制,一种是针对系统的临界资源设置的,由内核使用的信号量;另一种是供用户进程使用的。
内核管理着整个系统的资源,其中许多系统资源都属于临界资源,包括核心的数据结构、文件、设备、缓冲区等。为防止对这些资源的竞争导致错误,在内核中为它们分别设立了信号量。内核将信号量定义为一种结构类型
semaphore,其中包含了3个数据域:该资源的可用数count、等待该资源的进程数sleepers以及该资源的等待队列的地址wait。内核同时还提供了操作这种类型的信号量的两个函数down()up(),分别对应于P操作和V操作。当内核访问系统资源时,通过这两个函数进行互斥与同步。

        用户进程在使用系统资源时是通过调用内核函数来实现的,这些内核函数的运行由内核信号量进行同步。因而,用户程序不必考虑有关针对系统资源的互斥与同步问题。但如果是用户自己定义的某种临界资源,如前面例子中的停车场计数器,则不能使用内核的信号量机制。这是因为内核的信号量机制只是在内核内部使用,并未向用户提供系统调用接口。
  为了解决用户进程级上的互斥与同步问题,
Linux以进程通信的方式提供了一种信号量机制,它具有内核信号量所具有的一切特性。用于实现进程间信号量通信的系统调用有:semget(),用于创建信号量;semop(),用于操作信号量,如PV操作等;semctl(),用于控制信号量,如初始化等。用户进程可以通过这几个系统调用对自定义临界资源的访问进行互斥与同步。

8.4 死锁问题

        死锁(deadlock)是指系统中若干个进程相互“无知地”等待对方所占有的资源而无限地处于等待状态的一种僵持局面,其现像是若干个进程均停顿不前,且无法自行恢复。
  死锁是并发进程因相互制约不当而造成的最严重的后果,是并发系统的潜在的隐患。一旦发生死锁,通常采取的措施是强制地撤销一个或几个进程,释放它们占用的资源。这些进程将前功尽弃,因而死锁是对系统资源极大的浪费。
  死锁的根本原因是系统资源有限,而多个并发进程因竞争资源而相互制约。相互制约的进程需要彼此等待,在极端情况下,就可能出现死锁。如图
所示是可能引发死锁的一种运行情况。

         AB两进程在运行过程中都要使用到两个临界资源,假设资源1为独占设备磁带机,资源2为独占设备打印机。若两个进程执行时在时间点上是错开的,则不会发生任何问题。但如果不巧在时序上出现这样一种情形:进程A在执行完P(s1)操作后进入资源1的临界区运行,但还未执行到P(s2)操作时发生了进程切换,进程B开始运行。进程B执行完P(s2)操作后进入资源2的临界区运行,在运行到P(s1)操作时将被挂起,转入等待态等待资源1。当再度调度到进程A运行时,它运行到P(s2)操作时也被挂起,等待资源2。此时两个进程彼此需要对方的资源,却不放弃各自占有的资源,因而无限地被封锁,陷入死锁状态。

        分析死锁的原因,可以归纳出产生死锁的4个必要条件,即:
  
(1) 资源的独占使用:资源由占有者独占,不允许其他进程同时使用。
  
(2) 资源的非抢占式分配:资源一旦分配就不能被剥夺,直到占用者使用完毕释放。
  
(3) 对资源的保持和请求:进程因请求资源而被阻塞时,对已经占有资源保持不放。
  
(4) 对资源的循环等待:每个进程已占用一些资源,而又等待别的进程释放资源。

        上例中,磁带机和打印机都是独占资源,不可同时共享,具备了条件1;资源由进程保持,直到它用V操作主动释放资源,具备了条件2;进程A在请求资源2被阻塞时,对资源1还未释放,进程B也是如此,具备了条件3;两个进程在已占据一个资源时,又在相互等待对方的资源,这形成了条件4。所有这些因素凑到一起就导致了死锁的发生。
  解决死锁的方案就是破坏死锁产生的必要条件之一,方法有:
  
(1) 预防:对资源的用法进行适当的限制。
  
(2) 检测:在系统运行中随时检测死锁的条件,并设法避开。
  
(3) 恢复:死锁发生时,设法以最小的代价退出死锁状态。

        预防是指采取某种策略,改变资源的分配和控制方式,使死锁的条件无法产生。但这种做法会导致系统的资源也无法得到充分的利用。检测是指对资源使用情况进行监视,遇到有可能引发死锁的情况就采取措施避开。这种方法需要大量的系统开销,通常以降低系统的运行效率为代价。因此,一般系统都采取恢复的方法,就是在死锁发生后,检测死锁发生的位置和原因,用外力撤销一个或几个进程,或重新分配资源,使系统从死锁状态中恢复过来。

        每个系统都潜在地存在死锁的可能,Unix/Linux系统也不例外。但是,出于对系统效率的考虑,Unix/Linux系统对待死锁采取的是“鸵鸟算法”,即系统并不去检测和解除死锁,而是忽略它。这是因为对付死锁的成本过高,而死锁发生的概率过低(大约连续开机半年才会出现一次)。如果采用死锁预防或者检测算法会严重降低系统的效率。

9. 进程通信

        进程间为实现相互制约和合作需要彼此传递信息。然而每个进程都只在自己独立的存储空间中运行,无法直接访问其他进程的空间。因此,当进程需要交换数据时,必须采用某种特定的手段,这就是进程通信。进程通信(Inter-Process CommunicationIPC)是指进程间采用某种方式互相传递信息,少则是一个数值,多则是一大批字节数据。
  为实现互斥与合作,进程使用信号量相互制约,这实际上就是一种进程通信,即进程利用对信号量的
PV操作,间接地传递资源使用状态的信息。更广泛地讲,进程通信是指在某些有关联的进程之间进行的信息传递或数据交换。这些具有通信能力的进程不再是孤立地运行,而是协同工作,共同实现更加复杂的并发处理。

9.1 进程通信的方式

        进程间的通信有多种方式,大致可以分为信号量、信号、管道、消息和共享内存几类。
  从通信的功能来分,进程通信方式可以分为低级通信和高级通信两类。低级通信只是传递少量的数据,用于通知对方某个事件;高级通信则可以用来在进程之间传递大量的信息。低级通信的方式有信号量和信号,高级通信的方式有消息、管道和共享内存等。

        按通信的同步方式来分,进程通信又分为同步通信与异步通信两类。同步通信是指通信双方进程共同参与整个通信过程,步调协调地发送和接收数据。这就像是打电话,双方必须同时在线,同步地交谈。异步通信则不同,通信双方的联系比较松散,通信的发送方不必考虑对方的状态,发送完就继续运行;接收方也不关心发送方的状态,在自己适合的时候接收数据。异步通信方式就如同发送电子邮件,不必关心对方何时接收。管道和共享内存等都属于同步通信,而信号、消息则属于异步通信。
  现代操作系统一般都提供了多种通信机制,以满足各种应用需要。利用这些机制,用户可以方便地进行并发程序设计,实现多进程之间的相互协调和合作

        Linux系统支持以下几种IPC机制:
  
(1) 信号量(semaphore):作为一种IPC机制,信号量用于传递进程对资源的占有状态信息,从而实现进程的同步与互斥。关于信号量的介绍见4.5.3小节。
  
(2) 信号(signal):信号是进程间可互相发送的控制信息,一般只是几个字节的数据,用于通知进程有某个事件发生。信号属于低级进程通信,传递的信息量小,但它是Linux进程天生具有的一种通信能力,即每个进程都具有接收信号和处理信号的能力。系统通过一组预定义的信号来控制进程的活动,用户也可以定义自己的信号来通告进程某个约定事件的发生。

        (3) 管道(pipe):管道是连接两个进程的一个数据传输通路,一个进程向管道写数据,另一个进程从管道读数据,实现两进程之间同步传递字节流。管道的信息传输量大,速度快,内置同步机制,使用简单。关于管道的介绍见4.6.3小节。
  
(4) 消息队列(message queue):消息是结构化的数据,消息队列是由消息链接而成的链式队列。进程之间通过消息队列来传递消息,有写权限的进程可以向队列中添加消息,有读权限的进程则可以读走队列中的消息。与管道不同的是,这是一种异步的通信方式:消息的发送方把消息送入消息队列中,然后继续运行;接收进程在合适的时机去消息队列中读取自己的消息。相比信号来说,消息队列传递的信息量更大,能够传递格式化的数据。更主要的是,消息通信是异步的,适合于在异步运行的进程间交换信息。

        (5) 共享内存(shared-memory):共享内存通信方式就是在内存中开辟一段存储空间,将这个区域映射到多个进程的地址空间中,使得多个进程能够共享这个内存区域。通信双方直接读/写这个存储区即可达到数据共享的目的。由于进程访问共享内存区就如同访问进程自己的地址空间,因此访问速度最快,只要发送进程将数据写入共享内存,接收进程就可立即得到数据。共享内存的效率在所有IPC中是最高的,特别适用于传递大量的、实时的数据。但它没有内置的同步机制,需要配合使用信号量实现进程的同步。因此,较之管道,共享内存的使用较复杂。

9.2 Linux信号通信原理

        信号是Unix系统中最古老的IPC机制之一,主要用于在进程之间传递控制信号。信号属于低级通信,任何一个进程都具有信号通信的能力。

9.2.1 信号的概念

        信号是一组正整数常量,进程之间通过传送信号来通信,通知进程发生了某事件。例如,当用户按下Ctrlc键时,当前进程就会收到一个信号,通知它结束运行。子进程在结束时也会用信号通知父进程。
  
i386平台的Linux系统共定义了32个信号(还有32个扩展信号),常用的信号见表4-1。用kill -l命令可以列出系统的全部可用信号。

信号值信号名用途
1SIGHUP终端挂断信号
2SIGNT来自键盘(Ctrl+c)的终止信号
3SIGQUIT来自键盘(Ctrl+/)的终止信号,退出时产生调试文件(core文件)
8SIGFPE浮点异常信号,表示发生了致命的运输错误
9SIGKILL立即结束运行信号,杀死进程
14SIGALRM时钟定时信号
15SIGTERM结束运行信号,命令进程主动终止
17SIGCHLD子进程结束信号
18SIGCONT恢复运行信号,使暂停的进程继续运行
19SIGSTOP暂停执行信号,通常用来调试程序
20SIGSTP来自键盘(Ctrl+z)的暂停信号

9.2.2 信号的产生与发送

      信号可以由某个进程发出,也可以由键盘中断产生,还可以由kill命令发出。进程在某些系统错误情况下也会有信号产生。
  信号可以发给一个或多个进程。进程
PCB中含有几个用于信号通信的域,用于记录进程收到的信号以及各信号的处理方法。发送信号就是把一个信号送到目标进程的PCB的信号域上。如果目标进程正在睡眠(可中断睡眠态),内核将唤醒它。
  终端用户用
kill命令或键盘组合按键向进程发送信号,程序则是直接使用kill()系统调用向进程发送信号。  

kill命令

        【功能】向一个进程发信号,常用于终止进程的运行。
  
格式】kill [选项] 进程号
  
选项
  -s  向进程发s信号。S可以是信号值或信号名。常用的终止进程运行的信号为:
   15  SIGTERM,缺省
   2  SIGINT
   9  SIGKILL
  说明如果未指定-s选项,则缺省地向进程发SIGTERM信号。

        注意:此例中的9805号进程是本终端的shell进程,它忽略SIGTERM信号,用SIGKILL信号才可杀死,但这将导致终端窗口被关闭。因此,使用kill -9命令杀系统进程时应慎重。
  

kill()系统调用

     【功能向进程发送信号。
  
调用格式】int kill(int pid, int sig)
  参数pid是目标进程的进程号,sig为要发送的信号。
  
返回值发送成功返回0,否则返回-1

9.2.3 信号的检测与处理

        当一个进程要进入或退出睡眠状态时,或即将从核心态返回用户态时,都要检查是否有信号到达。若有信号到达,则转去执行与该信号相对应的处理程序。
  进程可以选择忽略或阻塞这些信号中的绝大部分,但有两个信号除外,这就是引起进程暂停执行的
SIGSTOP信号和引起进程终止的SIGKILL信号。至于其他信号,进程可以选择处理它们的具体方式。对信号的处理方式分为以下4种:

        (1) 忽略:收到的信号是一个可忽略的信号,不做任何处理;
  
(2) 阻塞:阻塞对信号的处理;
  
(3) 默认处理:调用内核的默认处理函数;
  
(4) 自行处理:执行进程自己的信号处理程序。

9.3 Linux管道通信原理

        管道是Linux系统中一种常用的IPC机制。管道可以看成是连接两个进程的一条通信信道。利用管道,一个进程的输出可以成为另一个进程的输入,因此可以在进程间快速传递大量字节流数据。
  管道通信具有以下特点:
  
(1) 管道是单向的,数据只能向一个方向流动。需要双向通信时,需要建立起两个管道;
  
(2) 管道的容量是有限的(一个内存页面大小)
  
(3) 管道所传送的是无格式字节流,使用管道的双方必须事先约定好数据的格式。

        管道是通过文件系统来实现的。Linux将管道看做是一种特殊类型的文件,而实际上它是一个以虚拟文件的形式实现的高速缓冲区。管道文件建立后由两个进程共享,其中一个进程写管道,另一个进程读管道,从而实现信息的单向传递。读/写管道的进程之间的同步由系统负责。
  终端用户在命令行中使用管道符“
|”时,Shell会为管道符前后的两个命令的进程建立起一个管道。前面的进程写管道,后面的进程读管道。用户程序中可以使用pipe()系统调用来建立管道,而读/写管道的操作与读/写文件的操作完全一样。

10. 线程

        在传统的操作系统中,一直将进程作为能独立运行的基本单位。20世纪80年代中期,Microsoft公司最先提出了比进程更小的基本运行单位——线程。线程的引入提高了系统并发执行的程度,因而得到广泛的应用。现代操作系统中大都支持线程,应用软件也普遍地采用了多线程设计,使系统和应用软件的性能进一步提高。

10.1 线程的概念

         多道处理系统中,进程是系统调度和资源分配的基本单位,每一次切换进程,系统都要做保护和恢复现场的工作。因此,切换进程的过程要耗费相当多的系统资源和CPU时间。为了减少并发程序的切换时间,提高整个系统的并发效率,引入了线程的概念。

        传统的进程中,每个进程中只存在一条控制线索。进程内的各个操作步是顺序执行的。现代操作系统提供了对单个进程中多条控制线索的支持。这些控制线索被称为线程(threads)。线程是构成进程的可独立运行的单元。一个进程由一个或多个线程构成,并以线程作为调度实体,占有CPU运行。线程可以看做是进程内的一个执行流,一个进程中的所有线程共享进程所拥有的资源,分别按照不同的路径执行。例如,一个Word进程中包含了多个线程,当一个线程处理编辑时,另一个线程可能正在做文件备份,还有一个线程正在发送邮件。网络下载软件通常也含有多个线程,每个线程负责一路下载,多路下载都在独立地、并发地向前推进。这些多线程的软件虽然只是一个进程,却表现出内在的并发执行的特征,效率明显提高。

10.2 线程和进程的区别

        进程和线程都是用来描述程序的运行活动的。它们都是动态实体,有自己的状态,整个生命周期都在不同的状态之间转换。它们之间的不同表现在以下几个方面:
  进程是操作系统资源分配的基本单位,每一个进程都有自己独立的地址空间和各种系统资源,如打开的文件、设备等;线程基本上不拥有自己的资源,只拥有一点在运行中必不可少的资源
(如程序记数器、寄存器和栈)。它与同一进程中的其他线程共享该进程的资源。在创建和撤销进程时系统都要进行资源分配和回收工作,而创建和撤销线程的系统开销要小得多。

        进程调度时,系统需要保存和切换整个CPU运行环境的现场信息,这要消耗一定的存储资源和CPU时间;线程共享进程的资源,线程调度是在进程内部切换,只需保存少量的寄存器,不涉及现场切换操作,因而切换速度很快。因此,对于切换频繁的工作,多线程设计方式比多进程设计方式可以提供更高的响应速度。
  此外,由于多个线程共享同一进程的资源,因而线程之间相互通信更容易;而进程间通信一般必须要通过系统提供的进程间通信机制。

10.3 内核级线程与用户级线程

        线程有“用户级线程”与“内核级线程”之分。所谓用户级线程,是指不需要内核支持而在用户程序中实现的线程,对线程的管理和调度完全由用户程序完成。内核级线程则是由内核支持的线程,由内核完成对线程的管理和调度工作。尽管这两种方案都可实现多线程运行,但它们在性能等方面相差很大,可以说各有优缺点。
  在调度方面,用户级线程的切换速度比核心级线程要快得多。但如果有一个用户线程被阻塞,则核心将整个进程置为等待态,使该进程的其他线程也失去运行的机会。核心级线程则没有这样的问题,即当一个线程被阻塞时,其他线程仍可被调度运行。

        在实现方面,要支持内核级线程,操作系统内核需要设置描述线程的数据结构,提供独立的线程管理方案和专门的线程调度程序,这些都增加了内核的复杂性。而用户线程不需要额外的内核开销,内核的实现相对简单得多,同时还节省了系统进行线程管理的时间开销。

10.4 Linux中的线程

        Linux实现线程的机制属于用户级线程。 从内核的角度来说,并没有线程这个概念。Linux内核把线程当作进程来对待。内核没有特别定义的数据结构来表达线程,也没有特别的调度算法来调度线程。每个线程都用一个PCB(task_struct)来描述。所以,在内核看来,线程就像普通的进程一样,只不过是该进程和其他一些进程共享地址空间等资源。
  用户程序可以利用线程库函数实现多线程。用线程库中的函数可以方便地创建、调度和撤销线程,也可以实现线程间通信。线程的创建与普通进程的创建类似,只不过在调用
fork()时需要传递一些参数标志来指明需要共享的资源。创建出的子进程与父进程共享地址空间、文件系统资源、文件描述符和信号处理程序等。

  • 8
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值