02-操作系统 进程和线程

进程和线程

什么是进程和线程

  • 进程(Process) 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。
  • 线程(Thread) 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。

线程可以分为两类:

  • 用户级线程(user level thread):对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。
  • 内核级线程(kernel level thread):对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的 CPU,以实现真正的并行计算。

事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。

进程与线程的区别和联系 ★★★★★

下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧!

Java 运行时数据区域(JDK1.8 之后)

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的 方法区 (JDK1.8 之后的元空间) 资源,但是每个线程有自己的 程序计数器虚拟机栈本地方法栈

进程和线程的关系

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
  • 处理机分给线程,即真正在处理机上运行的是线程。
  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

进程与线程的区别

  • 进程有自己的独立地址空间,线程没有。
  • 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
  • 进程和线程通信方式不同。线程之间的通信比较方便,同一进程下的线程共享数据(比如全局变量,静态变量),通过这些数据来通信不仅快捷而且方便,当然如何处理好这些访问的同步与互斥正是编写多线程程序的难点。而进程之间的通信只能通过进程通信的方式进行。
  • 进程上下文切换开销大,线程开销小。
  • 一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程。
  • 对进程操作一般开销都比较大,对线程开销就小了。

有了进程为什么还需要线程

  • 进程切换是一个开销很大的操作,线程切换的成本较低。
  • 线程更轻量,一个进程可以创建多个线程。
  • 多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。
  • 同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核。

上下文切换(Context Switch)

对于单核单线程 CPU 而言,在某一时刻只能执行一条 CPU 指令。上下文切换是一种 将CPU 资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。

在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。

为什么进程上下文切换比线程上下文切换代价高?

进程切换分两步:1.切换页目录以使用新的地址空间;2.切换内核栈和硬件上下文。

对于 linux 来说,线程和进程的最大区别就在于地址空间,对于线程切换,第 1 步是不需要做的,第 2 步是进程和线程切换都要做的。

切换的性能消耗:

  1. 线程上下文切换和进程上下文切换一个最主要的区别是:线程切换的虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
  2. 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

线程

为什么要使用多线程

先从总体上来说:

  • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

再深入到计算机底层来探讨:

  • 单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
  • 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。

线程间的同步的方式有哪些

线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。

