线程与进程的另一面

 好久没有来园子写文章了,今天我分享一篇关于进程和线程理论相关的内容,很多人都在用多线程、多进程或者线程池、、进程池,但是你是否清楚为什么要这样去用,从哪些方面去考虑的。如果你还不是特别的清楚,还是建议你读一读。

操作系统之进程

第一、进程的概念

进程概念是操作系统中最基本、最重要的概念之一。进程是一个正在执行中的程序或者可以描述为可并发执行的程序在一个数据集合上的运行过程。进程不只是程序代码,程序代码有时称为文本段(或代码段)。进程还包括当前活动、程序计数器的值和处理器寄存器的内容。另外,进程通常还包括进程堆栈段(包括临时数据,如函数参数、返回地址和局部变量)和数据段(包括全局变量)。进程还可能包括堆 ( heap),它是在进程运行期间动态分配的内存。

 

从操作系统管理角度出发,进程由数据结构以及在其上执行的程序组成,是程序在这个数据集合上的运行过程,也是操作系统进行资源分配和调度的基本单位。因此进程需要一些资源,比如:CPU时间、内存空间、文件以及I/O设备。这些资源可以在进程创建时分配,或者可以在进程运行过程中动态分配。

 

作为描述程序执行过程的概念,进程具有动态性、独立性、并发性和结构化等特征。

动态性是进程的最基本特征,它是程序执行过程,它是有一定的生命期。它由创建而产生、由调度而执行,因得不到资源而暂仃,并由撤消而死亡。

 

独立性是指各进程的地址空间相互独立,除非采用进程间通信手段,否则不能相互影响。

并发性也称为异步性,是指从宏观上看,各进程是同时独立运行的。

结构化是指进程地址空间的结构划分,如代码段、数据段等。

 

进程和程序是两个密切相关的不同概念,它们在以下几个方面存在区别和联系:

进程是动态的,程序是静态的。程序是有序代码的集合;进程是程序的执行。进程

通常不可以在计算机之间迁移;而程序通常对应着文件、静态和可以复制。

进程是暂时的,程序是永久的。进程是一个状态变化的过程;程序可长久保存。

进程与程序的组成不同:进程的组成包括程序、数据和进程控制块(即进程状态信息)。

进程与程序是密切相关的。通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序。进程可创建其他进程,而程序并不能形成新的程序。

 

第二、进程的状态与转换

进程与程序的最大区别是其动态性。随着进程的推进,它会不断改变状态。进程状态由具体操作系统根据进程当前活动而定义。例如,每个进程可处于如下状态:创建、就绪运行、等待(阻塞)、终止。

通常讲的进程三个基本状态是指就绪状态、运行状态和等待或阻塞状态

当前不在执行的进程会放在某个等 待队列中。究竟处于哪个等待队列 ,取决 于进程正在等待什么资源或什么事件。其中就绪队列有其特殊性,就绪队列中的进程正在等待CPU资源;而且,进程等待CPU之时,它一定已经得到了其它一切必需的资源。

 

当进程状态发生变化时,导致进程状态转换此时,必然引起资源的释放或申请。例如,进程得到CPU时,从就绪状态变成运行状态;进程需要一个资源却暂时得不到该资源时,从运行状态变成等待状态。

 

进程是个抽象的概念,它无法亲自挂在等待队列里“排队”。在计算机这种“0/1 ”世界里 ,进程需要一个具体的代表参与管理。这就是进程控制块(Process ControlBlock,PCB)。每个进程都有唯一的PCB与之对应。进程在各种队列之间转换,其实是进程的PC B在对应队列里转换;PCB拥有指针,处于同一队列的PCB由这些指针实现相互间的链接。PCB可以说是操作系统中最复杂的数据结构,它包含了所有描述其对应进程的独有信息。对进程的控制,实际上就是对进程PCB的控制。

 

操作系统在管理进程过程中需要负责以下工作:

进程的创建和删除;

