进程原理

进程原理

进程

进程概论

进程是执行中的程序,一个程序加载到内存中后就变为进程。进程=程序+执行。

为了提高CPU利用率,人们想起将多个程序同时加载到计算机里,并发执行。进程让每个用户感觉自己独占CPU。

进程模型

从物理内存的分配来看,每个进程占用一片内存空间。

在物理层面上,所有进程共用一个程序计数器。

从逻辑层面上看,每个进程有着自己的计数器,记录其下一条指令所在的位置。

从时间上看,每个进程都必须往前推进。

进程不一定必须终结。事实上,许多系统进程是不会终结的,除非强制终止或计算机关机。

对于操作系统来说,进程是其提供的一种抽象,目的是通过并发来提高系统利用率,同时还能缩短系统响应时间。

多道编程的好处

人们发明进程是为了支持多道编程,而进行多道编程的目的则是提高计算机CPU的效率,或者说系统的吞吐量。

除了提高CPU利用率外,多道编程更大的好处是改善系统响应时间,即用户等待时间。

多道编程带来的好处到底有多少与每个程序的性质、多道编程的度数、进程切换消耗等均有关系。但一般来说,只要度数适当,多道编程总是利大于弊。

进程的产生与消亡

造成进程产生的主要事件有:

  • 系统初始化
  • 执行进程创立程序
  • 用户请求创立新进程

造成进程消亡的事件:

  • 进程运行完成而退出。
  • 进程因错误而自行退出
  • 进程被其他进程所终止
  • 进程因异常而被强行终结

进程的层次结构

一个进程在执行过程中可以通过系统调用创建新的进程,这个新进程就称为子进程,而创建子进程的进程称为父进程。子进程又可以创建子进程,这样子子孙孙创建下去就形成了所谓的进程树。

Unix称这个进程树里所有的进程为一个进程组,进程组里面的进程分布在不同的层次上,从而形成一个层次架构。Windows没有进程组的概念,而是所有进程均地位平等。

进程的状态

进程可以在CPU上执行,也可以处于挂起状态。挂起的原因有:

  • 一个进程在运行过程中执行了某种阻塞操作,如读写磁盘,由于阻塞操作需要等待结果后才能继续执行,因此操作系统将把这个进程挂起,让其他进程运转。
  • 一个进程执行的时间太长了,为了公平,操作系统将其挂起,让其他程序也有机会执行。

操作系统在进程调度时就只需要查看第二种类型的挂起,而非第一类自身原因造成的挂起。

进程分为3种状态:执行、阻塞和就绪。

状态转换:

stateDiagram 执行 --> 就绪 执行 --> 阻塞 阻塞 --> 就绪 就绪 --> 执行

为何没有以下状态转换

阻塞->执行:阻塞进程即使给予CPU,也无法执行,因此操作系统在调度时并不会从阻塞队列里挑选。

就绪->阻塞:处于就绪状态的进程并没有执行,自然无法进入阻塞状态。

这里阐述的进程的3种典型状态并不是唯一的分类方式。事实上,许多商业操作系统的进程状态不止3个。不管是多少个,其目的都是便于操作系统管理进程。

进程与地址空间

进程与地址空间演技丶主要内容是如何让多个进程空间共享一个物理内存。具体来说,就是高效、安全地让所有进程共享这片物理内存。

进程管理

进程管理所需要的手段

当一个进程产生时,操作系统需要为其创建记录。操作系统用于维护进程记录的结构就是进程表或进程控制块(Process Control Block)。显然,不同的操作系统维护的进程记录不同。但一般来说,应当包括寄存器、程序计数器、状态字、栈指针、优先级、进程ID、信号、创建时间、所耗CPU时间、当前持有的各种句柄等。而采纳的数据结构主要是线性表、链表和结构体,当然也可能使用树和图结构。

