操作系统——进程线程协程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统——进程线程协程相关的知识,希望对你有一定的参考价值。


摘要

本博文详细介绍操作系统中的进程、线程、协程相关概念,帮助大家更好的理解操作系统底层原理。

一、操作系统的进程

操作系统中最核心的概念就是进程,进程是对正在运行中的程序的一个抽象。操作系统的其他所有内容都是围绕着进程展开的。进程是操作系统提供的最古老也是最重要的概念之一。即使可以使用的CPU只有一个,它们也支持伪并发操作。它们会将一个单独的CPU抽象为多个虚拟机的CPU可以说:没有进程的抽象,现代操作系统将不复存在。

操作系统——进程、线程、协程_操作系统

在许多多道程序系统中,CPU 会在进程间快速切换,使每个程序运行几十或者几百毫秒。然而,严格意义来说,在某一个瞬间,CPU只能运行一个进程,然而我们如果把时间定位为1秒内的话,它可能运行多个进程。这样就会让我们产生并行的错觉。有时候人们说的伪并行就是这种情况,以此来区分多处理器系统(该系统由两个或多个CPU来共享同一个物理内存)

伪并行:伪并行是指单核或多核处理器同时执行多个进程,从而使程序更快.通过以非常有限的时间间隔在程序之间快速切换CPU,因此会产生并行感。缺点是CPU时间可能分配给下一个进程,也可能不分配给下一个进程。

一、进程模型

总结一句话是:进程是计算机系统分配资源的最小单位。在进程模型中,所有计算机上运行的软件,通常也包括操作系统,被组织为若干顺序进程,简称为进程(process)。一个进程就是一个正在执行的程序的实例,进程也包括程序计数器、寄存器和变量的当前值。从概念上来说,每个进程都有各自的虚拟CPU,但是实际情况是CPU会在各个进程之间进行来回切换。

操作系统——进程、线程、协程_操作系统_02

在上图中,这4道程序被抽象为4个拥有各自控制流程〈即每个自己的程序计数器)的进程,并且每个程序都独立的运行。当然,实际上只有一个物理程序计数器,每个程序要运行时,其逻辑程序计数器会装载到物理程序计数器中。当程序运行结束后,其物理程序计数器就会是真正的程序计数器,然后再把它放回进程的逻辑计数器中。从下图我们可以看到,在观察足够长的一段时间后,所有的进程都运行了,但在任何一个给定的瞬间仅有一个进程真正运行。

操作系统——进程、线程、协程_操作系统_03

因此,当我们说一个CPU只能真正一次运行一个进程的时候,即使有n个核(或CPU),每一个核也只能一次运行一个线程。由于CPU 会在各个进程之间来回快速切换,所以每个进程在CPU中的运行时间是无法确定的。并且当同一个进程再次在CPU中运行时,其在CPU内部的运行时间往往也是不固定的。进程和程序之间的区别是非常微妙的。

但是通过一个例子可以让你加以区分:想想一位会做饭的计算机科学家正在为他的女儿制作生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原谅∶面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序、计算机科学家就是CPU、而做蛋糕的各种原谅都是输入数据。进程就是科学家阅读食谱、取来各种原料以及烘焙蛋糕等一系例了动作的总和。

这里的关键思想是认识到一个进程所需的条件,进程是某一类特定活动的总和,它有程序、输入输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另外一个进程提供服务。另外需要注意的是,如果一个进程运行了两遍,则被认为是两个进程。那么我们了解到进程模型后,那么进程是如何创建的呢?

1.1 进程的创建

操作系统需要一些方式来创建进程。下面是一些创建进程的方式:

  • 系统初始化(init)

启动操作系统时,通常会创建若干个进程。其中有些是前台进程(numerous processes),也就是同用户进行交互并替他们完成工作的进程。一些运行在后台,并不与特定的用户进行交互,例如,设计一个进程来接收发来的电子邮件,这个进程大部分的时间都在休眠,但是只要邮件到来后这个进程就会被唤醒。进程运行在后台用来处理一些活动像是e-mail,web网页,新闻,打印等等被称为守护进程(daemons)。大型系统会有很多守护进程。在UNIX中, ps程序可以列出正在运行的进程,在Windows 中,可以使用任务管理器。

  • 正在运行的程序执行了创建进程的系统调用(比如fork)