进程的调度;

进程的同步、进程的通信、进程死锁的处理等。

 

当然,都通过管理PCB而得以实现。进程控制块存储了某一具体进程的信息,包括:

进程状态:该状态可能是新、就绪、运行、等待、终止等等。?

程序计数器:计数器指明了该进程要执行的下一条指令的地址。

CPU寄存器:基于计算机体系结构,这些寄存器的数量和类型不相同。包括累加器、变址寄存器、栈指针、通用寄存器等。和程序计数器一样,在中断发生时必须要保存这些状态信息,这样便于后来进程继续正确执行。

CPU调度信息:包括进程优先权、指向调度队列的指针和其它的调度参数。

内存管理信息:包括基址寄存器和界限寄存器值、页表或段表、虚拟地址空间管理信息等。

记账信息:包括CPU时间、实际使用时间、账户数目、作业或进程数目等。

I/O 状态信息:包括分配给该进程的I/O设备列表、打开文件的列表等。存放在磁盘上的可执行文件的代码和数据的集合称为可执行映象(Ex ecut abl eImage)。当一个程序装入系统中运行时,它就形成了一个进程。进程是由程序代码、数据和进程控制块组成。进程上下文(context)实际上是进程执行活动全过程的静态描述。

 

具体说,进程上下文包括系统中与执行该进程有关的各种寄存器(例如:通用寄存器、程序计数器PC、程序状态寄存器PS等)的值,程序的机器指令代码集(或称正文段)、数据集及各种堆栈值和PC B结构。

 

上下文切换

当进程调度程序选择一个新进程并为其分配CPU时,要将CPU切换到另一个进程,要求首先保存原来进程的上下文,然后装入新进程的上下文。将CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态,这一任务称为上下文切换(context switch)。也就是说,当发生上下文切换时,内核会将旧进程的上下文保存在其PCB中,然后装入经调度要执行的新进程的已保存的上下文。上下文切换时间是额外开销,因为切换时系统并不能做任何用户进程要求的工作。进程上下文切换通过中断实现,当发生一个中断时,系统需要保存当前运行在CPU中进程的上下文,从而在其处理完后能恢复上下文,即先中断进程,之后再继续。进程上下文用进程的PCB表示,它包括CPU寄存器的值、进程状态和内存管理信息等。通常,通过执行一个状态保存(state save) 来保存CPU当前状态(不管它是内核模式还是用户模式),之后执行一个状态恢复(state restore) 重新开始运行。

 

第三、进程操作和进程通信

一、 进程操作计算机系统启动时没有进程。操作系统创建第1个进程,是所有进程的祖先。祖先进程创建新的进程,就是它的子进程。父进程创建子进程,如此重复,形成了进程组织的树型结构。

系统中的进程能够并发执行,它们必须要动态的创建和终止(撤销),操作系统必须要提供进程创建和终止的机制。属于进程控制方面的操作主要用于对进程的创建、进程的撤消和进程状态间转换控制。

(1) 进程创建

进程在运行期间通过创建进程系统调用可以创建多个新进程,新进程是它的子进程,每个新进程可以再创建其他进程,从而形成了进程树。创建一个进程主要是为新进程创建一个进程控制块(PCB)。创建进程系统调用首先从系统的PCB表中索取一个空白的PCB表目,并获得其内部标识,然后将调用进程提供的参数,如外部名、正文段、数据段的首址、大小、所需资源、优先级等填入这张空白PCB表目中。并设置新进程状态为就绪态,并把该PCB插入到就绪队列中,就可进入系统并发执行。

 

当进程创建新进程时,有两种执行可能:

① 进程与子进程并发执行。

② 父进程等待,直到某个或全部子进程执行完。

新进程的地址空间也有两种可能:

① 进程是父进程的复制品(具有与父进程相同的程序和数据)。

② 子进程装入另一个新程序。

 