进程的创建过程

  1. 分配进程控制块
  2. 初始化机器寄存器
  3. 初始化页表
  4. 将程序代码从磁盘读进内存
  5. 将处理器状态设置为用户态
  6. 跳转到程序的起始地址(设置程序计数器)

这里一个最大的问题是,跳转指令是内核态指令,而在第5步时处理器状态已经被设置为用户态。硬件必须将第5步和第6步作为一个步骤一起完成。

进程创建在不同操作系统里方法也不一样:

  • Unix:fork创建一个与自己完全一样的新进程;exec将新进程的地址空间用另一个程序的内容覆盖,然后跳转到新程序的起始地址,从而完成新程序的启动。
  • Windows:使用一个系统调用CreateProcess就可以完成进程创建。把欲执行的程序名称作为参数传过来,创建新的页表,而不需要复制别的进程。

Unix的创建进程要灵活一点,因为我们既可以自我复制,也可以启动新的程序。而自我复制在很多情况下是很有用的。而在Windows下,复制自我就要复杂一些了。而且共享数据只能通过参数传递来实现。

进程管理要处理的问题

进程管理的最大问题是资源分配。除了公平之外,还要考虑效率最优。每个进程分配同样的资源肯定不行,不如让部分人先富起来,给他们使用资源的优先权。

进程调度

进程调度的定义

在多进程并发的环境里,虽然从概念上看,有多个进程在同时执行,但在单CPU下,实际上在任何时刻只能由一个进程处理执行状态。

程序使用CPU的模式有3种:

  • 程序大部分时间在CPU上执行,称为CPU导向或计算密集型程序。计算密集型程序通常是科学计算方面的程序。
  • 程序大部分时间在进行输入输出,称为I/O导向或输入输出密集型程序。一般来说,人机交互式程序均属于这类程序。
  • 介于前两种之间,称为平衡型程序。例如,网络浏览或下载、网络视频。

对于不同性质的程序,调度所要达到的目的也不同:

  • CPU导向的程序:周转时间turnaround比较重要
  • I/O导向的程序:响应时间非常重要
  • 平衡型程序:两者之间的平衡

进程调度的目标

CPU调度就是要达到:

  • 极小化平均响应时间:极小化用户发出命令和看到结果之间所花费的时间,即减少做一件工作平均等待的时间
  • 极大化系统吞吐率:在单位时间内完成尽可能多的程序
  • 保持系统各个功能部件均处于繁忙状态:闲置即浪费
  • 提供某种貌似公平的机制

对于不同的系统来说,在调度目标方面也有一些细微的不同:

  • 对于批处理系统,响应时间不太重要,但系统吞吐率、CPU利用率和周转时间则很重要
  • 对于交互式系统来说,响应时间比较重要,但这里要注意适度性。适度性就是响应时间要和期望值匹配,不要超越用户的期望。这是因为,提供超出用户期望的相应会增加系统设计的难度,而又不会提高用户的满意度。
  • 对于实时系统来说,在截止时间前完成所应该完成的任务和提供性能可预测性。

先来先服务调度算法First Come First Serve

类似排队,先来先到的原则。缺点是短的工作由可能因为前面有很长的工作变得很慢。这样就造成用户的交互式体验比较差。

时间片轮转算法

时间片轮转算法是对FCFS算法的一种改进,其主要目的是改善短程序的相应时间,其方法就是周期性地进行进程切换。

系统响应时间依赖于时间片的选择。如果时间片过大,则越来越像FCFS。如果时间片过小,则进程切换所用的系统消耗将太多,从而降低系统效率,并造成浪费。

那如何选择一个合适的时间片呢?我们需要知道进行一次进程切换所用的系统消耗和我们能够承受的整个系统消耗,就可以得出合适的时间片。还需要考虑的一个因素是有多少进程在系统里运行。如果运行的进程多,时间片就需要短一些,不然用户的交互体验会很差。

短任务优先算法Shorted Time to Completion First

