zoukankan      html  css  js  c++  java
  • 【知识强化】第二章 进程管理 2.1 进程与线程

    处理机管理相关的内容。认识一个很重要的概念叫做进程。

    系统当中正在运行的程序怎么怎么地,怎么怎么地,偶尔也会提到进程这个术语。“进程”和“程序”这两个概念是比较容易混淆,不容易理解的。我们还会介绍一个进程由哪些部分组成,有什么重要的特征。

    所以其实所谓的“程序”,它是一种静态的,存放在磁盘里的可执行文件,在Windows电脑里就是QQ.EXE。那么这个可执行文件其实就是一系列的指令集合。而指令的概念我们在第一章当中讲解过,而所谓的进程它是动态的,是程序的一次执行过程。也就是说,即使是同一个程序,它多次执行,那么每一次的执行,都会对应一个不同的进程,就像我们在这个地方所看到的那样。既然这三个进程执行的都是同一个程序,那么操作系统在背后要怎么区分这三个进程呢?不能把它们都叫做腾讯QQ进程吧。

    所以其实为了解决这个问题,操作系统在创建一个进程的时候,会给这个进程分配一个唯一的、不重复的ID叫做PID,也就是进程ID。它就相当于我们人类世界的身份证号,我们每个人的身份证号都是唯一的、不重复的。而PID就是进程的身份证号。

    苹果的电脑有一个叫做活动监视器的一个小工具。它和Windows的那个任务管理器其实是一样的。现在在我的系统上正在运行的进程有哪些,那这些就是现在正在运行的进程。我们让这些进程根据PID的递减的次序来排列。

    我来打开一个叫做Typora的应用程序。

    我们每一次新建一个进程,都会给它分配一个不重复的、唯一的ID。在很多操作系统当中,PID的分配都是每一次加1这样的一个很简单的策略。

    现在系统在背后它又自己创建了一个新的进程,虽然我不知道这个进程是干嘛的,但是它的PID依然是递增的。

    虽然说这些进程,它们的进程的名称都叫“腾讯QQ(32位)”。但是它们在背后都有各自的PID,它们的PID肯定是不重复的。

    除了PID之外,操作系统还会记录各个进程其他的一些信息。比如说它使用了多久的CPU时间,它是由哪个用户所创建的。还有一些各个进程对内存的使用信息,比如这个进程使用了745.7MB字节的内存。

    另外操作系统还会统计各个进程对磁盘的一个访问的情况。比如说这个进程它向磁盘写入了3.45GB这么多的数据,而其他进程对磁盘写入和读取的数据会少很多。那这些信息其实在我们的进程管理当中都是很有必要的。像这个进程,它经常往磁盘里写数据,所以这个进程肯定是一个需要经常访问磁盘的一个进程。那操作系统在对这些进程进行管理的时候,都可以根据这些进程的各自的特性来制定相应的策略。

    那还有像最后的这一列,它是列出了各个进程的一个对网络流量的使用情况。所以其实操作系统在背后它不只是记录了各个进程的PID,

    除了PID之外它还记录了各个进程的其他的一些信息。比如我们刚才看到的它所属的用户ID,或者简称UID,那操作系统可以根据PID、UID这些基本的进程描述信息来区分各个进程。还有刚才我们看到的,分配了多少内存,正在使用哪些I/O设备,正在使用哪些文件。这些信息的记录可以帮助操作系统实现对系统资源的一个管理工作。第二呢,还有刚才我们看到的什么CPU使用时间、磁盘使用情况、网络流量使用情况等等。这些信息可以帮助操作系统实现对进程的控制和调度等等一系列的管理策略。那既然操作系统在背后要记录这么多的信息,那么这些信息都会被统一地放在一个叫做PCB的一个数据结构当中。它的英文缩写是Process Control Block,就是进程控制块。总之操作系统它需要对各个并发运行的进程进行管理,而但凡它在管理这些进程的时候,所需要用到的信息都会放在这个叫做PCB的数据结构当中。

    所以PCB是一个很重要的数据结构,它是进程存在的唯一标志。当一个进程被创建的时候,操作系统也会为它创建相应的PCB,当然这个PCB当中又包含了进程的PID、UID等等一系列的信息。而当一个进程结束的时候,操作系统就会回收它的PCB。那在PCB当中需要保存的信息大致上可以分为这样的四类,进程描述信息、进程控制和管理相关的信息还有资源分配的情况。这三种类型的信息刚才我们都已经介绍过,除此之外还会在PCB当中保留处理机相关的信息,那只有在PCB当中保存了处理机相关的信息才可以实现进程的切换工作。那这个坑我们先留着,之后再来填。

    在一个叫做sched.h这样的文件当中,我们就可以看到,在Linux操作系统当中,它定义的PCB长什么样子。在Linux当中,它的PCB的名字叫做task_struct,有兴趣的同学也可以大家去Linux的官网下载一下它的源码来看一下这个文件。那在这个task_struct里边,它会记录各种各样的信息。比如说这个字段state,这个字段它其实就是记录了进程当前的状态,它是处于就绪态呢、运行态呢还是阻塞态呢等等。那进程的状态这是我们下一个小节当中会展开介绍的内容。files这个字段它记录的是当前进程它打开了哪些文件。

    然后还有一个叫做io_context的字段,这个字段它记录的是I/O管理当中所需要使用的信息。

    当然这个数据结构里面定义了好多好多字段,光是这个数据结构的定义它就写了1900多行的代码,这么多代码是让人很抓狂的事情。总之我们想要认识PCB当中的所有字段那是不可能的,我们只需要知道PCB当中存放的都是操作系统在对进程进行管理的时候所需要的那些信息就可以了。那除了PCB之外呢,进程还有两个很重要的组成部分,一个叫做程序段,一个叫做数据段。刚才我们说过PCB它是给操作系统用的一个数据结构,而程序段和数据段它其实是给进程自己用的。

    程序它在运行之前需要编译成二进制的机器指令。而这个程序执行的过程其实就是CPU执行这些一条一条的指令的一个过程。那接下来我们把程序运行的过程再进一步地细化一下。

     其实我们写完一个程序之后,经过编译链接等等一系列的步骤,最后会形成一个可执行文件,像大家熟悉的Windows电脑里就是.exe的文件。那这个可执行文件平时是存放在硬盘当中的。这个可执行文件当中保存的其实就是我们刚才说的那一系列的指令序列。而当这个程序要运行之前,需要把它从硬盘读入到内存当中,并且操作系统会建立一个与它相对应的进程。那根据刚才我们的分析我们知道,它会建立相对应的PCB。那除了PCB之外,这个程序的那一系列指令序列也需要读到内存当中。那这一系列的指令序列我们把它称作为程序段,那其实这个程序执行的过程或者说这个进程它执行的过程就是CPU从内存当中读入这些一条一条的指令,然后来执行这些指令。那除了执行这些指令之外,其实在执行指令的过程当中会有一些中间的数据。比如说我们这儿定义了一个变量叫做x,那么这些变量的内容其实也需要放在内存当中,所以还会有另外一个叫做数据段的区域,用来存放这个程序运行过程当中所产生、所需要使用的各种数据。就比如说我们定义了哪些变量,这些信息就是放在数据段里的。所以一个进程的实体它由PCB、程序段和数据段这么三个部分组成。我们之前一直在说,进程由哪些部分组成,但其实更严格的来说,应该是说,进程实体或者说进程映像由哪些部分组成。进程它是动态的,而进程实体(进程映像)它是静态的。我们可以把进程实体理解为是这个进程在动态执行过程当中某一个时刻的一个快照,一个照片。进程实体能够反映这个进程在某一个时刻的状态,比如说这个进程运行的过程当中,x的值,这个变量的值本来是1,但是在进行了++这个操作之后,x的值就会变成2,所以在进程的运行过程当中进程实体它是在不断变化的。

    所以准确的说,我们应该说进程实体由PCB、程序段和数据段这么三个部分组成。不过除非题目特别考察进程和进程实体、进程映像的区别,不然大家也可以认为所谓的进程就是进程实体,没必要去钻这个牛角尖,抠这个字眼。那PCB就是给管理者也就是给操作系统使用的,而程序段和数据段里边的内容是给进程自己使用的,和进程自己的运行逻辑有关,所以在引入了进程实体的概念之后,我们可以把进程定义为这样:进程它是进程实体的一个运行过程,是系统进行资源分配和调度的独立单位。进程是资源分配的独立单位这点很好理解,从刚才活动监视器这儿我们也可以看到,操作系统是以进程为单位给各个进程来分配这些资源的,比如说内存,

    所以进程是资源分配的独立单位。那这个地方还涉及到另一个概念叫做进程的调度,其实所谓的调度就是指操作系统决定让哪个进程上CPU运行。进程的调度相关的内容我们会在之后的小节当中有更进一步的学习。总之一个程序运行多次会对应多个进程,如果说我们同时挂三个QQ号的话,那么这三个QQ进程它们的PCB和数据段都是各不相同的。它们的PCB不相同这个很好理解,刚才我们也已经解释过了。而数据段不相同是因为我们登录的三个QQ号它们的信息数据这些都是存放在数据段当中的,所以三个不同的QQ号它们的数据段肯定是不同的。但是这三个QQ进程背后,它们运行的其实是相同的QQ程序,也就是它们的程序段内容其实是相同的。那讲到这儿相信大家对进程和程序的理解应该会比较直观了。虽然说绕老绕去耗费了很多时间。

    我们之前介绍了进程的定义和进程的组成,接下来我们来看一下进程有哪些特征。相比于程序,进程拥有以下的一些特征,第一叫做动态性。因为进程是程序的一次执行过程,或者说进程是进程实体的一次执行过程,它是动态地产生、变化和消亡的,所以动态性很好理解,它也是进程的一个最基本的特性。第二个特性叫并发性,并发性是指内存当中同时会有多个进程实体,然后各个进程可以并发地执行,那这一点在经过第一章的学习,相信也很好理解。然后独立性是指进程能够独立运行、独立获得资源、独立接受调度。然后各个并发运行的进程还会有异步性,就是指它们以各自独立的、不可预知的速度向前推进。进程的并发带来的异步性可能会让进程的执行结果不确定,但是有的时候我们又必须让并发执行的进程来相互配合,协调着工作。那具体怎么实现呢,这个我们会在“进程同步”的小节当中进行学习,这儿先不展开。最后进程还拥有结构性,就是指每个进程都会有一个PCB,一个程序段、数据段。那所有的这些特性,大家都只需要理解,不需要死记硬背。

    好的那么这个小节我们介绍了进程这个很重要的概念,进程或者说进程实体由PCB、程序段和数据段这样三个部分组成。PCB是一个很重要很重要的数据结构,它是进程存在的唯一标志。操作系统就是通过PCB里边记录的这些各种各样的信息来对各个进程进行管理的。所以但凡是操作系统管理进程所需要的数据,肯定都是放在PCB当中,而进程它自己所需要的数据是放在程序段和数据段当中的。另外需要注意,进程的动态性是它最基本的特性,并且还需要注意,进程它是独立获得资源、独立接受调度的一个基本单位。让大家注意这一点的原因是,在引入了线程之后,进程就不再是接受调度的基本单位了,但是进程依然是获得资源的基本单位。

    进程的状态和状态的转换相关的知识点。那我们会介绍进程所拥有的各种各样的状态和它们在什么情况下需要转换。另外我们还会介绍进程的组织方式的问题,也就是各个进程的PCB之间要用什么样的方式把它们组织起来这样的一个问题。

    进程有哪些状态?在上个小节当中我们提到过,其实我们的程序,也就是可执行文件,平时是存放在硬盘里的。而当这个程序想要执行的时候,需要把这个可执行文件把它调入内存,同时操作系统会为它建立相应的PCB,也就是建立一个相应的进程。那当一个进程正在被创建的这个期间,这个进程的状态就是处于创建态,在这个阶段操作系统会给这个进程分配相应的系统资源。比如说给它分配一些内存空间,另外在这个阶段操作系统也会完成对PCB的一个初始化的工作。而当一个进程完成了创建工作之后,它就会进入一个新的状态叫做“就绪态”。处于就绪态的进程,其实是已经具备了运行的条件,只不过此时CPU比较忙,它还没有空闲,所以CPU暂时不能为这个进程服务。

    那一个系统当中可能会有很多很多个处于就绪态的进程。那么当CPU空闲的时候,操作系统就会从这些处于就绪态的进程当中,选择其中的一个让它上CPU运行。

    而如果一个进程此时正在CPU上运行的话,那么这个进程就处于运行态。那么经过之前的学习我们知道,一个进程它正在运行意味着此时CPU正在处理这个进程背后的那个程序,也就是CPU正在执行这个进程相应的那些指令序列。比如说CPU执行了进程2的指令1、指令2、指令3,那我们假设此时进程2的指令3是发出了一个系统调用。而这个系统调用是请求操作系统给它分配打印机资源,而此时打印机设备它很忙,它正在为别的进程服务,所以这个打印机资源暂时不能分配给进程2,所以这个进程2接下来的这个指令,也就是要往打印机输出数据的这条指令,这条指令就没办法往下执行。那既然这个进程接下来的这些指令暂时不能往下执行的话,那么显然我们不应该让这个进程一直占用着CPU资源,

    所以类似于刚才我们所说的这种情况,很多时候进程在运行的过程当中,有可能会请求等待某个事件的发生。比如说像我们刚才所说的,它会等待系统给它分配某一种系统资源,或者它需要等待其他进程的响应等等。总之这个进程在运行的过程当中,有可能会主动地请求等待某个事件的发生。而当这个事件发生之前,这个进程是没有办法继续往下执行的,所以在这个时候操作系统就会剥夺这个进程对CPU的使用权,

    让这个进程下CPU并且让它进入到一个新的状态叫做阻塞态,

    也就是说这个进程因为等待某个事件而被阻塞了。那当这个CPU再次空闲之后,

    操作系统又会选择移到处于就绪态的进程,让它上CPU运行。接下来的故事是这样的,这个打印机设备之前正在为别的进程服务,那如果说这个打印机的服务已经结束,那这个打印机就会空闲下来,所以当打印机空闲下来的时候,它就会分配给刚才请求打印机的那个进程,也就是进程2,所以当操作系统把这个打印机资源分配给进程2的时候,这个进程2等待的事件其实就已经发生了。此时这个操作系统会让这个进程2,

    从阻塞态再次回到就绪态,也就是说当它等待的这个事件发生了之后,这个进程就再次拥有了上处理机运行的条件。

    此时正在CPU上运行的这个进程1,它已经运行结束了。那在它运行结束的时候,它会发出一个叫做exit的系统调用。那这个系统调用其实就是要请求操作系统终止这个进程。

    那此时这个进程的状态就会变成终止态,

    然后操作系统会让这个进程下CPU,并且会做一系列的善后的工作。它会回收这个进程所占有的各种资源,包括什么内存空间啊,什么打印机设备啊等等,总之所有的资源都需要回收,并且最后它还会回收这个进程的PCB。

    而当终止进程的这些工作完成了之后,这个进程就从这个系统当中彻底消失了。

    用一个图把刚才所提到的这些进程的状态,和他们的转换再给串一下。一个进程在运行之前需要被创建,在创建的过程当中,系统会完成一系列相应的工作,包括新建PCB,还有给这个进程分配一系列的资源等等。那如果一个进程正在被创建的话,那这个进程此时就是处于创建态的。当一个进程被创建完毕之后,它就拥有了可以上处理机、上CPU运行的这种条件。那这个时候进程就进入了就绪态,也就是说处于就绪态的进程,它其实只差处理机这种资源,其他它所需要的资源它已经都具备了。那如果处于就绪态的一个进程被操作系统调度,那这个进程就可以上处理机运行。当它在处理机上运行的时候,它就处于运行态。也就是说,正在处理机上运行的进程,其实是既拥有了它所需要的其他所有的那些条件和资源,同时它也拥有了处理机这种资源。而有的时候正在运行的进程,可能会请求等待某些事件的发生。在这个事件发生之前,这个进程是没有办法继续往下执行的,所以在这种情况下,进程不应该一直占用着处理机资源,所以此时这个进程应该被剥夺处理机资源,同时除了处理机资源之外,它还在等待其他的某一种资源或者说等待其他的某一种事件的发生。那如果说处于阻塞态的进程,它等待的事件发生了,那这个进程就可以从阻塞态又回到就绪态。那当它回到就绪态,就说明这个进程拥有了除了处理机之外的所有的它想要的那些资源,所以从刚才的这个讲解过程当中我们会发现,运行态到阻塞态的这个转换其实是进程自身主动的一种选择,主动的行为。一般都是进程通过主动调用的方式,来申请某一种资源,或者请求等待某一个事件的发生,所以这个转换的过程是进程主动选择的。而阻塞态到就绪态的转变,它并不是进程自身能够控制的,比如说一个进程它正在等待打印机资源,那么这个资源什么时候分配给它,这个并不是这个进程能够说了算的,所以阻塞态到就绪态的转换,它是一种被动的行为,它并不是进程自己可以控制的,所以大家需要注意的是,一个进程不可能直接从阻塞态转换为运行态,也不可能直接从就绪态转换为阻塞态。因为进程要变成阻塞态那肯定是需要进程主动请求,而进程要发出这种主动请求那就意味着这个进程正在CPU上运行才可能发出这种主动的请求,所以只可能从运行态转换成阻塞态,而不可能从就绪态转换成阻塞态。那在之前的讲解中我们也提到过,处于运行态的进程它可以主动地请求运行结束,或者说一个进程它在运行的过程当中遇到了一些不可修复的错误,比如说整数除以零这样的错误,那在这种情况下,这个进程也应该被终止。那在操作系统对这个进程做相应的终止工作的时候,这个进程就处于终止态。此时操作系统会回收进程所拥有的各种资源,并且会撤销它的PCB。那最后我们还要强调一个刚才没有提到的状态的转换,有的时候进程可以直接从运行态转换成就绪态。比如说操作系统给进程分配的时间片用完的时候,进程就会从运行态转换成就绪态。什么叫时间片用完呢,还记得我们在第一章当中提到过时钟中断。一个进程本来正在处理机上运行的好好的,但是此时如果CPU收到一个时钟中断的信号,然后发现这个进程已经运行了很长时间了,不应该让它往下执行了。那这种情况就是所谓的时间片到,时间片用完的一个状态。此时这个进程就应该被剥夺CPU的使用权,让它从运行态又回到就绪态。因为在这种情况下,其实进程它还是拥有继续往下执行的条件。它只不过是被剥夺了处理机而已,它并不需要再等待,除了处理机之外的其他的事件发生。因此进程是从运行态回到了就绪态,那我们在这儿涉及到了进程的五个状态,这就是经典的进程五状态模型。去年也有同学把这个模型称为丁字裤模型,也确实很形象。

    那在这五种状态当中,运行态、就绪态、阻塞态是基本状态,因为进程的整个生命周期当中,其实大部分的时间它是处于这三种状态的,所以它们是基本状态。另外这儿需要强调一点,在单CPU的环境下处于运行态的进程同一时刻只会有一个,而如果系统是多核CPU的话,那么就意味着多个进程可以并行地运行。那在这种情况下就会有多个进程处于运行态。另外一点需要强调的是,阻塞态又称为等待态,创建态又可以叫新建态,终止态又可以叫结束态,所以它们的这些别名大家也稍微注意一下。那操作系统是怎么记录进程的这些状态的呢,其实在上个小节当中我们有提到过,在进程的PCB当中,会有一个变量state。这个state就是用来表示进程的当前状态。比如说我们可以用1表示它处于创建态,然后2表示它处于就绪态等等。另外呢操作系统会把处于同一个状态的各个进程把它们的PCB有规律的组织起来,然后这样的话可以方便统一的管理。所以怎么把各个进程的PCB组织起来这个问题就是

    进程的组织想要探讨的问题。那进程的组织方式有这样的两种,一种是链式方式,一种叫索引方式,其实都很好理解。链式方式就是指操作系统会管理一系列的队列,那每个队列都会指向相应状态的这些进程的PCB。比如说有个执行指针,它指向的这个PCB就是此时处于运行态的进程的PCB。而就绪队列指针它所指向的这个队列,就是此时系统当中处于就绪态的这一些的进程PCB队列。那为了方便对这些就绪进程的调度,操作系统经常会把优先级更高的那些进程的PCB放在这个队列的队头。那阻塞队列指针也一样,它指向的就是此时处于阻塞状态的那些进程的PCB。而在很多操作系统当中,它还会根据阻塞原因的不同再把阻塞队列分成多个,

    比如说像这个样子。这两个进程它们是因为正在等待打印机资源而阻塞的,所以它们会放在等待打印机的这个阻塞队列当中。而这个进程它是正在等待磁盘这种资源,所以它放在了等待磁盘的这个阻塞队列当中。总之操作系统用这样的方式把各个状态的进程PCB把它们有规律的组织起来,这样可以方便操作系统对这些进程进行统一的管理。

    那除了链式方式之外,另一种组织方式叫做索引方式。操作系统会给各种状态的进程建立相应的索引表,然后每一个索引表的表项又会指向相应的一个PCB,那索引方式通过这张图就很容易理解了。但是大多数的操作系统都是使用的链式方式,所以这个地方有个简要的了解就可以了。

    那进程的组织方式这一块的内容大家只要稍微有个印象就可以。总之它回答的就是操作系统应该怎么把各个进程的PCB把它们组织起来这样的一个问题。

    那这个小节当中更应该注意的是进程的状态还有进程状态之间的转换这样的问题,绿框部分是考研当中最喜欢考的,最高频的考点部分。那希望大家能够主动地去通过课后习题再对这一部分的内容进行巩固和理解。

    比如说创建一个新进程就是让一个进程从无到有,到创建态,再到就绪态,这是创建新进程所需要干的事情。那撤销一个已有进程不就是让进程进入终止态,最终把这个进程干掉的一个过程。所以其实所谓的进程控制,就是要实现这些进程的状态转换,那么在进程的状态转换的时候,操作系统需要做一些什么事情呢?这就是这个小节当中我们要讨论的内容。

    刚才我们简要地了解了什么是进程控制,接下来我们会介绍怎么实现进程控制。需要用原语来实现。之后我们会介绍几个进程控制需要的原语,它们分别需要实现哪些功能。

    那接下来我们要探讨的问题是怎么实现进程控制。刚才提到进程控制也就是进程的状态转换相应的处理,是需要用原语来实现的。而原语这个概念我们在第一章当中提到过。操作系统的内核中,有一些特殊的程序,它叫原语。原语这种程序,它的执行是具有原子性的。也就是说,这个程序运行是必须一气呵成的,中间不可以被中断。

    也就是说实现进程的状态转换这个事情,中间的一系列操作必须一气呵成。为什么进程控制或者说进程的状态转换这个过程需要“一气呵成”呢?我们结合上一小节当中学习到的知识,我们知道在PCB当中会有一个变量用来表示进程当前所处的状态。比如说这个State变量,当它为1的时候,我们认为它是在就绪态,当它为2的时候在阻塞态。那么如果一个进程它处于就绪态,State=1的话,那这个进程的PCB肯定是需要挂在就绪队列里的。而如果State等于2的话,那么这个进程的PCB就应该被挂在阻塞队列里。那么我们接下来考虑这样一个事情,我们知道处于阻塞队列的这些进程,它肯定是在等待某一种事件的发生。那假设现在这个进程2也就是PCB2所对应的那个进程,它所等待的事件已经发生了。那么在这种情况下,这个进程是不是应该从阻塞态转换为就绪态呢?所以操作系统当中的内核程序就需要把这个进程的状态从阻塞态变为就绪态。那进行状态转换的这个过程它至少需要做这样的两件事情,第一件事要把PCB当中State这个变量从2变为1,第二件事它还需要把这些PCB2从阻塞队列当中摘出去然后挂到就绪队列当中。所以操作系统在让这个进程的状态发生转换的过程当中,它至少需要干这样的两件事。

    那么接下来,我们来考虑这样一个事情,假设现在State1已经被它设为了1了,而在完成了这一步之后,突然又检测到了一个中断信号。那么既然检查到了中断信号,那系统肯定需要对这个中断进行处理。而在这个时候PCB当中State=1,也就是说从State这个变量来看,这个进程它的状态是就绪态。但是从它所处的队列来看,这个PCB2此时又还是在这个阻塞队列当中,所以这就导致了PCB2当中的这个变量所表示的状态和PCB2它所处的这个队列这两个信息对不上了。所以说如果说操作系统让这个进程的状态转换的这个中间处理的步骤不能一气呵成的话,就有可能会出现我们在这个地方所看到的这种某些关键的数据结构信息不统一的情况。那这些数据结构是非常重要的,这有可能会影响到操作系统进行后续的别的一些工作,可能会让系统出错。所以这就是为什么进程的状态转换或者说进程控制的过程需要“一气呵成”了。那刚好原语这种特殊的内核程序,它具有“一气呵成”不可被中断的这种美好的性质,所以我们可以用原语这种特殊的程序来实现“一气呵成”这样的事情。

    为什么原语这种特殊的程序它可以“一气呵成”不可被中断呢?其实它的这种原子性是用两个特殊指令,“关中断”和“开中断”这两个指令来实现的。那假设这是一个正在运行的内核程序,那CPU会依次执行这些指令。

    并且根据第一章的讲解我们知道,CPU每执行完一条指令之后,它都会例行地检查是否有中断信号需要处理。那如果说它在执行了指令2之后,CPU发现此时有一个中断信号,

    那在这种情况下CPU就会暂停执行当前的这个程序,转而执行一个处理中断的程序。那等这个中断处理完成之后,它才会再回到原来这个程序继续往下执行。那这是我们之前认识到的情况,就是CPU每执行完一条指令它都会检查是否有外部中断信号需要处理。

    如果执行了关中断指令会发生什么情况?假设此时CPU正在依次地执行这些指令。

    然后当它执行了关中断这条特权指令之后,CPU就不再例行检查中断信号了。

    所以接下来CPU会继续往下执行,那如果说此时它执行了指令a的这个过程当中,有一个外部中断信号到来了。但是此时它并不会像之前一样例行地检查是否有中断信号,

    而是会继续往下处理,

    一直到CPU执行了开中断指令之后,它才会恢复以前的那种习惯,也就是每执行完一条指令,那就会检查一下此时是否有外部中断信号需要处理,所以当它执行了开中断指令之后,它就会发现之前有一个中断信号我还没有处理,所以这个时候CPU才会转向执行中断处理程序,

    所以从刚才这个例子当中我们就可以看到,在关中断和开中断这两条指令中间的这一系列的指令序列它们的执行肯定是不可被中断的。这样的话就实现了我们开篇提到的所谓的原子性,这段指令序列的执行肯定是一气呵成的,它中间不可能再被中断,所以这是关中断指令和开中断指令的一个特殊的作用。那显然这两个指令它们肯定是特权指令,如果这两个特权指令允许普通的用户程序使用的话,会发生什么情况呢?那是不是就意味着我可以在我的程序开头,就植入一个关中断指令,然后一直到我的程序末尾才再执行开中断指令。这样的话只要我的程序上CPU运行了,那我的程序肯定会一直霸占着CPU而不会被中断,那显然这种情况是不应该让它发生的。所以关中断指令和开中断指令它们是特权指令,只能让内核程序使用,而不能让普通的用户程序使用。好的,那么到目前为止我们知道了两件事情,第一进程控制或者说进程的状态转换这个事情必须一气呵成,而想要做到一气呵成我们可以用原语这种特殊的程序来实现。而原语的实现需要由开中断指令和关中断指令来配合着完成。

    进程控制相关的这些原语或者说相关的这些特殊的程序它们在背后需要完成一些什么事情呢?首先来看第一个原语,这个原语是用于实现进程的创建的。如果操作系统要创建一个进程,那么它就必须使用创建原语。那么这个创建原语它会干这样的几件事情,首先是要申请一个空白的PCB,因为PCB是进程存在的唯一标志,所以要创建一个进程当然是需要创建一个和它相对应的PCB。另外还会给这个进程分配它所需要的资源,比如说像内存空间等等。然后还会把这个PCB的内容进行一些初始化的工作,比如说分配PID,设置UID等等。最后它还会把这个PCB插入到就绪队列,所以说创建原语让一个进程从创建态进入到了就绪态,把它放到了就绪队列里。那有一些典型的事件会引起操作系统使用创建原语创建一个进程,比如说当一个用户登录的时候,操作系统会给这个个用户建立一个与它对应的用户管理进程或者用户通信进程等等。或者发生作业调度的时候,也会创建一个进程。根据去年的反馈,很多同学不知道作业到底是什么,其实作业就是此时还放在外存里的那些还没有投入运行的程序,所以所谓的作业调度就是指从外存当中挑选一个程序让它把它放入内存让它开始运行。那我们知道当一个程序要开始运行的时候,肯定是需要创建和它相对应的进程的,所以当发生作业调度的时候就需要使用到这个创建原语。另外有的时候一个进程可能向操作系统提出某些请求,然后操作系统会专门建立一个进程来处理这个请求。还有的时候一个进程也可以主动地请求创建一个子进程,总之发生这些事件的时候都会引起系统创建一个新的进程,也就是说它会使用到这个创建原语。

    那接下来要看的是撤销原语。撤销原语是要终止一个进程的时候使用的,使用了撤销原语之后,就可以让一个进程从某一种状态转向终止态最终这个进程从系统中彻底消失。那撤销原语需要做这样的一些事情,首先既然要撤销一个进程那肯定需要找到这个进程相应的PCB,那如果说这个进程此时正在运行的话,那就需要立即剥夺它的CPU使用权,然后把CPU分配给其他进程。同时操作系统在杀死一个进程的时候还会杀死所有它的子进程。并且这个进程被撤销之后,它之前所占有的那些资源应该归还给它的父进程,最后的最后还需要把这个进程的PCB从系统中删除,那这样的话这个进程就彻底的完蛋了。有的同学可能不理解子进程、父进程这样的概念。我们来看一个很实际的例子,这个界面中的这种显示方式,其实就反映了这些进程之间的父子关系。可以看到这儿有一个PID为0的进程,它是最祖先的一个祖先进程。然后这个祖先进程它创建了一个PID为1的进程,这个进程叫做launchd,其实它就是我们在第三章会学习到的所谓的装入程序或者叫装入进程,在开机了之后我们启动的所有的别的那些进程,其实都是这个装入进程来启动的,所以别的那些进程都是这个装入进程的子进程。那除了它之外,其他的那些进程也可以创建自己的子进程

    来完成相应的一系列工作,比如说访达这个进程它就创建了这几个它自己的子进程。

    那这样的设计方法有什么优点呢?我们可以想一下,刚开始系统中几乎所有的资源都是这个进程launchd所拥有的。比如说我的电脑里8GB的内存,全部是launchd它所拥有的。那之后当它在建立自己的这些子进程的时候,它可以按照这些子进程的需要,把自己手里的那些资源再分配给它手底下的这些进程。比如说它本来手里有8GB的内存,那它把其中的50MB分配给了这个进程,把54.5MB分配给了这个进程。而当它的这些子进程终止了之后,那当然是需要把自己手里的这些资源还给它们的父进程,也就是上面的这个进程。

    所以回到刚才我们提到的这个地方,相信大家就更容易理解了。其实我们的操作系统当中的各个进程它们之间的关系是一种树型的结构,系统中的0号进程和1号进程是最祖先的两个进程,然后这两个进程又依次创建了它们的子进程,各个进程又可以再创建各自的子进程,所以这些进程之间的关系其实是一种树型的结构,不过这个并不是我们操作系统这一门课要考察的重点,只是为了让大家能够更深入地理解这儿提到的这两个特性。很多事件有可能会引起一个进程的终止,比如说一个进程自己请求终止,也就是它使用了exit这个系统调用。那这种情况下操作系统在背后就会需要使用到这个撤销原语来把这个进程撤销掉。另外如果一个进程做了一些非法的事件,比如说整数除以0或者非法使用特权指令,这种情况也会被操作系统强行地撤销,强行把它干掉。还有一种情况,有时候是用户会选择杀掉一个进程。比如说我们在使用Windows电脑的时候,经常出现卡死的情况。那这种情况下很多同学喜欢用这样的方式打开任务管理器然后结束掉某一个卡死的进程,那这种就是外界干预的情况。

    那接下来我们要看的是阻塞原语和唤醒原语。这个小节的内容确实比较多,可能很多同学听到这儿就比较疲惫了。但是我们这些原语它其实要干什么,这些事情我们并不需要死记硬背,我们只需要理解它背后的过程就可以,考试的时候不可能让你默写这些东西的,所以我们着重以理解为主,大家不需要刻意地记忆。有的时候一个进程可能会从运行态进入到阻塞态,那在这种情况下操作系统就会在背后执行一个阻塞原语来实现这个状态的转换。阻塞一个进程需要做的事情比较简单,首先是找到这个进程对应的PCB,然后需要保护进程运行的现场。什么叫保护进程运行的现场这个我们一会儿再解释,这又是一个比较庞大的话题。另外呢系统需要把PCB当中的状态信息设置为“阻塞态”,然后让这个进程下处理机,并且把它插入到等待队列当中。那经过上个小节的学习我们知道,一个进程会被阻塞那肯定是因为它主动请求要等待某一个事件的发生,而如果这个进程它所等待的事件发生了之后,这个进程就会被唤醒,也就是说操作系统会让这个进程的状态从阻塞态又回到就绪态,那在这个时候又会使用到唤醒原语。唤醒原语需要做这样的几个事情,首先要找到它的PCB,然后把它的PCB从等待队列当中移除,然后把它设置为就绪态,并且把PCB插入到就绪队列当中,等待被调度。这两个原语做的这些事情相信都不难理解,那需要注意的是,一个进程因为什么事情被阻塞,就应该被什么事情给唤醒,所以唤醒原语和阻塞原语它们必须是成对使用的。

    那接下来我们再来认识最后一个原语,叫做切换原语。切换原语会让此时正在处于运行态的进程下处理机,让它回到就绪队列,并且从就绪队列当中选择一个处于就绪态的进程让它上处理机运行,所以切换原语会让两个进程的状态发生改变。那切换原语需要做这样的一些事情,首先是需要把进程的运行环境信息存到PCB当中。什么叫进程的运行环境信息呢?这点涉及到一些硬件的知识,我们一会儿再展开细聊。另外它还会把进程的PCB移到相应的队列,比如说让这个下处理机的进程的PCB回到就绪队列当中。另外它还会挑选一个进程,让它上处理机运行,并且更新它的PCB的内容。同时它还会从这个进程的PCB当中恢复这个进程所需要的运行环境,那什么叫保存运行环境什么叫恢复运行环境这是比较难理解的地方。接下来我们得深入探讨一下这个问题。

    那在之前的学习中我们认识到了一个程序的运行需要经历这样一系列的流程,程序运行之前需要把它相应的这些指令把它放入到内存当中,然后CPU从内存中读取这些一条一条的指令并且执行。但是接下来我们要拓展一个更深层的细节,CPU在执行这些指令的过程中,需要进行一系列的运算。那么CPU当中会设置很多的寄存器来存放这些指令这些程序在运行过程当中所需要的某些数据。总之寄存器就是CPU里边用于存放数据的一些地方。

    那CPU当中会有各种各样的寄存器,比如说我们之前提到过PSW,就是程序状态字寄存器。CPU的状态,内核态还是用户态,这个状态信息就是保存在PSW这个寄存器当中的。当然除了CPU状态信息之外,PSW中还会保存别的一些信息。那这儿我们就不展开,这是计算机组成原理里边需要学习的地方。

    另外CPU中还会有一个比较关键的寄存器叫做PC,也就是程序计数器、寄存器。这个寄存器里边存放的是接下来需要执行的指令它的地址是多少,那这一点我们一会结合实例就很好理解了。

    另外CPU当中还会有一个指令寄存器,这个寄存器当中存放的是当前CPU正在执行的那条指令。

    还有呢,CPU中还会有一些其他的通用的寄存器,可以用来存放一些别的必要的信息等等等等。总之CPU当中会有一系列的寄存器,我们这儿只列举了几个操作系统这门课当中大家需要稍微地了解一下的寄存器。那接下来我们来分析一下,这样的一些指令序列的执行在背后发生了什么样的事情。这四条指令所完成的事情就是定义了一个叫做x的变量,并且实现了x++的操作。那假设此时CPU正在执行的是指令1,那么它会把指令1的内容读到IR,也就是指令寄存器当中,并且程序计数器这个寄存器当中,会存放接下来它应该执行的那条指令,也就是指令2的地址。那此时CPU执行指令1,它发现指令1是让它往内存的某一个地方写入一个变量为x的值。那CPU执行指令1的时候,就会往内存的某一个地方写入变量x的这个值,也就是1。那执行完指令1之后,CPU就会开始执行下一条指令。而从PC这个寄存器当中它就知道下一条要执行的指令应该是指令2,所以接下来它会取出指令2,把指令2的内容放在IR也就是指令寄存器当中,

    同时PC的内容也更新为再下一条指令。

    那指令2是让CPU把变量x的值把它放到某一个通用寄存器当中,所以CPU会从内存中取出这个x变量的值,把它放到通用寄存器当中。于是这个通用寄存器的内容就变成了1,那这样的话就执行完了指令2。

    那再接下来CPU又要执行再下一条指令,所以它会取出指令3,

    然后PC的内容同样的也会更新。那指令3是让它把寄存器当中的这个数据进行加1的操作,

    所以这个通用寄存器中的值就会从1变成2。

    再接下来CPU又会执行再下一条指令,那么此时执行的这条指令指令4,是让它把这个通用寄存器当中的内容把它写回到变量x所存放的这个位置当中,所以执行了指令4之后,就会把内存当中x的值从1变成了2,所以可以看到我们执行x++的操作,其实在背后CPU是执行了一系列的更基本的那些指令才完成了这个事情。并且从刚才讲的这个过程当中我们会发现,这些指令顺序执行的过程当中,有很多中间结果是放在这些寄存器当中的。比如说x++这个操作,刚开始其实只是把它放在了通用寄存器里,而并没有写回内存。但是需要注意的是这些寄存器并不是这个进程所独属的,如果其他进程上CPU运行的话,那么这些寄存器也会被其他进程所使用。

    那这会发生什么情况呢?我们再把这个故事从头捋一遍。现在这个CPU它要依次地执行这些指令,那刚开始执行指令1、

    指令2、

    指令3。当它执行了指令3之后,寄存器里的这个值变成了2。而此时如果说它要转向执行另一个进程会发生什么情况呢?刚才我们说道如果另一个进程上CPU运行的话,那么另一个进程也会使用到这些寄存器,所以另一个进程在上CPU运行的时候有可能会把前一个进程在寄存器当中保留的这些中间结果给覆盖掉。

    比如说它覆盖成了这个鬼样子,那我们之前的这个进程不是执行到了指令3吗?因为它的前三条指令执行的中间结果都已经被覆盖了,所以这个进程也就没有办法再往下执行了,

    所以为了解决这个问题,可以采取这样的策略。当一个进程它要下处理机的时候,可以把它之前运行的这个运行环境的信息把它保存在自己的PCB当中。当然这个PCB当中并不需要把所有的寄存器信息都要保存下来,只需要保存一些必要的信息就可以了,比如说PSW、PC、还有这个通用寄存器。那这个进程执行了前三条指令之后,它的运行环境是这个样子的,我们把它放到了PCB当中,

    接下来才可以切换成别的进程。

    那接下来别的进程在使用CPU的时候可能会往这个寄存器当中写各种各样的数据,总之之前那个进程的数据有可能会被覆盖。

    但是当之前的这个进程需要重新回到CPU运行的时候,操作系统就可以根据之前保存下来的这些信息来恢复它的运行环境了。

    那把它的运行环境恢复之后,CPU就知道接下来它要执行的是指令4,

    并且此时通用寄存器当中存放的数值是2,所以既然接下来要执行的是指令4,

    那CPU就会根据PC的这个指向把指令4的内容取到IR这个寄存器当中,然后让PC指向下一条指令。同时CPU开始解析这条指令到底是要干什么,它发现指令4是让它把寄存器当中的内容写回到x存放的位置,所以接下来它就会把2这个内容写回到x的这个位置,于是x++这个操作就真正地被完成了。

    总之这个地方讲了那么多的内容,想让大家知道的就是什么叫做进程的运行环境。其实所谓的运行环境,或者说进程上下文,它就是进程在运行过程当中,寄存器里存储的那些中间结果。当一个进程需要下处理机的时候,需要把它的这个运行环境,把它存到自己的PCB当中。而当一个进程需要重新回到CPU运行的时候,就可以从PCB当中恢复它之前的这个运行环境,让它继续往下执行了,所以保存进程的运行环境和恢复进程的运行环境这是实现进程并发执行的一个很关键的一个技术。

    那这个小节的干货比较多,我们讲了一些涉及底层硬件的一些知识。可能学过计组的同学觉得这些其实都很好理解,但是我们又不得不照顾到一些跨考的同学和一些不考计组的同学。那在操作系统这门课当中,硬件相关的知识我们不需要去深究,只是为了让大家理解其中的某一些很关键的操作系统概念,又不得不提一些硬件的知识,毕竟操作系统是最接近硬件的一层软件了,所以中间扯这么多也是为了让大家能够更好更深入地理解。那大家需要注意一下原语这个概念,它使用关中断和开中断来实现,它的执行必须一气呵成不可中断。之后我们介绍了一些进程控制相关的原语,各个原语它中间做了各自的事情。但是这些都不需要死记硬背,其实无论是哪一个控制原语,它所要做的无非就是这么三件事,第一就是更新PCB当中的一些信息,第二是把PCB插入到合适的队列,第三向进程创建和进程终止的时候有可能还需要分配和回收这个进程的资源。那更新PCB的信息主要是修改这个进程的状态,也就是state那个变量,或者就是往PCB当中保存进程的运行环境或者从PCB恢复进行的运行环境。总之我们了解了这些控制原语背后做的事情,能够帮助我们更好地理解进程管理、处理机管理这一系列的知识。但是这些内容确实不需要死记硬背,所以虽然这个小节看起来内容很多,但是更多的都是理解性的东西,大家不需要花时间去记忆,希望大家不要惊慌。

    进程通信相关的知识点。进程通信可以用共享存储、消息传递和管道通信这样三个大类的方式来进行实现。大家在学习的过程中要注意体会各种实现方式背后,进程通信的一个流程大概是什么样子。

    首先我们来看一下什么是进程通信。其实从字面上的意思也可以理解,进程通信指的就是进程之间的信息交换、信息传递。那么我们知道进程它是分配系统资源包括内存地址空间这样的系统资源的基本单位。各个进程拥有各自不同的内存地址空间,它们的地址空间是相互独立的。比如说进程1它可以访问它自己的地址空间,进程2也可以访问自己的地址空间。但是进程1并不能直接访问进程2的地址空间,因为这个地址空间它是操作系统分配给进程2的一个系统资源,这个资源是属于进程2的,不能被进程1所共享。为了保证这个系统的安全,一个进程是不允许直接访问其他进程的地址空间的。可以想一下,如果进程1可以随意地访问进程2的地址空间的话,那么就意味着进程1可以修改进程2在内存当中存放的数据。那这样显而易见,是会对系统造成很大的安全隐患了。但是有的时候,进程之间的信息交换又是必须实现的。但是为了保证这个信息交换的安全性,操作系统就会提供一些方法来供各个进程使用。操作系统提供的进程通信方式,分为共享存储、消息传递和管道通信这样三种。

    首先我们来看一下共享存储。两个进程它们不能直接访问对方的地址空间,所以操作系统会为两个进程分配一个共享空间。然后两个进程之间的通信就可以通过这个共享空间来进行完成。需要注意的是,这两个进程对共享空间的访问,必须是互斥的。当进程1正在往这个共享空间当中写数据的时候,进程2是不允许访问这个共享空间的,只有进程1把这个共享空间的访问给释放了,那么进程2才可以开始读操作。而这个互斥一般来说是使用操作系统提供的同步互斥工具来实现的。比如说之后咱们会学到的PV操作,这儿只需要有一个印象就可以,之后会有更深入的理解。那么共享存储或分为两种,第一种是基于数据结构的共享,第二种是基于存储区的共享。前者就比如说这个共享空间当中,只能存放一种固定的数据结构。比如说只能往里边存长度为10的数组,那么两个进程之间的通信每一次就只能传一个长度为10的数组。所以这种通信方式会看到会极大地限制这种通信的速度,所以速度会很慢。并且对于数据的格式是有限制的,因此基于数据结构的这种共享它是一种低级的通信方式。而另外一种基于存储区的共享,操作系统只负责为通信的进程提供一个共享空间,但是在这个共享的存储空间当中,两个进程相互交换的数据是什么形式,存放在什么位置,这些其实都是由两个进程来相互决定的,而不是由操作系统来决定。所以相比于前面这种来说,基于存储区的共享,它相对来说要更自由一些,并且它的数据传输速度会更快。因此基于存储区的共享,它是一种高级的通信方式。那么这就是通过共享存储实现的进程通信。

    第二种是管道通信,所谓的管道其实就是一个特殊的共享文件。那么管道其实就是在内存当中开辟的一个固定大小的缓冲区,这个缓冲区的大小一般和内存页面是一样的。像Linux系统当中,一个管道对应的这个缓冲区的大小就是4KB。那么在管道通信当中,大家需要注意的是,一个管道只能实现半双工的通信。所谓的半双工就是指,在某一个时间段只能实现单向的传输。半双工的概念大家会在计算机网络里再进一步的学习。其实就是指,数据是可以这样传输,也可以这样传输。但是在同一时间段内,只能往一个方向传输。这就是半双工通信的意思。那么如果这个用管道通信的方式,想要实现双向同时通信的话,就需要设置两个管道。一个管道负责由左到右,另外一个管道负责另外一个方向的这个数据的传输。而我们需要注意的是,各个进程对管道的访问其实也是需要互斥地进行的,和刚才咱们提到的共享空间的访问一样,当进程1正在往里边写的时候,进程2是不允许访问管道的。只有进程1把管道的访问给释放了,那么进程2才可以开始读。那么我们来看一下,管道通信的这个数据传输是怎么实现的。

    首先进程1会往管道当中写数据。当这个管道中的数据写满了之后,进程2才可以开始往外读数据。

    而只有这个数据全部被读出之后,

    进程1才可以继续往里边写数据。

    那么在管道通信当中就有这样的一些特点,数据,这些数据,是以字符流的形式写入管道的。并且当管道写满的时候,这个进程就不可以继续往里边写数据了。写进程的write()系统调用会被阻塞,需要等待读进程把这些数据读出之后,写进程才可以继续往里边写。而如果说这个读进程把数据全部取走之后,管道为空,那么读进程的read()的系统调用又会被阻塞,需要等待写进程往里边写数据。而且大家需要注意的是,如果没有写满的话,是不允许读的。同样,如果没有读空的话,是允许写的。另外管道中的数据一旦被读出之后,就会被抛弃,那么这个数据就再也找不回来,所以这就意味着在一个管道的读进程最多只能有一个,如果有多个读进程的话,就有可能会发生读错数据的情况,比如说这些数据本来是应该由进程2读出的。但是如果有多个读进程的话,那么这些数据有可能会被另外一个进程给取走。而这些数据一旦被取走之后,就再也找不回来,就被抛弃了,所以读进程最多只有一个,那么这就是管道通信。

    接下来我们再来看第三种消息传递。进程之间会以格式化消息的方式来传递,消息传递的过程是用发送和接收两个原语来实现的。那么一个格式化的消息会分为消息头和消息体两个部分,消息头当中会包含发送进程的ID、接收进程的ID,还有什么消息类型、消息长度等等各种各样的一系列的格式化的信息。像咱们计算机网络当中,发送的报文其实就是一种格式化的消息。那么消息传递方式又分为两种,一种是直接通信方式,另外一种是间接通信方式。直接通信方式其实就是把消息直接挂到接收进程的消息缓冲队列上,每一个进程会有一个消息缓冲队列,然后如果有另外一个进程想给它发送消息的时候,这个进程会首先创建好这个消息体,格式化的消息体,然后这个会通过发送原语发送给目标进程,这个消息就会被挂到目标进程的消息缓冲队列的队尾,然后这个进程又会通过接收原语来依次把这些队列当中的这些消息一个一个取走,然后进行处理。这就是直接通信方式。

    那么间接通信方式其实也比较类似,只不过这些消息是需要先发送到一个中间实体,这个中间实体又可以称之为“信箱”,所以间接通信方式也可以称之为“信箱通信方式”。比如说像计算机网络里之后会学到的电子邮件系统就是一种间接通信方式,一种“信箱通信方式”。系统会为各个通信的进程管理一个信箱,而这个信箱当中可能会有各种各样的消息,并且这些消息可能是不同的进程之间通信的一些消息。那具体是由哪个进程发,由哪个进程接收,这些都是在消息头里会包含的一些数据,所以并不需要担心这些消息会被取错。那么如果一个进程想要给另外一个进程发送消息的话,

    这个消息会用发送原语先发送到这个中间实体——信箱当中,

    之后读进程会用接收原语再从信箱当中取走属于自己的消息,所以这就是消息传递的两种方式。

    大家需要理解各种通信方式的背后的一个通信的流程。那么共享存储当中需要保证各个进程是要互斥地访问这个共享空间。而管道通信当中需要注意的是一个管道只能实现半双工通信,在同一时间段内只能实现单向的数据传输。而管道通信还有两个比较容易作为选择题考察的限制,在管道写满的时候就不能再写,在管道被读空的时候就不能再读。那么管道如果没有被写满的话是不允许从管道中读取数据的。相应的,没有被读空的话也不能往管道里写数据。之后我们又介绍了消息传递的两种方式,一种是直接通信方式和间接通信方式,它们都是用发送、接收原语来实现的。二者的区别在于间接通信方式是需要先把消息发送到一个中间体,一个信箱当中。

    一个新的概念叫线程。

    线程和进程之间是有很紧密的联系的。什么是线程?为什么有必要引入线程?之后会把线程机制和之前我们认识过的进程机制进行一些比较,然后用这种方式让大家体会到线程的一些特点优点。

    什么是线程?可以用QQ进行视频聊天、文字聊天以及传送文件,那值得注意的是,在我们用户看来,这几个事情似乎是可以同时发生的。但是我们学过操作系统我们知道,这几个事情其实在背后程序的处理逻辑可能是并发地运行的。不过不难想象,处理视频聊天、处理文字聊天和处理传送文件的程序肯定不会是同一段程序代码。也就是说在我们的电脑当中,至少处理这三件事的三个程序,是需要并发地运行的。要让这三个程序并发地运行,那么一个比较简单的处理办法就是这三个程序分别给它们建立一个各自的进程,比如说进程1它是用来处理视频聊天的,而进程2是用来处理文字聊天的,而进程3是用来处理文件传输的。

    也就是说用这种方案的话,我们的QQ其实它背后可能会有三个它所对应的进程。这些代码各自实现了不同的功能,那根据之前我们对进程的认知我们知道,每一个进程会有各自的一个PCB,并且操作系统会给各个进程分配它们各自的内存地址空间,它们的地址空间是不一样的。另外呢,可能像视频聊天的这个进程可能需要给它分配摄像头这种I/O设备。然后文字聊天这个进程,可能需要给它分配键盘这个I/O设备。还有文件传输这个进程,可能需要给它分配磁盘这个设备。那除了这儿我们列举的这些我们比较熟悉的系统资源之外,操作系统还会给它们各自分配一些其他的系统资源。但是由于进程是资源分配的基本单位,所以说操作系统给进程1分配的这些资源,进程2是不能使用的。同样的,给进程2分配的这些资源,另外两个进程也是不能使用的。那为了让用户能够一边视频一边文字聊天还一边文件传输,所以这三个进程需要并发地运行。也就是说处理机CPU会轮流地给各个进程服务,轮流地执行各个进程的代码,所以这是我们之前学习过的进程机制。在这种情况下,进程是处理机调度的基本单位。当操作系统调度一个进程的时候,其实就是让这个进程所对应的这些代码上处理机运行。这几个进程它们一起在系统当中并发地运行,但是有的时候它们又不得不使用到对方手里的一些资源。比如说我们假设进程2有的时候也许会使用到进程1手里的这个摄像头资源。那如果它要使用的话,

    操作系统要做的事情是它需要先把这个摄像头资源给回收,

    然后再给分配给进程2。而接下来如果进程1又要使用这个摄像头资源,那操作系统还需要把摄像头回收,

    然后再还给进程1。所以其实这样的资源回收分配、回收分配这个过程也是需要很大的系统开销的,那这显然不太科学。那大家先把这个问题留下,大家之后思考一下线程这种方案能不能解决这个问题。

    那现在我们来深入思考一下,其实这三个进程它们本身都是为了完成QQ的某一部分功能,但是功能和功能之间又必须并发地运行,因此我们不得不把它们拆分成三个不同的进程来分别实现这些功能。但是我们在上一小节当中学到过,其实进程之间的切换是需要一定的时间开销、时间成本的。比如说在切换进程的时候我们需要保存和恢复进程的运行环境,这个在上一小节当中具体讲过。同时还需要切换进程的内存地址空间,因为它们的内存地址空间是相互独立的,等大家学习了第三章内存管理的知识之后,大家会知道其实切换这个内存地址空间,开销是非常大的。切换一个进程意味着操作系统需要同时更新快表、需要更新缓存。那这些信息大家现在理解不了没有关系,总之我们需要知道的就是由于各个进程它们的内存地址空间这个资源是相互独立的、不共享的。因此在进程切换的时候,切换内存地址空间需要非常大的时间开销。那么有没有可能让这些程序并发运行的这个系统开销能降低一些呢?

    那为了解决这个问题人们就提出了线程这种机制。在引入了线程机制之后,一个进程里边可以包含多个线程,而不同的线程之间它们可以执行不同的代码序列。比如说线程1的这些代码,是实现了视频聊天的功能,线程2是实现文字聊天,线程3是实现文件传输。在引入了线程之后,CPU的调度单位就变成了以线程为单位。也就是说,CPU会轮流地给各个线程服务。那样的话,虽然这些线程从属于同一个进程,然而它们之间也是可以并发运行的。不过呢,虽然引入了线程,但是操作系统在对系统资源进行分配的时候,依然是以进程为分配单位的,所以如果QQ进程它需要键盘、需要磁盘、需要摄像头,那么这些资源都是分配给QQ这个进程的。并且从属于同一个进程的各个线程,它们可以共享地使用它们的这个进程所拥有的资源。那这个特性可以给整个系统带来很多好处。

    比如说刚才我们提到,各个进程之间它们的资源是不共享的,包括内存地址空间,所以进程之间的切换所要付出的代价是比较大的。但是如果我们引入了线程机制的话,那么从属于同一个进程的各个线程之间它们的并发运行、它们之间的切换就不再需要像刚才我们所说的,再切换什么内存地址空间,切换进程的运行环境,这些工作就都不需要做了。因为这两个线程其实共享的是同一个内存地址空间,所以如果是这两个线程之间的切换,那就不需要再更新快表,因此我们可以看到进程之间的并发其实开销是比较大的,但是线程之间的并发开销会比较小,所以在引入了线程之后,并发所带来的系统开销会降低,而系统的并发性就可以得到提升,这个应该不难理解。不过大家需要注意的是,如果是从属于不同进程的两个线程之间的切换也会导致进程的切换,因为不同进程下的线程,它们也不共享内存地址空间,所以依然会存在刚才我们所说的切换的开销大的问题。但是同一个进程间的各个线程之间的切换它们的开销就很小很小。

    再来看第二个特点。在之前小节的学习当中我们知道,各个进程之间它们不可以直接访问对方的内存地址空间。比如说进程1不可以访问这一片内存,而进程2又不可以访问这一片内存,所以如果两个进程之间需要交换数据、需要通信的话,那么它们就必须请求操作系统的服务,由操作系统来帮助它们完成进程间的通信。但是如果我们引入了线程机制的话,我们知道这两个线程它们可以共享地访问这一片内存空间,因此如果它们之间想要通信的话,其实直接往这一片内存空间当中读和写就可以了,因此从属于同一个进程的各个线程之间的通信就不需要操作系统的干涉。那这个特性其实也会带来性能上的提升,因为在请求操作系统服务的时候,需要发出系统调用。而系统调用就意味着CPU的状态会从用户态变成核心态,而我们在第一章的学习当中知道CPU的状态转换这个事情其实开销也是比较大的。而进程之间的通信必然会请求操作系统的服务也必然会引起CPU状态的转换,那这样的话它们之间通信的速度就会降低。而如果是同一个进程下的各个线程之间进行通信的话,那么它们之间的通信就不需要请求操作系统的服务,也就是不需要CPU的状态转换,那这样的话时间开销就会比较小。那显然这样的话可以使通信的效率大幅度地提升,不过大家也需要注意,如果说两个线程它们从属于不同的进程,那么这两个线程之间的通信也需要像刚才我们所说的那样请求操作系统的服务,原因还是一样的,因为它们没办法同时共享一片内存空间。

    那刚才的这一系列学习相信大家能够比较直观地体会到在引入线程之前进程它既是资源分配的基本单位,也是调度的基本单位。但是在引入了线程之后,进程它只是资源分配的基本单位,而CPU的调度单位变成了线程。那以前我们给进程下的定义是这样的,进程它是程序的一次执行过程。但是在引入了线程之后,应该说线程才是程序的一次执行过程。那在引入了线程之后,同样也会有运行态、就绪态、阻塞态这样的三种状态,在这儿就不再展开,其实和进程的运行、就绪、阻塞是一样的。另外需要强调的一点是,既然线程是CPU的调度单位,那么如果说是在多核CPU的环境下,那各个线程它们其实可以被分派到不同的CPU上并行地执行。也就是说这两个线程虽然它们从属于同一个进程,但是这两个线程它们在多核CPU的环境下,其实是可以并行地运行的。系统中的资源都是分配给各个进程的,而线程几乎不拥有资源,但是各个线程的运行其实也需要有一些极少量的资源,所以为了让各个线程之间能够顺利地并发运行,所以也需要给各个线程建立类似于PCB那样的数据结构。那各个线程上处理机运行的时候,其实也需要用到这个处理机上边的各种寄存器,所以在切换线程的时候也需要把线程的寄存器信息进行保存和恢复,同时还需要给各个线程分配它们各自的堆栈。那所谓的堆栈的概念我们之后遇到了需要的时候再讲,反正堆栈其实也是用来存放数据的。存放的就是这个线程运行的过程当中,函数调用的信息啊或者一些参数变量的信息啊等等。总之这儿想表达的就是每个线程也会拥有一些它自己的一些资源,只不过这个资源量很小很小而已。大部分的资源其实依然是归这个进程所有的。

    这个小节当中我们介绍了线程的概念,主要是和传统的进程机制进行了对比,大家结合这个图再来回忆一下它们之间的区别和线程的一些优点。那线程有一些很重要的特点,容易在选择题当中进行考察。那大家可以再结合我们的课后习题进行进一步的巩固和练习。

  • 相关阅读:
    C# 实现任务栏图标程序
    C#实现的木马之客户端
    sql基本语法
    水晶报表引用DataSet做数据源
    解决多线程操作控件时可能出现的异常:“在某个线程上创建的控件不能成为在另一个线程上创建的控件的父级”
    电子书籍制作工具软件大全
    C#实现的木马之服务端
    2进制、8进制、10进制、16进制...各种进制间的轻松转换
    VC# .Net中使用Crystal Report水晶报表
    P2P技术学习
  • 原文地址:https://www.cnblogs.com/ZHONGZHENHUA/p/10670404.html
Copyright © 2011-2022 走看看