为了说明这些不同实现,现在来看一下UNIX类操作系统。在UNIX中,每个进程都用一个唯一的整数形式的进程标识符来标识。通过fork()系统调用,可创建新进程。新进程通过复制原来进程的地址空间而成。这种机制允许父进程与子进程方便地进行通信。两个进程(父进程和子进程)都继续执行位于系统调用fork()之后的指令。但是,有一点不同:对于新(子)进程,系统调用fork()的返回值为0; 而对于父进程,返回值为子进程的进程标识符(非零)。通常,在系统调用fork()之后,一个进程会使用系统调用exec(),以用新程序来取代进程的内存空间。系统调用exec()将二进制文件装入内存,并开始执行。采用这种方式,两个进程能相互通信,并能按各自的方法执行。父进程能创建更多的子进程,或者如果在子进程运行时没有什么可做,那么它采用系统调用wait()把自己移出就绪队列来等待子进程的终止。

下面所示的C 程序说明了上述UNIX系统调用。现在有两个不同的进程运行同一程序。子进程的pid 值为0 ,而父进程的pid值大于0。子进程通过系统调用execlp()(execlp()是系统调用exec()的一种版本),用UNIX命令/bin/ls (用来列出文件和目录清单)来覆盖其地址空间。父进程通过系统调用wait()来等待子进程的完成。当子进程完成时(通过显式或隐式调用exit()) ,父进程会从wait()调用的下一指令继续执行,并调用系统调用exit()以表示结束。

(2) 进程终止

对于树型层次 结构的进程系统终止系统调用采用的策略 是由父进程发出,终止它的一个子进程及该子进程所有的子孙进程,被终止进程的所有资源(主存、外存、PCB表目)全部释放出来归还系统,并将它们从所有的队列中移去。如终止的进程正在运行,则要调用进程调度程序将处理器分给其它进程。

(3) 进程的阻塞

当前进程因请求某事件而不能执行时(例如请求I/O而等待I/O完成时),该进程将调用阻塞系统调用阻塞自己,暂时放弃处理机。进程阻塞是进程自身的主动行为。阻塞过程首先立即停止原来程序的执行,把PCB中的现行状态由运行态改为阻塞态,并将PCB插入到等待某事件的阻塞队列中,最后调用进程调度程序进行处理机的重新分配。

(4) 进程的唤醒

当被阻塞的进程所期待的事件发生时(例如I/O完成时),则有关进程(例如I/O设备处理 程序或 释放 资源的进程等)调用唤醒 系统调用,将阻塞 的进程 唤醒 ,将等待该事件的进程从阻塞队列移出,插入到就绪队列中,将该进程的PCB中现行状态改为就绪状态。

 

进程间通信

进程间通信(Inter-Process Communication, IPC)要解决的问题是进程间的信息交换这种信息交换的量可大可小。操作系统提供了多种进程间通信机制,可分别适用于多种不同的场合,要特别关注的三种进程间通信:共享存储机制、消息传递机制、管道通信机制。

(1) 共享存储系统

在这种通信方式中,在内存中划出一块存储区 ,供多个进程共享,共享进程通过对这一共享存储区中的数据进行读或写来实现通信。共享存储区是进程通信速度最快的一种通信机制,且可以实现大量数据传送。

(2) 消息传递系统