除了在启动阶段创建进程之外,一些新的进程也可以在后面创建。通常,一个正在运行的进程会发出系统调用用来创建一个或多个新进程来帮助其完成工作。例如,如果有大量的数据需要经过网络调取并进行顺序处理,那么创建一个进程读数据,并把数据放到共享缓冲区中,而让第二个进程取走并正确处理会比较容易些。在多处理器中,让每个进程运行在不同的CPU上也可以使工作做的更快。

  • 用户请求创建一个新进程

在许多交互式系统中,输入一个命令或者双击图标就可以启动程序,以上任意一种操作都可以选择开启一个新的进程,在基本的UNIX系统中运行X,新进程将接管启动它的窗口。在 Windows 中启动进程时,它一般没有窗口,但是它可以创建一个或多个窗口。每个窗口都可以运行进程。通过鼠标或者命令就可以切换窗口并与进程进行交互。

  • 初始化一个批处理工作

最后一种创建进程的情形会在大型机的批处理系统中应用。用户在这种系统中提交批处理作业。当操作系统决定它有资源来运行另一个任务时,它将创建一个新进程并从其中的输入队列中运行下一个作业。从技术上讲,在所有这些情况下,让现有流程执行流程是通过创建系统调用来创建新流程的。该进程可能是正在运行的用户进程,是从键盘或鼠标调用的系统进程或批处理程序。这些就是系统调用创建新进程的过程。该系统调用告诉操作系统创建一个新进程,并直接或间接指示在其中运行哪个程序。

1.2 进程的终止

进程在创建之后,它就开始运行并做完成任务。然而,没有什么事儿是永不停歇的,包括进程也一样。进程早晚会发生终止,但是通常是由于以下情况触发的

  • 正常退出(自愿的)

多数进程是由于完成了工作而终止.当编译器完成了所给定程序的编译之后,编译器会执行一个系统调用告诉操作系统它完成了工作。这个调用在UNIX中是l exit l,在 Windows 中是ExitProcess面向屏幕中的软件也支持自愿终止操作。字处理软件、Internet浏览器和类似的程序中总有一个供用户点击的图标或菜单项,用来通知进程删除它锁打开的任何临时文件,然后终止。

  • 错误退出(自愿的)

进程发生终止的第二个原因是发现严重错误,例如,如果用户执行如下命令:cc foo.c
为了能够编译foo.c但是该文件不存在,于是编译器就会发出声明并退出。在给出了错误参数时,面屏幕的交互式进程通常并不会直接退出,因为这从用户的角度来说并不合理,用户需要知道发生了什么并想要进行重试,所以这时候应用程序通常会弹出一个对话框告知用户发生了系统错误,是需要重试还是退出。

  • 严重错误(非自愿的)

进程终止的第三个原因是由进程引起的错误,通常是由于程序中的错误所导致的。例如,执行了一条非法指令,引用不存在的内存,或者除数是О等。在有些系统比如UNIX中,进程可以通知操作系统,它希望自行处理某种类型的错误,在这类错误中,进程会收到信号(中断),而不是在这类错误出现时直接终止进程。

  • 其他进程杀死(非自愿的)