下面是几种常见的线程同步的方式:

  1. 互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
  2. 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
  3. 信号量(Semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  4. 屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 CyclicBarrier 是这种机制。
  5. 事件(Event):Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

或者说一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。

线程安全问题都是由全局变量及静态变量引起的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程共享资源和独占资源问题

参考链接

一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(私有的)栈(stack),Windows 线程的缺省堆栈大小为 1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个 C 运行时堆,这个堆为本进程中所有线程共享,windows 进程还有所谓进程默认堆,用户也可以创建自己的堆。

用操作系统术语,线程切换的时候实际上切换的是一个可以称之为线程控制块的结构(TCB),里面保存所有将来用于恢复线程环境必须的信息,包括所有必须保存的寄存器集,线程的状态等。

堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

栈: 是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe 的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP 寄存器。栈空间不需要在高级语言里面显式的分配和释放。

进程

PCB 是什么?包含哪些信息?

PCB(Process Control Block) 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。你可以将 PCB 视为进程的大脑。

当操作系统创建一个新进程时,会为该进程分配一个唯一的进程 ID,并且为该进程创建一个对应的进程控制块。当进程执行时,PCB 中的信息会不断变化,操作系统会根据这些信息来管理和调度进程。

PCB 主要包含下面几部分的内容:

  • 进程的描述信息,包括进程的名称、标识符等等;
  • 进程的调度信息,包括进程阻塞原因、进程状态(就绪、运行、阻塞等)、进程优先级(标识进程的重要程度)等等;
  • 进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。
  • 进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。
  • 处理机的状态信息(由处理机的各种寄存器中的内容组成的),包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
  • ……

进程有哪几种状态?

我们一般把进程大致分为 5 种状态,这一点和线程很像!

  • 创建状态(new):进程正在被创建,尚未到就绪状态。
  • 就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
  • 运行状态(running):进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
  • 阻塞状态(waiting):又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
  • 结束状态(terminated):进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。

进程间的通信方式有哪些 ★★★

进程通信,是指进程之间的信息交换(信息量少则一个状态或数值,多者则是成千上万个字节)。

  • 低级进程通信:用信号量进行的进程间的互斥和同步,由于其所交换的信息量少而被归结为低级通信。
  • 高级进程通信:用户可以利用操作系统所提供的一组通信命令传送大量数据的一种通信方式。

操作系统隐藏了进程通信的实现细节。或者说,通信过程对用户是透明的。

高级通信机制可归结为三大类:

  1. 共享存储器系统(存储器中划分的共享存储区);实际操作中对应的是“剪贴板”(剪贴板实际上是系统维护管理的一块内存区域)的通信方式,比如举例如下:word 进程按下 ctrl+c,在 ppt 进程按下 ctrl+v,即完成了 word 进程和 ppt 进程之间的通信,复制时将数据放入到剪贴板,粘贴时从剪贴板中取出数据,然后显示在 ppt 窗口上。
  2. 消息传递系统(进程间的数据交换以消息(message)为单位,当今最流行的微内核操作系统中,微内核与服务器之间的通信,无一例外地都采用了消息传递机制。应用举例:邮槽(MailSlot)是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。
  3. 管道通信系统(管道即:连接读写进程以实现他们之间通信的共享文件(pipe文件,类似先进先出的队列,由一个进程写,另一进程读)。实际操作中,管道分为:匿名管道、命名管道。匿名管道是一个未命名的、单向管道,通过父进程和一个子进程之间传输数据。匿名管道只能实现本地机器上两个进程之间的通信,而不能实现跨网络的通信。命名管道不仅可以在本机上实现两个进程间的通信,还可以跨网络实现两个进程间的通信。

下面这部分总结参考了:《进程间通信 IPC (InterProcess Communication)》 这篇文章,推荐阅读,总结的非常不错。

  1. 管道/匿名管道(Pipes):管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。
    进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
    匿名管道用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。

  2. 有名管道(Named Pipes): 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 先进先出(First In First Out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。

  3. 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;除了用于进程间通信外,进程还可以发送信号给进程本身。linux 除了支持 Unix 早期信号语义函数 sigal 外,还支持语义符合 Posix.1 标准的信号函数 sigaction(实际上该函数是基于 BSD 的,BSD为了实现可靠信号机制,又能够统一对外接口,用 sigaction 函数重新实现了 signal 函数)。

  4. 消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。

  5. 信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。

  6. 共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。

  7. 套接字(Sockets):此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

进程调度

调度种类

  • 高级调度(High-Level Scheduling):又称为作业调度,它决定把后备作业调入内存运行;
  • 低级调度(Low-Level Scheduling):又称为进程调度,它决定把就绪队列的某进程获得 CPU;
  • 中级调度(Intermediate-Level Scheduling):又称为在虚拟存储器中引入,在内、外存对换区进行进程对换。

非抢占式调度与抢占式调度

  • 非抢占式:分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生进程调度进程调度某事件而阻塞时,才把处理机分配给另一个进程。
  • 抢占式:操作系统将正在运行的进程强行暂停,由调度程序将CPU分配给其他就绪进程的调度方式。

调度策略的设计

  • 响应时间: 从用户输入到产生反应的时间。
  • 周转时间: 从任务开始到任务结束的时间。

CPU任务可以分为交互式任务批处理任务,调度最终的目标是合理的使用 CPU,使得交互式任务的响应时间尽可能短,用户不至于感到延迟,同时使得批处理任务的周转时间尽可能短,减少用户等待的时间。

进程的调度算法有哪些 ★★★

常见进程调度算法

这是一个很重要的知识点!为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率,计算机科学家已经定义了一些算法,它们是:

  • 先到先服务调度算法(FCFS,First Come, First Served):从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。

    • 未考虑任务特性,平均等待时间可以缩短。
  • 短作业优先的调度算法(SJF,Shortest Job First):从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。

    • SJF 可以保证最小的平均等待时间。
  • 优先级调度算法(Priority):为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。

    • 注意:优先权太低的任务一直就绪,得不到运行,出现“饥饿”现象。
  • 时间片轮转调度算法(RR,Round-Robin) :时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

    • 优点:定时有响应,等待时间较短;缺点:上下文切换次数较多;
    • 时间片太大,响应时间太长;吞吐量变小,周转时间变长;当时间片过长时,退化为 FCFS。
  • 多级反馈队列调度算法(MFQ,Multi-level Feedback Queue):前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成,因而它是目前被公认的一种较好的进程调度算法,UNIX、Windows 等采取的便是这种调度算法或其变形。

多级反馈队列调度算法描述:

  • 进程在进入待调度的队列等待时,首先进入优先级最高的 Q1 等待。
  • 首先调度优先级高的队列中的进程。若高优先级中队列中已没有调度的进程,则调度次优先级队列中的进程。 例如:Q1,Q2,Q3三个队列,只有在 Q1 中没有进程等待时才去调度 Q2,同理,只有 Q1,Q2 都为空时才会去调度 Q3。
  • 对于同一个队列中的各个进程,按照时间片轮转法调度。 比如 Q1 队列的时间片为 N,那么 Q1 中的作业在经历了 N 个时间片后若还没有完成,则进入 Q2 队列等待,若 Q2 的时间片用完后作业还不能完成,一直进入下一级队列,直至完成。
  • 在最后一个队列 QN 中的各个进程,按照时间片轮转分配时间片调度。
  • 在低优先级的队列中的进程在运行时,又有新到达的作业,那么在运行完这个时间片后,CPU 马上分配给新到达的作业(抢占式)。 换而言之,任何时刻,只有当第 1~i-1 队列全部为空时,才会去执行第i队列的进程(抢占式)。特别说明,当再度运行到当前队列的该进程时,仅分配上次还未完成的时间片,不再分配该队列对应的完整时间片。

算法示例

假设系统中有 3 个反馈队列 Q1,Q2,Q3,时间片分别为 2,4,8。

设有 3 个作业 J1,J2,J3 分别在时间 0,1,3 时刻到达。而它们所需要的CPU时间分别是 3,2,1 个时间片。

  1. 时刻0:J1 到达。于是进入到队列 1,运行 1 个时间片,时间片还未到,此时 J2 到达。
  2. 时刻1:J2 到达。由于同一队列采用先来先服务,于是 J2 等待。J1 在运行了1个时间片后,已经完成了在 Q1 中的2个时间片的限制,于是 J1 置于Q2等待被调度。当前处理机分配给J2。
  3. 时刻2:J1 进入 Q2 等待调度,J2 获得 CPU 开始运行。
  4. 时刻3:J3 到达,由于同一队列采用先来先服务,故 J3 在 Q1 等待调度,J1 也在 Q2 等待调度。
  5. 时刻4:J2 处理完成,由于 J3,J1 都在等待调度,但是 J3 所在的队列比 J1 所在的队列的优先级要高,于是 J3 被调度,J1 继续在 Q2 等待。
  6. 时刻5:J3 经过 1 个时间片,完成。
  7. 时刻6:由于 Q1 已经空闲,于是开始调度 Q2 中的作业,则 J1 得到处理器开始运行。J1 再经过一个时间片,完成了任务。于是整个调度过程结束。

从上面的例子看,在多级反馈队列中,后进的作业不一定慢完成。

守护进程、僵尸进程、孤儿进程 ★★★

在 Unix/Linux 系统中,子进程通常是通过 fork() 系统调用创建的,该调用会创建一个新的进程,该进程是原有进程的一个副本。子进程和父进程的运行是相互独立的,它们各自拥有自己的 PCB,即使父进程结束了,子进程仍然可以继续运行。

当一个进程调用 exit() 系统调用结束自己的生命时,内核会释放该进程的所有资源,包括打开的文件、占用的内存等,但是该进程对应的 PCB 依然存在于系统中。这些信息只有在父进程调用 wait()waitpid() 系统调用时才会被释放,以便让父进程得到子进程的状态信息。

这样的设计可以让父进程在子进程结束时得到子进程的状态信息,并且可以防止出现“僵尸进程”(即子进程结束后 PCB 仍然存在但父进程无法得到状态信息的情况)。

  • 守护进程:运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务

  • 僵尸进程:子进程已经终止,但是其父进程仍在运行,且父进程没有调用 wait()waitpid() 等系统调用来获取子进程的状态信息,释放子进程占用的资源,导致子进程的 PCB 依然存在于系统中,但无法被进一步使用。这种情况下,子进程被称为“僵尸进程”。避免僵尸进程的产生,父进程需要及时调用 wait()waitpid() 系统调用来回收子进程。
    百度百科:僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。

    危害:

    • 占用系统资源:僵尸进程虽然不再占用CPU等实际资源,但仍然会占用一定的系统资源,如进程表项、文件描述符等。
    • 影响系统性能:如果僵尸进程数量较多,会导致进程表等内核数据结构变得过大,从而影响整个系统的性能和稳定性。
    • 安全漏洞:恶意攻击者可以创建大量僵尸进程,以消耗系统资源,甚至会引发拒绝服务攻击等安全漏洞。
  • 孤儿进程:一个进程的父进程已经终止或者不存在,但是该进程仍在运行。这种情况下,该进程就是孤儿进程。
    孤儿进程通常是由于父进程意外终止或未及时调用 wait()waitpid() 等系统调用来回收子进程导致的。为了避免孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为 init 进程(进程号为 1),由 init 进程来回收孤儿进程的资源。

僵尸进程将会导致资源浪费,而孤儿进程则不会。

如何查看是否有僵尸进程?

Linux 下可以使用 Top 命令查找,zombie 值表示僵尸进程的数量,为 0 则代表没有僵尸进程。

僵尸进程查看

下面这个命令可以定位僵尸进程以及该僵尸进程的父进程:

1
ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'

进程同步与进程互斥

操作系统中的并发进程有些是独立的有些需要相互协作,独立的进程在系统中执行不影响其他进程,也不被其他进程影响(因为他们没有共同需要一起用到的资源)。而另外一些进程则需要与其他进程共享数据,以完成一项共同的任务。因此,为了保证操作系统的正常活动,使得程序的执行具有可再现性,保证执行结果的正确性。操作系统必须为这种协作的进程提供某种机制。

进程间的协作关系分为:互斥,同步,通信。

临界资源、临界区、如何解决冲突?

在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。

对于临界资源的访问,必须是互斥进行。

每个进程中访问临界资源的那段程序称为临界区。 每次只准许一个进程进入临界区,进入后不允许其他进程进入。进入临界区的进程要在有限时间内退出,以便其他进程能及时进入自己的临界区。如果不能进入自己的临界区,就应该让出 CPU,避免进程出现忙等等现象。进入临界区之前需要判断能否进入,进入时需要改变标志阻止其他进程进入,进入的进程执行完成后退出时修改标志。

进程同步

同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。

例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。

进程同步的主要任务:是对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效地共享资源和相互合作,从而使程序的执行具有可再现性。

进程互斥

互斥亦称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。

例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时,系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。

为了实现临界资源的互斥访问,可以在逻辑上将一个进程对临界资源的访问过程分为四个部分:

  • 进入区:为了进入临界区使用临界资源,在进入区要检查可否进入临界区,如果可以进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区。
  • 临界区:进程中访问临界资源的那段代码,又称临界段。
  • 退出区:将正在访问临界区的标志清除。
  • 剩余区:代码中的其余部分。

对于一个进程,如果此时它也想要访问这个资源,同样就会在进入区做一个检查,它知道了其他进程正在访问,所以自己就不能访问了。这样就实现了资源访问的互斥。

四个原则:

更加具体的细节,我们需要用四个原则来约束这个互斥的过程:

  1. 空闲则入,没有进程进入时可以进入。
  2. 忙则等待,有进程在临界区时其他进程均不可进入(保证对临界区的互斥访问)。
  3. 有限等待,等待进入临界区的进程不能无限制等待(有限代表有限的时间,避免死等)。
  4. 让权等待,不能进入的进程应释放CPU进入阻塞状态(当进程不能进入自己的临界区时,应该释放处理机,以免陷入忙等状态)。

实现互斥常用的方法有:软件实现法,硬件实现法,pv信号量法。

  • 软件层面如何实现进程互斥
    • 单标志法
    • 双标志先检查法
    • 双标志后检查法
    • Peterson 算法
  • 硬件层面如何实现进程互斥
    • 中断屏蔽方法
    • TestandSetLock 指令
    • Wrap 指令

进程同步有哪几种机制

  1. 信号量机制:一个信号量只能置一次初值,以后只能对之进行p操作或v操作。由此也可以看到,信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点。

  2. 自旋锁:旋锁是为了保护共享资源提出的一种锁机制。调用者申请的资源如果被占用,即自旋锁被已经被别的执行单元保持,则调用者一直循环在那里看是否该自旋锁的保持着已经释放了锁,自旋锁是一种比较低级的保护数据结构和代码片段的原始方式,可能会引起以下两个问题;

    1. 死锁
    2. 过多地占用CPU资源
  3. 管程:信号量机制功能强大,但使用时对信号量的操作分散,而且难以控制,读写和维护都很困难。因此后来又提出了一种集中式同步进程——管程。其基本思想是将共享变量和对它们的操作集中在一个模块中,操作系统或并发程序就由这样的模块构成。这样模块之间联系清晰,便于维护和修改,易于保证正确性。

  4. 会合:进程直接进行相互作用。

  5. 分布式系统:由于在分布式操作系统中没有公共内存,因此参数全为值参,而且不可为指针。

线程同步

线程同步是指多个线程同时访问某资源时,采用一系列的机制以保证最多只能一个线程访问该资源。线程同步是多线程中必须考虑和解决的问题,因为很有可能发生多个线程同时访问(主要是写操作)同一资源,如果不进行线程同步,很可能会引起数据混乱,造成线程死锁等问题。

多线程同步和互斥有几种实现方法

线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。

用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。

内核模式下的方法有:事件,信号量,互斥量。

  1. 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
  2. 互斥量:为协调共同对一个共享资源的单独访问而设计的。
  3. 信号量:为控制一个具有有限数量用户资源而设计。
  4. 事件(信号):用来通知线程有一些事件已发生,从而启动后继任务的开始。

线程同步不同方式间的总结比较

互斥量和临界区很相似,但是互斥量是可以命名的,它可以跨越进程使用,所以创建互斥量所需要的资源更多,如果只是为了在进程内部使用使用临界区会带来速度上的优势并能够减少资源占用量。
互斥量、信号量、事件都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,所以可以使用WaitForSingleObject来等待进程和线程退出。

Semaphore(信号量) Vs Mutex(互斥锁) VS 自旋锁

  • 当用户创立多个线程/进程时,如果不同线程/进程同时读写相同的内容,则可能造成读写错误,或者数据不一致。此时,需要通过加锁的方式,控制临界区(critical section)的访问权限。对于semaphore而言,在初始化变量的时候可以控制允许多少个线程/进程同时访问一个临界区,其他的线程/进程会被堵塞,直到有人解锁。
  • Mutex 相当于只允许一个线程/进程访问的 semaphore。此外,根据实际需要,人们还实现了一种读写锁(read-write lock),它允许同时存在多个阅读者(reader),但任何时候至多只有一个写者(writer),且不能于读者共存。
  • 通互斥量(也叫互斥锁)相同,都是在临界资源被占用时,阻止其他线程再去使用。与互斥锁的不同:自旋锁不会让出CPU,但是互斥锁会。

02-操作系统 进程和线程
https://flepeng.github.io/interview-01-操作系统-02-操作系统-进程和线程/
作者
Lepeng
发布于
2020年8月8日
许可协议