这种算法的核心是短任务的优先级比长任务的高。

短任务优先算法有两个变种:

  • 非抢占:让已经在CPU上运行的程序执行到结束或阻塞,然后在所有候选的程序中选择执行时间最短的进程。
  • 抢占:每增加一个新的进程就需要对所有进程(包括正在运行的进程)进行检查,选择执行时间最短的进程。

事实上,在所有非抢占调度算法中,STCF算法的响应时间最优;而在所有抢占调度算法中,抢占式STCF的响应时间最优

缺点:

  • 可能造成长程序无法得到CPU而导致饥饿
  • 如何知道每个进程还需要运转多久?我们可以用一些启发式heuristic方法来进行估算。

优先级调度算法

可以赋予重要的进程以高优先级以确保重要任务能够得到CPU时间。

缺点:

  • 低优先级进程可能会饥饿(可以动态地调节优先级)
  • 响应时间不能保证

混合调度算法

将所有进程分成不同的大类,每个大类为一个优先级。如果两个进程处于同一个大类,则采用时间片轮转来执行。

其他调度算法

  • 保证调度Guaranteed Scheduling:保证每个进程占用CPU的时间完全一样。保证调度不一定要轮转,每次给的时间片不一定要一样。
  • 彩票调度Lottery Scheduling:概率调度算法,给每个进程分发一定数量的彩票,而调度器则从所有彩票里随机抽取一张彩票,中奖的进程就获得CPU。彩票调度的优越性:通过给每个进程至少一张彩票可以防止饥饿;可以用于模拟其他进程调度算法。
  • 用户公平调度Fair Share Scheduling Per User:按照每个用户而不是每个进程来进行公平分配。如果一个用户的进程多,则其所拥有的进程所获得的CPU时间将短。贪婪的用户可以通过启动许多进程来抢占CPU时间,用户公平调度可以解决这个问题。

实时调度算法

实时系统是一种必须提供时序可预测性的系统。实时系统必须考虑每个具体任务的相应时间必须符合要求,即每个任务在什么时间之前完成,而无须考虑如何降低整个系统的响应时间或吞吐率。

EDF调度算法Earlist Deadline First

最早截止的任务先做。动态地计算每个任务的截止时间并动态调节优先级。如果需要,还会对当前进程进行抢占。

EDF调度算法就是STCF算法变化来的。如果将STCF算法的任务所需执行时间变为截止时间,则抢占式STCF算法就是EDF调度算法。

虽然EDF算法在理论上是最优的,但动态计算截止时间和动态抢占CPU均要消耗系统资源。

RMS调度算法Rate Monotonic Scheduling

在进行调度前先计算出所有任务的优先级,然后按照计算出来的优先级进行调度,任务执行中间既不接收新的进程,也不进行优先级的调整或进行CPU抢占。

缺点是不灵活。

对于RMS算法来说,一个重要的任务是判断一个任务组能否调度。而这个判断并不是容易做的。具体来说,一个系统里所有任务的截止时间如果想都得到满足,则这些任务必须满足下面的条件:

\[U=\sum_{i=1}^n \frac {c_i}{p_i}\le n(\sqrt[n]2-1) \]

n为任务是数量,ci为第i个任务的执行时间,pi为第i个任务的释放周期。当n趋于无穷时,U=ln2。

根据上述公式,如果CPU利用率在ln2以下时,所有任务的截止时间均可满足。因为此时系统还剩下约30%的CPU时间。这个时间可以用来处理一些非实时任务。

RMS算法为静态最优算法。即如果任何静态优先级算法可以满足一组任务的截止时间,则RMS算法也必能满足。

进程调度的过程

  1. 因时序或外部中断或进程挂起而导致操作系统获得CPU控制权
  2. 操作系统在所有就绪的进程中按照某种算法遴选进程
  3. 如果选中的是非当前进程,则操作系统将当前进程状态予以保护
  4. 将选中的进程环境布置好(设置寄存器、栈指针、状态字等)
  5. 跳转到选中的进程