第四个终止进程的原因是,某个进程执行系统调用告诉操作系统杀死某个进程。在UNIX中,这个系统调用是kill!在Win32中对应的函数是TerminateProcess(注意不是系统调用。

1.3 进程的层次结构

在一些系统中,当一个进程创建了其他进程后,父进程和子进程就会以某种方式进行关联。子进程它自己就会创建更多进程,从而形成一个进程层次结构。

  • UNIX进程体系

在UNIX中,进程和它的所有子进程以及子进程的子进程共同组成一个进程组。当用户从键盘中发出一个信号后,该信号被发送给当前与键盘相关的进程组中的所有成员(它们通常是在当前窗口创建的所有活动进程)。每个进程可以分别捕获该信号、忽略该信号或采取默认动作,被信号kill掉。

这里有另一个例子,可以用来说明层次的作用,考虑UNIX在启动时如何初始化自己。一个称为init 的特殊进程出现在启动映像中。当init进程开始运行时,它会读取一个文件,文件会告诉它有多少个终端。然后为每个终端创建一个新进程。这些进程等待用户登录。如果登录成功,该登录进程就执行一个shell 来等待接收用户输入指令,这些命令可能会启动更多的进程,以此类推。因此,整个操作系统中所有的进程都隶属于一个单个以init为根的进程树。

操作系统——进程、线程、协程_系统调用_04

  •  window进程体系

相反,Windows 中没有进程层次的概念,Windows 中所有进程都是平等的,唯一类似于层次结构的是在创建进程的时候,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程。然而,这个令牌可能也会移交给别的操作系统,这样就不存在层次结构了。而在 UNI中,进程不能剥夺其子进程的进程权。

1.4 进程的状态

尽管每个进程是一个独立的实体,有其自己的程序计数器和内部状态,但是,进程之间仍然需要相互帮助。例如,一个进程的结果可以作为另一个进程的输入,在shell命令中

cat chapter1 chapter2 chapter3 l grep tree

操作系统——进程、线程、协程_操作系统_05

 图中会涉及三种状态

  • 1.运行态,运行态指的就是进程实际占用CPU时间片运行时
  • 2.就绪态,就绪态指的是可运行,但因为其他进程正在运行而处于就绪状态
  • 3.阻塞态,除非某种外部事件发生,否则进程不能运行

逻辑上来说,运行态和就绪态是很相似的。这两种情况下都表示进程可运行,但是第二种情况没有获得CPU时间分片。第三种状态与前两种状态不同的原因是这个进程不能运行,CPU空闲时也不能运行。三种状态会涉及四种状态间的切换,在操作系统发现进程不能继续执行时会发生状态1的轮转,在某些系统中进程执行系统调用,例如pause ,来获取一个阻塞的状态。在其他系统中包括UNIX,当进程从管道或特殊文件(例如终端)中读取没有可用的输入时,该进程会被自动终止。

转换⒉和转换3都是由进程调度程序(操作系统的一部分)引起的,进程本身不知道调度程序的存在。转换⒉的出现说明进程调度器认定当前进程已经运行了足够长的时间,是时候让其他进程运行CPU时间片了。当所有其他进程都运行过后,这时候该是让第一个进程重新获得CPU时间片的时候了,就会发生转换3。当进程等待的一个外部事件发生时(如从外部输入一些数据后),则发生转换4。如果此时没有其他进程在运行,则立刻触发转换3,该进程便开始运行,否则该进程会处于就绪阶段,等待CPU空闲后再轮到它运行。

操作系统——进程、线程、协程_操作系统_06

基于进程的操作系统中最底层的是中断和调度处理,在该层之上是顺序进程。操作系统最底层的就是调度程序,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。

1.5 进程的实现

操作系统为了执行进程间的切换,会维护着一张表格,这张表就是进程表(process table),每个进程占用一个进程表项。该表项包含了进程状态的重要信息包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。

操作系统——进程、线程、协程_操作系统_07

 系统中断和调用的过程

  1. 硬件压入堆栈程序计数器等
  2. 硬件从中断向量装入新的程序计数器3.汇编语言过程保存寄存器的值
  3. 汇编语言过程设置新的堆栈
  4. C中断服务器运行(典型的读和缓存写入)6.调度器决定下面哪个程序先运行
  5. C过程返回至汇编代码
  6. 汇编语言过程开始运行新的当前进程

一个进程在执行过程中可能被中断数千次,但关键每次中断后,被中断的进程都返回到与中断发生前完全相同的状态。

二、操作系统的线程

2.1 线程的使用

  • 多线程之间会共享同一块地址空间和所有可用数据的能力,这是进程所不具备的。
  • 线程要比进程更轻量级,由于线程更轻,所以它比进程更容易创建,也更容易撤销。在许多系统中,创建一个线程要比创建一个进程快10-100倍。
  • 第三个原因可能是性能方面的探讨,如果多个线程都是CPU密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的I/O处理,拥有多个线程能在这些活动中彼此重叠进行,从而会加快应用程序的执行速度

2.2 线程模型

操作系统——进程、线程、协程_程序计数器_08

 下图中,我们可以看到有一个进程三个线程的情况。每个线程都在相同的地址空间中运行。

操作系统——进程、线程、协程_临界区_09

线程不像是进程那样具备较强的独立性。同一个进程中的所有线程都会有完全一样的地址空间,这意味着它们也共享同样的全局变量。由于每个线程都可以访问进程地址空间内每个内存地址,因此一个线程可以读取、写入甚至擦除另一个线程的堆栈。线程之间除了共享同一内存空间外,还具有如下不同的内容

操作系统——进程、线程、协程_临界区_10

上图左边的是同一个进程中每个线程共享的内容,上图右边是每个线程中的内容。也就是说左边的列表是进程的属性,右边的列表是线程的属性。和进程一样,线程可以处于下面这几种状态:运行中、阻塞、就绪和终止,正在运行的线程拥有CPU时间片并且状态是运行中。一个被阻塞的线程会等待某个释放它的事件。例如,当一个线程执行从键盘读入数据的系统调用时,该线程就被阻塞直到有输入为止。线程通常会被阻塞,直到它等待某个外部事件的发生或者有其他线程来释放它。线程之间的状态转换和进程之间的状态转换是一样的。每个线程都会有自己的堆栈,如下图所示

操作系统——进程、线程、协程_系统调用_11

 2.3 线程在系统调用

进程通常会从当前的某个单线程开始,然后这个线程通过调用一个库函数〈比如thread_create )创建新的线程。线程创建的函数会要求指定新创建线程的名称。创建的线程通常都返回一个线程标识符,该标识符就是新线程的名字。