在消息传递系统中,进程间的数据交换以消息(message)为单位,在计算机网络中,消息又称为报文。消息传递系统因其实现方式不同,又可分为直接通信方式和间接通信方式两种。消息传递机制至少需要提供两条原语send和receive,send向一个给定的目标发送一个消息,receive则从一个给定的源接受一条消息。在直接通信方式下,发送或接收消息的每个进程必须指出消息发给谁或从谁那里接收消息,用send原语和receive原语实现进程之间的通信,这两个原语定义如下:send(P,消息):把消息发送给进程Preceive(Q,消息):从进程Q接收消息进程P和Q通过执行这两个操作自动建立了一种连接 ,并且这一种 连接仅仅发生在这P和Q进程之间。间接通信是消息传递的另一种方式。在这种情况,消息不直接从发送者发送到接收者,而是发送到暂存消息的共享数据结构组成的队列,这个实体称为信箱(mail box)。因此二个进程通信情况,一个进程发送一个消息到某个信箱,而另一个进程从信箱中摘取消息。 间接通信的使用好处是增加了使用消息的灵活性。发 送者和接收者的关系可能是一对一、多对一,一对多或多对多。一对一的关系允许一个专用通信链路用于二个进程间的交互,进程间 交互不 受其它进程错误 的影响;多对一的关系对 客户/服务器交互特别 有用,一个进程对多个其它进程(用户)提供 服务 ,在这种情况 信箱经 常称作为 端口;一对多关系允许一个发送进程和多个接收进程交互,这可用来将消息广播给一组进程。

(3) 管道通信

管道(pipe)是指用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,又称为管道文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符形式将大量的数据送入管道,而接收管道输出的接收进程(即读进程)可从管道中接收数据。由于发送进程和接收进程是利用管道通信的,故又称管道通信。管道通信是基于文件系统形式的一种通信方式。管道分为无名管道和有名管道两种类型。无名管道是一个只存在于文件系统中的一个临时文件,从结构上没有文件路径名,不占用文件目录项。有名 管道是建立在文件系统中长期存在的、具有路径名的文件,因而其它进程可以知道它的存在,并能利用该路径名来访问该文件。

操作系统之线程

第一、线程基本概念

线程定义为进程内一个执行单元或一个可调度实体。在不拥有线程的进程概念中,进程既是一个拥有资源的独立单位,它可独立分配虚地址空间、主存和其它,又是一个可独立调度和分派的基本单位。在有了线程以后,资源拥有单位称为进程(或任务),调度的单位称为线程、又称轻进程(Light Weight Process,LWP)。如果进程有多个控制线程,那么它能同时做多个任务。

多线程的进程在同一地址空间内包括多个不同的控制流,也即属于同一进程下的线程,它们共享进程拥有的资源,如代码、数据、文件等。线程也独占一些资源,如堆栈、程序计数器等。多线程系统的优点包括对用户响应的改进,进程内的资源共享,以及利用多处理器体系结构的便利。

 

第二、多线程模型

从实现的角度看,把线程分为用户级线程和内核级线程。

用户级线程对程序员来说是可见的,而对内核来说是未知的,用户空间的线程库通常用以管理用户级线程,线程库提供对线程创建、调度和管理的支持。因为内核并不知道用户级线程的存在,所有的线程创建和调度工作都在用户空间完成,而且整个过程不受内核的干涉。所以,用户级线程的创建和管理通常很快;然而,它们也有一些缺点。例如:如果内核是单线程的,那么任何用户级线程执行一个导致阻塞的系统调用时就会导致整个进程阻塞,即使程序内部的其它线程可以运行。内核级线程由操作系统支持和管理,在内核空间实现线程创建、调度和管理。因为线程管理由操作系统完成,所以内核线程的创建和管理要比用户线程慢。然而,由于线程由内核管理,如果一个线程执行一个导致阻塞的系统调用,那么内核可以调度程序中的其它线程执行。同样,在多处理机环境中,内核能够在不同的处理器中调度线程。大多数现代操作系统都支持内核线程。

 

有三种不同模型将用户级线程和内核级线程关联起来:

多对一模型将多个用户线程映射到一个内核线程;

一对一模型将每个用户线程映射到一个相应的内核线程;

多对多模型将多个用户线程在同样(或更少)数量的内核线程之间切换。

 

第三、线程库

线程库( thread library )为程序员提供创建和管理线程的API 。主要有两种方法来实现线程库。第一种方法是在用户空间中提供一个没有内核支持的库,此库的所有代码和数据结构都存在于用户空间中。调用库中的一个函数只是导致了用户空间中的一个本地函数调用,而不是系统调用。