进程通信

进程之间的交互称为进程间通信(Inter-Process Communication,IPC)。

进程对白:管道、记名管道、套接字

进程对白就是一个进程发出某种数据信息, 另外一方接收数据信息,而这些数据信息通过一片共享的存储空间进行传递。

管道

在这种方式下,一个进程向这片存储空间的一端写入信息,另一个进程从存储空间的另外一端读取信息。这看上去就像管道。管道所占的空间既可以是内存,也可以是磁盘。要创建一个管道,一个进程只需调用管道创建的系统调用即可。该系统调用所做的事情就是在某种存储介质上划出一片空间,赋给其中一个进程写的权利,另一个进程读的权利即可。

从根本上说,管道是一个线性字节数组,类似文件,可以使用文件读写的方式进行访问。但却不是文件。因为通过文件系统看不到管道的存在。管道可以设在内存里,而文件很少设在内存里(当然,有研究人员在研发基于内存的文件系统,但这个还不是主流)。

创建管道在壳命令行下和在程序里是不同的。

  • 壳(shell)命令行下,只需要使用符号“|”即可。例如,在UNIX壳下,我们可以键入如下命令:
    $sort<file1|grep zou
  • 在程序里面,创建管道需要使用系统调用popen()或者pipe()。
    • popen()需要提供一个目标进程作为参数,然后在调用该函数的进程和给出的目标进程之间创建一个管道。创建时还需要提供一个参数表明管道类型:读管道或者写管道。
    • 而pipe()调用将返回两个文件描述符(文件描述符是用来识别一个文件流的一个整数,与句柄不同),其中一个用于从管道进行读操作,一个用于写入管道。也就是说,pipe()将两个文件描述符连接起来,使得一端可以读,另一端可以写。通常情况下, 在使用pipe()调用创建管道后,再使用fork产生两个进程,这两个进程使用pipe()返回的两个文件描述符进行通信。

管道的一个重要特点是使用管道的两个进程之间必须存在某种关系。

记名管道

如果要在两个不相关的进程(如两个不同进程里面的进程)之间进行管道通信,则需要使用记名管道。顾名思义,命名管道是一个有名字的通信管道。记名管道与文件系统共享一个名字空间,即我们可以从文件系统中看到记名管道。也就是说,记名管道的名字不能与文件系统里的任何文件名重名。

一个进程创建一个记名管道后,另外一个进程可使用open来打开这个管道(无名管道则不能使用open操作),从而与另外一端进行交流。

记名管道的名称由两部分组成:计算机名和管道名,例如\\[主机名]\管道\[管道名]\。对于同一主机来讲,允许有多个同一命名管道的实例并且可以由不同的进程打开,但是不同的管道都有属于自己的管道缓冲区而且有自己的通信环境,互不影响。命名管道可以支持多个客户端连接一个服务器端。命名管道客户端
不但可以与本机上的服务器通信也可以同其他主机上的服务器通信。管道和记名管道虽然具有简单、无需特殊设计(指应用程序方面)就可以和另外一个进程进行通信的优点, 但其缺点也很明显。首先是管道和记名管道并不是所有操作系统都支持。主要支持管道通信方式的是UNIX和类UNIX(如Linux)的操作系统。这样,如果需要在其他操作系统上进行通信,管道机制就多半会力不从心了。其次,管道通信需要在相关的进程间进行(无名管道),或者需要知道按名字来打开(记名管道), 而这在某些时候会十分不便。

虫洞:套接字