当一个线程完成工作后,可以通过调用一个函数(比如thread_exit)来退出。紧接着线程消失,状态变为终止,不能再进行调度。在某些线程的运行过程中,可以通过调用函数例如thread_join ,表示一个线程可以等待另一个线程退出。这个过程阻塞调用线程直到等待特定的线程退出。在这种情况下,线程的创建和终止非常类似于进程的创建和终止。

另一个常见的线程是调用thread_yield ,它允许线程自动放弃CPU从而让另一个线程运行。这样一个调用还是很重要的,因为不同于进程,线程是无法利用时钟中断强制让线程让出CPU的。

2.4 线程的实现方式

2.4.1 在用户空间中实现线程

第一种方法是把整个线程包放在用户空间中,内核对线程一无所知,它不知道线程的存在。所有的这类实现都有同样的通用结构

操作系统——进程、线程、协程_系统调用_12

运行时系统(Runtime System):也叫做运行时环境,该运行时系统提供了程序在其中运行的境。此环境可能会解决许多问题,包括应用程序内存的布局,程序如何访问变量,在过程之间传递参数的机制,与操作系统的接口等等。编译器根据特定的运行时系统进行假设以生成正确的代码。通常,运行时系统将负责设置和管理堆栈,并且会包含诸如垃圾收集,线程或语言内置的其他动态的功能。

在用户空间管理线程时,每个进程需要有其专用的线程表(thread table),用来跟踪该进程中的线程。这些表和内核中的进程表类似,不过它仅仅记录各个线程的属性,如每个线程的程序计数器、堆栈指针、寄存器和状态。该线程标由运行时系统统一管理。当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程的所有信息,与内核在进程表中存放的信息完全一样。

在用户空间实现线程的优点是:

在用户空间中实现线程要比在内核空间中实现线程具有这些方面的优势︰考虑如果在线程完成时或者是在调用pthread_yield时,必要时会进程线程切换,然后线程的信息会被保存在运行时环境所提供的线程表中,然后,线程调度程序来选择另外一个需要运行的线程。保存线程的状态和调度程序都是本地过程,所以启动他们比进行内核调用效率更高。因而不需要切换到内核,也就不需要上下文切换,也不需要对内存高速缓存进行刷新,因为线程调度非常便捷,因此效率比较高。

在用户空间实现线程的缺点是:

你如何实现阻塞系统调用呢?假设在还没有任何键盘输入之前,一个线程读取键盘,让线程进行系统调用是不可能的,因为这会停止所有的线程。所以,使用线程的一个目标是能够让线程进行阻塞调用,并且要避免被阻塞的线程影响其他线程。

与阻塞调用类似的问题是缺页中断问题,实际上计算机并不会把所有的程序都一次性的放入内存中,如果某个程序发生函数调用或者跳转指令到了一条不在内存的指令上,就会发生页面故障,而操作系统将到磁盘上取回这个丢失的指令,这就称为缺页故障。而在对所需的指令进行读入和执行时,相关的进程就会被阻塞。如果只有一个线程引起页面故障,内核由于甚至不知道有线程存在,通常会吧整个进程阻塞直到磁盘I/O完成为止,尽管其他的线程是可以运行的。

