Linux进程的详细内容

1 进程

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

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

特点:

  • 顺序性:一个操作结束,下一个操作才能开始执行。多个程序要运行时,仅当一个程序全部执行结束,另一个程序才能开始。
  • 封闭性:程序在封闭环境中运行,即程序运行时独占所有资源,程序的执行过程不受外界影响,结果只取决于程序自身。
  • 可再现性:程序执行的结果与运行的时间和速度无关,结果都是可再现的,重复执行该程序结果相同。

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

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

特点:

  • 间断性:并发程序之间因竞争资源而相互制约,导致程序运行过程的间断。例如,在只有一个CPU的系统中,多个程序需要轮流占用CPU运行,未获得CPU使用权限的程序必须等待。
  • 失去封闭性:当多个程序共享系统资源时,一个程序的运行收到其他程序的影响,其运行过程和结果不完全由自身决定。例如,一个程序计划在某一时间段执行一个操作,但很可能在那个时刻到来时它没有获得CPU的使用权限,因而也无法完成该操作。
  • 不可再现性:由于没有了封闭性,并发程序执行结果与执行的时机及执行的速度有关,结果往往不可再现。

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

1.2 进程的概念:

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

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

1.3 进程的特性:

  • 动态性:进程由“创建”而产生,由“撤销”而消亡,因“调度”而运行,因“等待”而停顿。进程由创建到消失的过程称为进程的生命周期。
  • 并发性:在同一时间段内有多个进程在系统内活动。在宏观上是并发运行,而在微观上是在交替运行。
  • 独立性:进程是可独立运行的基本单位,是操作系统分配资源和调度资源管理的基本对象。因此,每个进程都是独立地拥有各种必要的资源,独立地占有CPU并独立的运行。
  • 异步性:每个进程都独立地执行,各自按照不可预知的速度向前推进。进程之间的协调运行由操作系统负责。

1.4 进程的基本状态

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

进程有3个基本的状态:

  • 就绪态:进程已经分配到了除CPU之外的所有资源,这时的进程状态称为就绪态。处于就绪态的进程一旦获得了CPU便可立即执行,系统中常会有多个进程处于就绪态,他们拍成一个就绪队列。
  • 运行态:进程已经获得CPU,正在运行,这时的进程状态称为运行态。在CPU系统中,任何时刻只能有一个进程处于运行态。
  • 等待态:进程因某种资源不能满足,或希望的某事件尚未发生而暂停执行时,称为等待态。系统中常常会有多个进程处于等待态,它们按等待事件分类,排成多个等待队列。

1.5 进程状态的转换

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NYl63BfW-1662905170670)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8ea15a08-d13d-46a8-b3db-279e9fb91614/Untitled.png)]

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

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

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

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

进程控制块
进程由程序、数据和进程控制块三部分组成,其中程序是进程执行的可执行代码,数据是进程所处理的对象,进程控制块记录进程的所有信息。它们存在于内存,其内容会随着执行过程的进展不断变化,在某个时刻的进程内容被称为进程映像。
2.1 进程控制块(PCB)
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.1 Linux进程的状态

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t0olYL6q-1662905170672)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/23beea9e-4e87-4cb8-b53b-3d61bc3c372e/Untitled.png)]

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

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

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

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

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

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

  • 僵死态:进程运行结束或因某些原因被终止时,它释放除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 显示所有不控制终端的进程

【说明】

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

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

PID TTY TIME CMD
进程标识号 进程对应的终端; 进程累计使用的CPU时间 进程执行的命令
?表示进程不占用终端

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

UID PID PPID C STIM TTY TIME CMD
进程属主的用户名 进程标识号 父进程标识号 进程最近使用的CPU时间 进程开始时间
进程对应的终端;

?表示进程不占用终端

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

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

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
同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运行在核心态,执行的时内核程序代码。此时有两种情况:
如果程序时被中断打断的,则B期间执行的时中断处理程序,它时随机插入的,与进程本身无关;
如果进程时因调用了系统调用而陷入内核空间的,则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系统创建进程的方式与众不同。它不是一步构造出新的进程,而是采用先复制再变身两个步骤,即先按照父进程创建一个子进程,然后再更换进程映像开始执行

6.2.1.1 创建进程

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

fork()

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

fork()系统调用

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

【调用格式】 int fork();

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

  • 0 向父进程返回的返回值,它是子进程的PID
  • 1 创建失败

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nktyFWsv-1662905170673)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/35a815ce-376b-4e40-a00c-66ebb6881411/Untitled.png)]

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YzeTssdK-1662905170673)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/50e3987a-6ccc-4492-90a9-745b203cc014/Untitled.png)]

例: 一个简单的fork_test程序:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        int rid;
        rid = fork();

        if(rid < 0){
                printf("fork error!");
                return 0;
        }

        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 1;
}

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

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

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

6.2.1.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(),变身成功后即开始执行性的程序了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WwpVChQB-1662905170674)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e1b5fbe2-3446-405f-84d3-9406162ba64c/Untitled.png)]

例:一个简单的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 进程的终止与等待

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编程习惯,这样的程序可以将自己的运行状况告知系统,因而能更好地与系统和其他程序合作。

6.2.2.2 终止进程

进程无论以那种方式结束,都会调用一个exit()系统调用,通过这个系统调用终止自己的运行,并及时通知父进程回收本进程。exit()系统调用完成以下操作:

  • 释放进程除PCB外的几乎所有资源;
  • 向PCB写入进程退出状态和一些统计信息;
  • 置进程态为“僵死态”;
  • 向父进程发送"子进程终止"信号;
  • 调用进程调度程序切换CPU的运行进程。

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

exit()系统调用
【功能】使进程主动终止

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

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

6.2.2.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()系统调用等待子进程的流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4U3gdcuQ-1662905170675)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/003a1280-5067-4324-a542-dd4bda275d83/Untitled.png)]

例:一个简单的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和退出状态判断子进程的运行情况并输出相应

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YBfRNmtI-1662905170676)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/949b80f3-b68b-4c8c-bcdc-76dc4ceb2b28/Untitled.png)]

6.3 Shell命令的执行过程

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SeUv9ptA-1662905170676)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/63742acd-b9d9-40ce-9f59-3f6a9eb03872/Untitled.png)]

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队列结构中实际包含了多个进程队列,它们将进程按优先级划分,相同优先级的链接在一起,成为一个优先级队列。所有优先级队列的头地址都记录在一个优先级数组中,按优先级顺序排列。实时进程的优先级队列在前(1~99),普通进程的优先级队列在后(100~139)。当进程调度选择进程时,只需在优先级数组中选择当前最高优先级队列中的第1个进程即可。无论进程的多少,这个操作总可以在固定的时间内完成,因而是O(1)级别的。

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

7.2.3 Linux的进程调度策略

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

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

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

3)普通调度法
普通调度法(NORMAL,Normal 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标志,如果已被设置,内核将调用调度函数进行重新调度。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芯片烧毁大师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值