套接字(socket)是另外一种可以用于进程间通信的机制。套接字首先在BSD操作系统中出现,随后几乎渗透到所有主流操作系统中。套接字的功能非常强大,可以支持不同层面、不同应用、跨网络的通信。使用套接字进行通信需要双方均创建一个套接字,其中一方作为服务器方,另外一方作为客户方。服务器方必须先创建一个服务区套接字,然后在该套接字上进行监听,等待远方的连接请求。欲与服务器通信的客户则创建 一个客户套接字,然后向服务区套接字发送连接请求。服务器套接字在收到连接请求后,将在服务器方机器上创建一个客户套接字,与远方的客户机上的客户套接字形成点到点的通信通道。之后,客户方和服务器方就可以通过send和recv命令在这个创建的套接字通道上进行交流了。

使用套接字进行通信稍微有点复杂,我们下面以一个网页浏览的例子对套接字这种通信方式予以说明。对于 一个网站来说,要想提供正常的网页浏览服务,其网站服务器需要首先创建一个服务区套接字,作为外界与本服务器的通信信道。为了使该信道为外人所知,我们通常将该服务区套接字与某公共主机的一个众所周知
的端口进行绑定。

#创建一个INET的流套接字
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM);
#将套接字与某公共主机的一个众所周知的端口绑定
serversocket.bind(socket.gethostname(),80);

进行套接字和端口绑定的语句里的socket.gethostname()用来将套接字向外界公开。如果将该语句的使用socket.gethostname()改为''或者'localhost或者某个具体的IP地址(如120.121.4.1),则该服务区套接字将被限制在本地机器上使用。

在创建了服务区套接字并将其向外界公开后,网站服务器就可以在该套接字上进行监听。

serversocket.listen(5); // 将套接字变为一个服务区套接字

将端口上的等待队列长度限制为5,即超过5个的请求将被拒绝。

到这里,服务器方的设置就宣告结束。

对于客户方来说,如要访问上述网站,则需要点击该网站的网址。在点击网址后(我们这里假定该网站网址为www.sjtu.edu.cn),客户机上的网络浏览器进行若干步操作:

#创建一个INET的流套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM);
#将套接字与某公共主机的一个众所周知的端口绑定
s.connect("www.sjtu.edu.cn",80);

s.connect命令将向服务器www.sjtu.edu.cn在端口80打开的服务器套接字发送连接请求。而服务器端在接收到该连接请求后,将生成一个新的客户端套接字与该客户端套接字对接,从而建立一个套接字通信信道。
网站服务器上运行的主循环

while (true)
{
	(clientsocket, address) = serversocket.accept(); // 接收外部连接请求
	// 对clientsocket进行相关操作,例如创建一个新进程来处理客户请求
	newct = client_thread(clientsocket);
	newct.run();
}

至此,套接字通信信道成功创建。客户端程序可以使用套接字s来发送请求、索取网页,而服务器端则使用套接字clientsocket进行发送和接收消息。

这里需要指出的是服务区套接字既不发送数据,也不接收数据(指不接收正常的用户数据,而不是连接请求数据),而仅仅生产出“客户”套接字。当其他(远方)的客户套接字发出一个连接请求时,我们就创建一个客户套接字。一旦创建客户套接字clientsocket,与客户的通信任务就交给了这个刚刚创建的客户套接字。而
原本的服务器套接字serversocket则回到其原来的监听操作上。

套接字由于其功能强大而获得了很大发展,并出现了许多种类。不同的操作系统均支持或实现了某种套接字功能。例如按照传输媒介是否为本地,套接字可以分为本地(UNIX域)套接字和网域套接字。而网域套接字又按照其提供的数据传输特性分为几个大类,分别是:

  • 数据流套接字(stream socket):提供双向、有序、可靠、非重复数据通信。
  • 电报流套接字(datagram socket):提供双向消息流。数据不一定按序到达。
  • 序列包套接字(sequential packet):提供双向、有序、可靠连接,包有最大限制。
  • 裸套接字(raw socket):提供对下层通信协议的访问。

套接字从某种程度上来说非常繁杂,各种操作系统对其处理并不完全一样。因此,如要了解某个特定套接字实现,读者需要查阅关于该套接字实现的具体手册或相关文档。