另外一个问题是,如果一个线程开始运行,该线程所在进程中的其他线程都不能运行,除非第一个线程自愿的放弃CPU,在一个单进程内部,没有时钟中断,所以不可能使用轮转调度的方式调度线程!除非其他线程能够以自己的意愿进入运行时环境,否则调度程序没有可以调度线程的机会。

2.4.2 在内核空间中实现线程

现在我们考虑使用内核来实现线程的情况,此时不再需要运行时环境了。另外,每个进程中也没有线程表。相反,在内核中会有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用通过对线程表的更新来完成线程创建或销毁工作。

操作系统——进程、线程、协程_程序计数器_13

内核中的线程表持有每个线程的寄存器、状态和其他信息。这些信息和用户空间中的线程信息相同,但是位置却被放在了内核中而不是用户空间中。另外,内核还维护了一张进程表用来跟踪系统状态。所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程〈如果有就绪线程的话)还是运行一个另一个进程中的线程。但是在用户实现中,运行时系统始终运行自己的线程,直到内核剥夺它的CPU时间片(或者没有可运行的线程存在了)为止。

由于在内核中创建或者销毁线程的开销比较大,所以某些系统会采用可循环利用的方式来回收线程。当某个线程被销毁时,就把它标志为不可运行的状态,但是其内部结构没有受到影响。稍后,在必须创建一个新线程时,就会重新启用旧线程,把它标志为可用状态。

如果某个进程中的线程造成缺页故障后,内核很容易的就能检查出来是否其他可运行的线程,如果有的话,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止)比较多,就会带来很大的开销。

2.4.3 在用户和内核空间中混合实现线程

结合用户空间和内核空间的优点,设计人员采用了一种内核级线程的方式,然后将用户级线程与某些或者全部内核线程多路复用起来。

用户线程与内核线程的多路复用:在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。

操作系统——进程、线程、协程_操作系统_14

2.5 进程通信的原理

2.5.1 竞态条件

即两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性时,这种就被称为竞态条件(race condition)。

2.5.2 临界区

不仅共享资源会造成竞态条件,事实上共享文件、共享内存也会造成竞态条件、那么该如何避免呢?或许一句话可以概括说明:禁止一个或多个进程在同一时刻对共享资源(包括共享内存、共享文件等)进行读写。换句话说,我们需要一种互斥(mutual exclusion)条件,这也就是说,如果一个进程在某种方式下使用共享变量和文件的话,除该进程之外的其他进程就禁止做这种事(访问统一资源)。上面问题的纠结点在于,在进程A对共享变量的使用未结束之前进程B就使用它。在任何操作系统中,为了实现互斥操作而选用适当的原语是一个主要的设计问题。

避免竞争问题的条件可以用一种抽象的方式去描述。大部分时间,进程都会忙于内部计算和其他不会导致竞争条件的计算。然而,有时候进程会访问共享内存或文件!或者做一些能够导致竞态条件的操作。我们把对共享内存进行访问的程序片段称作临界区域或临界区。如果我们能够正确的操作,使两个不同进程不可能同时处于临界区,就能避免竞争条件,这也是从操作系统设计角度来进行的。

尽管上面这种设计避免了竞争条件,但是不能确保并发线程同时访问共享数据的正确性和高效性。一个好的解决方案,应该包含下面四种条件

  • 1.任何时候两个进程不能同时处于临界区
  • 2.不应对CPU的速度和数量做任何假设
  • 3.位于临界区外的进程不得阻塞其他进程
  • 4.不能使任何进程无限等待进入临界区

操作系统——进程、线程、协程_系统调用_15

使用临界区的互斥从抽象的角度来看,我们通常希望进程的行为如上图所示,在t1时刻,进程A进入临界区,在t2的时刻,进程B尝试进入临界区,因为此时进程A正在处于临界区中,所以进程B会阻塞直到t3时刻进程A离开临界区,此时进程B能够允许进入临界区。最后,在t4时刻,进程B离开临界区,系统恢复到没有进程的原始状态。