第二种方法是执行一个由操作系统直接支持的内核级的库。此时,库的代码和数据结构存在于内核空间中。调用库中的一个API 函数通常会导致对内核的系统调用。

目前使用的三种主要的线程库是: (1) POSIX Pthread、(2) Win32 、(3) Java。

Pthread作为POSIX 标准的扩展,可以提供用户级或内核级的库。Win32 线程库是适用于Windows操作系统的内核级线程库。Java 线程API 允许线程在Java 程序中直接创建和管理。然而,由于大多数JVM 实例运行在宿主操作系统之上,Java 线程API 通常采用宿主系统上的线程库来实现。这意味着在Windows系统上,Java 线程通常用Win32 API实现,而在UNIX和Linux系统中采用Pthread

 

操作系统之CPU调度

第一、调度的基本概念

处理机调度负责动态地把处理器分配给进程或内核级线程。处理机调度也称CPU调度或进程调度,在有线程的操作系统中也称线程调度。为了最大限度的提高CPU 利用率,多道程序设计的目标是保持总是有进程可供执行。在单处理机系统中,一次只能运行一个进程;其它的任何进程都必须等到CPU 空闲时才能够被重新调度。

 

多道程序设计的思想十分简单。一个进程持续运行直到它必须等待某些操作(I/O请求是个典型)的完成。在简单的计算机系统中,进程等待时CPU 将处于空闲状态;这浪费了所有的等待时间。利用多道程序设计,我们可以有效地利用这段时间。在内存中同时保留多个进程。当一个进程必须等待时,操作系统将CPU 撤离该进程并把CPU 分配给另一个进程。然后以这种方式继续运行。

 

从处理器调度的对象、时间、功能等不同角度,我们可把处理器调度分成不同类型。处理器调度不仅涉及选择哪一个就绪进程进入运行状态,还涉及何时启动一个进程的执行。按照调度所涉及的层次的不同,我们可把处理器调度分成高级调度、中级调度和低级调度三个层次。

 

高级调度也称为作业调度或宏观调度。从用户工作流程的角度,一次作业提交若干个流程,其中每个程序按照流程进行调度执行。中级调度涉及进程在内外存间的交换。从存储器资源管理的角度来看,把进程的部分或全部换出到外存上,可为当前运行进程的执行提供所需的内存空间,将当前进程所需部分换入到内存。指令和数据必须在内存里才能被处理器直接访问。低级调度也称为微观调度。从处理器资源分配的角度来看,处理器需要经常选择就绪进程或线程进入运行状态。

 

第二、调度时机、切换与过程

有四种情况都会发生CPU调度:

当一个进程从运行状态转换到等待状态时;

当一个进程从运行状态转换到就绪状态时;

当一个进程从等待状态转换到就绪状态时;

当一个进程终止运行时

 

从操作系统角度观察,进程或者其PCB总是在各种等待队列中,或者在队列之间迁移。

其中,就绪队列是个特殊的队列,它所包含的进程都已经得到了几乎所有资源,只缺少CPU了。CPU调度的任务,就是从就绪队列中选择一个等待进程,并为其分配CPU;选

择的目标,则是如何使CPU得到最好的利用,用户进程得到最好的服务。从图中看,CPU调度使选中的进程从“就绪队列”节点,迁移到“CPU”节点。操作系统配备有专门的调度程序,列入schedule()函数,只有该调度程序被调用执行了,才会发生一次CPU调度。所谓调度时机,就是什么时候去调用这个程序。

 

第三、调度的基本准则

常见的指标有CPU利用率、吞吐率、响应时间、周转时间、等待时间等

 

第四、调度方式进程调度可采用下述两种方式:

(1)非抢占方式

采用这种调度方式时,一旦把处理机分配给某进程后,便让进程一直执行,直到该进程完成或发生某事件而 被阻塞 时,才把处理机分配给其它进程,不允许 某进程抢占 已经分配出去的处理机。