进程电报:信号

管道和套接字的缺点:

  • 如果使用管道和套接字方式来通信,必须事先在通信的进程间建立连接(创建管道或套接字),这需要消耗系统资源。
  • 通信是自愿的。即一方虽然可以随意向管道或套接字发送信息,但对方却可以选择接收的时机。即使对方对此充耳不闻,你也奈何不得。再次,由于建立连接消耗时间,一旦建立,我们就想进行尽可能多的通信。而如果通信的信息量微小,如我们只是想通知一个进程某件事情的发生,则用管道和套接字就有点“杀鸡用牛刀”的味道,效率十分低下。

那么信号是什么呢?在计算机里,信号就是一个内核对象,或者说是一个内核数据结构。发送方将该数据结构的内容填好,并指明该信号的目标进程后,发出特定的软件中断。操作系统接收到特定的中断请求后,知道是有进程要发送信号,于是到特定的内核数据结构里查找信号接收方,并进行通知。接到通知的进程则对信号进行相应处理。

进程旗语:信号量

在计算机里,信号量实际上就是一个简单整数。一个进程在信号变为0或者1的情况下推进,并且将信号变为1或0来防止别的进程推进。当进程完成任务后,则将信号再改变为0或1,从而允许其他进程执行。需要注意的是,信号量不只是一种通信机制,更是一种同步机制。

进程拥抱:共享内存

共享内存就是两个进程共同拥有同一片内存。对于这片内存中的任何内容,二者均可以访问。要使用共享内存进行通信,一个进程首先需要创建一片内存空间专门作为通信用,而其他进程则将该片内存映射到自己的(虚拟)地址空间。这样,读写自己地址空间中对应共享内存的区域时,就是在和其他进程进行通信。

与管道的区别:

  • 使用共享内存机制通信的两个进程必须在同一台物理机器上;
  • 共享内存的访问方式是随机的,而不是只能从一端写,另一端读,因此其灵活性比管道和套接字大很多,能够传递的信息也复杂得多。

缺点:

  • 管理复杂,且两个进程必须在同一台物理机器上才能使用这种通信方式。
  • 安全性脆弱。因为两个进程存在一片共享的内存,如果一个进程染有病毒,很容易就会传给另外 一个进程。就像两个紧密接触的人,一个人的病毒是很容易传染另外一个人的。

这里需要注意的是,使用全局变量在同一个进程的进程间实现通信不称为共享内存。

信件发送:消息队列

与管道的区别:

  • 它无需固定的读写进程,任何进程都可以读写(当然是有权限的进程)。
  • 它可以同时支持多个进程,多个进程可以读写消息队列。即所谓的多对多,而不是管道的点对点。
  • 消息队列只在内存中实现。
  • 它并不是只在UNIX和类UNIX操作系统中实现。几乎所有主流操作系统都支持消息队列。

其他通信机制

除了上面介绍的主流通信方式外,有些操作系统还提供了一些其所特有的通信机制,例如Windows支持的进程通信方式就有所谓的剪贴板(clipboard)、COM/DCOM、动态数据交换(DDE)、邮箱(mailslots);而Solaris则有所谓的Solaris门机制,让客户通过轻量级(16KB)系统调用使用服务器的服务。

虽然进程之间的通信机制繁多,且每种机制都有着自己的特性,但归根结底都来源于AT&T的UNIX V系统。该系统在1983年加入了对共享内存、信号量和消息队列的支持。而这三者就是众所周知的System VIPC(POSIX IPC也是源于该系统并成为当前IPC的标准)。因此,虽然不同操作系统的IPC机制可能不尽相同,但其基本原理则并无大的区别。如果需要了解具体操作系统的IPC机制的实现,读者可以阅读相关的操作系统内核教程。

posted @ 2021-04-21 18:49  睿阳  阅读(457)  评论(0编辑  收藏  举报