2.5.3 忙等互斥

下面我们会继续探讨实现互斥的各种设计,在这些方案中,当一个进程正忙于更新其关键区域的共享内存时,没有其他进程会进入其关键区域,也不会造成影响。

2.5.4 屏蔽中断

在单处理器系统上,最简单的解决方案是让每个进程在进入临界区后立即屏蔽所有中断,并在离开临界区之前重新启用它们。屏蔽中断后,时钟中断也会被屏蔽。CPU只有发生时钟中断或其他中断时才会进行进程切换。这样,在屏蔽中断后CPU不会切换到其他进程。所以,一旦某个进程屏蔽中断之后,它就可以检查和修改共享内存,而不用担心其他进程介入访问共享数据。

这个方案可行吗?进程进入临界区域是由谁决定的呢?不是用户进程吗?当进程进入临界区域后,用户进程关闭中断,如果经过一段较长时间后进程没有离开,那么中断不就一直启用不了,结果会如何?可能会造成整个系统的终止。而且如果是多处理器的话,屏蔽中断仅仅对执行disable 指令的CPU有效。其他CPU仍将继续运行,并可以访问共享内存。

另一方面,对内核来说,当它在执行更新变量或列表的几条指令期间将中断屏蔽是很方便的。例如,如果多个进程处理就绪列表中的时候发生中断,则可能会发生竞态条件的出现。所以,屏蔽中断对于操作系统本身来说是一项很有用的技术,但是对于用户线程来说,屏蔽中断却不是一项通用的互斥机制。

2.5.5 锁变量

可以寻找一种软件层面解决方案。考虑有单个共享的(锁)变量,初始为值为0。当一个线程想要进入关键区域时,它首先会查看锁的值是否为0,如果锁的值是О,进程会把它设置为1并让进程进入关键区域。如果锁的状态是1,进程会等待直到锁变量的值变为0。因此,锁变量的值是0则意味着没有线程进入关键区域。如果是1则意味着有进程在关键区域内。我们对上图修改后,如下所示

操作系统——进程、线程、协程_操作系统_16

这种设计方式是否正确呢?是否存在纰漏呢?假设一个进程读出锁变量的值并发现它为0,而恰好在它将其设置为1之前,另一个进程调度运行,读出锁的变量为0,并将锁的变量设置为1。然后第一个线程运行,把锁变量的值再次设置为1,此时,临界区域就会有两个进程在同时运行。

也许有的读者可以这么认为,在进入前检查一次,在要离开的关键区域再检查一次不就解决了吗?实际上这种情况也是于事无补,因为在第二次检查期间其他线程仍有可能修改锁变量的值,换句话说,这种set-before-check不是一种原子性操作,所以同样还会发生竞争条件。

2.5.6 严格轮询法

第三种互斥的方式先抛出来一段代码,这里的程序是用C语言编写,之所以采用C是因为操作系统普遍是用C来编写的(偶尔会用C++),而基本不会使用Java 、Modula3或Pascal这样的语言,Java中的native关键字底层也是C或C++编写的源码。对于编写操作系统而言,需要使用C语言这种强大、高效、可预知和有特性的语言,而对于Java,它是不可预知的,因为它在关键时刻会用完存储器,而在不合适的时候会调用垃圾回收机制回收内存。在C语言中,这种情况不会发生,C语言中不会主动调用垃圾回收回收内存。有关C、C++ 、Java和其他四种语言的比较可以参考链接

进程和线程的区别与联系

区别:

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
  • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler等;线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集等;
  • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

联系

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
  • 处理机分给线程,即真正在处理机上运行的是线程;
  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

三、协程的概念

协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制在用户态执行。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

操作系统——进程、线程、协程_系统调用_17

 协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。

def A():
print 1
print 2
print 3

def B():
print x
print y
print z

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:1 2 x y 3 z。

 协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

  • 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
     


进程线程和协程的区别

首先,给出“进程、线程和协程”的特点:

  • 进程:拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;
  • 线程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;
  • 协程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度

以上是关于操作系统——进程线程协程的主要内容,如果未能解决你的问题,请参考以下文章

进程线程轻量级进程协程和go中的Goroutine

9-[协程]-协程理论

线程同步之临界区

多线程小结

面向面试操作系统面经

进程线程协程