(2)抢占方式

这种调度方式,允许进程调度程序根据某个原则,去停止某个正在执行的进程,将已分配给进程的处理机,重新分配给另一个进程。

抢占的原则有:

●时间片原则。各进程按时间片运行,当一个时间片用完后,便停止该进程的执行而重新进行调度。这个原则适用于分时系统。

●优先权原则。通常是对一些重要的和紧急的进程赋予较高的优先权。当这种进程进入就绪队列时,例如由阻塞 态转换为就绪态,或从静止就绪态转为活动就绪态时,或新 创建 进入就绪态的进程进入就绪队列时,如果其优先权比正在执行的进程优先权高,便停止正在执行的进程,将处理机分配给优先权高的进程,使之执行。

第五、典型调度算法典型的调度算法有:

先来先服务调度算法,

短作业(短任务、短进程、短线程)优先调度算法,

时间片轮转调度算法,

优先级调度算法,高响应比优先调度算法,

多级反馈队列调度算法,等等。

 

先来先服务(First Come First Served,FCFS)调度算法是按照进程进入就绪队列的先后次序来挑选进程,先进入就绪队列的进程优先被挑中。先来先服务调度算法是最简单的调度算法,但是它会让短进程等待非常长的进程。FCFS会产生所谓的Belady异常现象。

 

最短作业/进程优先 (Shortest Job/Process First,SJF/SPF)调度算法是以进程所要求的CPU时间为标准,总是选取估计计算时间最短的进程 投入运行。最短进程 优先 调度算法是局部最佳的方法,它满足最短平均等待时间的要求。但实现SJF调度比较困难,因为预测进程的下一个CPU需求区间的长度有难度。SJF算法是优先权调度算法的特例,优先权调度和SJF调度会产生饥饿,老化(Aging)技术可解决饥饿问题。

 

时间片轮转(Round Robin,RR)调度算法是调度程序每次把CPU分配给就绪队列首进程使用一个时间片,例如100ms,就绪队列中的每个进程轮流地运行一个这样的时间片。时间片轮转调度算法对于分时(交互)系统更为合适。RR算法的主要问题是选择时间片,如果时间片太大,那么RR调度就成了FCFS调度;如果时间片太小,那么因上下文切换而引起的调度开销就过大。

优先级(Priority)调度算法是 根据确定 的优先 数来选取进程,每次总是选择优先 级高的进程。规定用户进程优先数的方法是多种多样的,进程的优先级的设置可以是静态的,也可以是动态的。

 

高响应比优先(Highest Response Ratio Next,HRRN)调度算法计算就绪队列进程的响应,调度时总是选择响应比最高的就绪进程得到CPU。响应比=(进程已等待时间+进程要求运行时间)/进程要求运行时间。此调度算法首先有利于短进程,但也兼顾到等待时间长的进程,该算法是FCFS算法和SJF算法的折衷。

 

多队列调度算法(Multilevel Queue,MQ)是根据进程的性质 和类型的不同,将就绪队列 再分为若干个子队列,所有进程根据不同情况排入相应的队列中,而不同的就绪队列采用不同的调度算法。最为常用的是前台交互队列(使用RR调度)和后台批处理队列(使用FCFS调度)的组合。

 

多级反馈队列(Multilevel Feedback Queue Scheduling,MFQ)调度算法在多级队列算法的基础上,允许就绪进程在队列之间迁移。

 

FCFS算法是非抢占的,RR算法是 抢占 的,SJF算法和 优先 级算法 既可以是 抢占 的,也可以是非抢占的。

如果操作系统在内核级支持线程,那么必须调度线程而不是调度进程。

 

       博客园的内容更新比较缓慢,如有需要了解 爬虫(图片识别、滑块验证)、机器学习 、自动化开发相关内容请加公众号:PythonCoder1024,谢谢大家的支持。

posted @ 2018-04-07 11:57  还是牛  阅读(680)  评论(0编辑  收藏  举报