不断学习,做更好的自己!💪

进程和线程的区别

典型回答

  1. 进程是操作系统资源分配的基本单位,而线程是 CPU 调度和分配的基本单位;
  2. 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程只是一个进程中的不同的执行路径;
  3. 进程是执行的程序,线程是进程内的控制流。

知识延伸
进程是现代分时操作系统的工作单元,进程是执行的程序,这是一种非正式的说法。进程不只是程序代码,进程还包括当前活动,如程序计数器、进程堆栈和数据段。

操作系统内的每个进程表示,采用进程控制块(Process Control Block,PCB),它包含了许多与某个特定进程相关的信息:

  • 进程状态:状态可以包括新的、就绪、运行、等待、停止等。
  • 程序计数器:计数器表示进程将要执行的下个指令的地址。
  • CPU 寄存器:根据计算机体系结构的不同,寄存器的类型和数量也会不同。它们包括累加器、索引寄- 存器、堆栈指针、通用寄存器和其他条件码信息寄存器。在发生中断时,这些状态信息与程序计数器一起需要保存,以便进程以后能正确的继续执行。
  • CPU 调度信息:这类信息包括进程优先级、调度队列的指针和其他调度参数。
    内存管理信息:根据操作系统使用的内存系统,这类信息可以包括基地址和界限寄存器的值、页表或段表。
  • 记账信息:这类信息包括 CPU 时间、实际使用时间、时间期限、记账数据、作业或进程数量等。
  • I/O 状态信息:这类信息包括分配给进程的 I/O 设备列表、打开文件列表等。

PCB 结构如下图:
【Android -- 面试】操作系统 & 计算机网络_套接字
每个线程是 CPU 使用的一个基本单元;它包括线程 ID、程序计数器、寄存器组和堆栈。。它与同一进程的其他线程共享代码段、数据段和其他操作系统资源,如打开文件和信号。每个传统或重量级进程只有单个控制线程,如果一个进程具有多个控制线程,那么它能同时执行多个任务,下图说明了传统单线程进程和多线程进程的差异:
【Android -- 面试】操作系统 & 计算机网络_数据_02
对于多处理器体系结构,多线程的优点更大,因为线程可以在多处理核上并行运行。不管有多少可用 CPU,单线程进程只能运行在一个 CPU 上。

扩展阅读
知乎上有一个对于 “线程和进程的区别” 的回答非常好,总结一句话是:

进程和线程都是一个时间段的描述,是 CPU 工作时间段的描述,只是颗粒不同罢了。

原文地址如下:​​线程和进程的区别是什么?​

进程有哪几种状态

典型回答
进程在执行时会改变状态。进程状态,部分取决于进程的当前活动。每个进程可能处于以下状态:

  • 新建(New):进程正在创建
  • 运行(Running):指令正在执行
  • 等待(Waiting):进程等待发生某个事件(如 I/O 完成或收到信号)
  • 就绪(Ready):进程等待分配处理器
  • 终止(Terminated):进程完成执行

这些状态名称比较随意,而且随着操作系统的不同而有所不同。不过,它们表示的状态在所有操作系统上都会出现。有的操作系统对进程状态定义的更细。重要的是认识到:一次只有一个进程可以在一个处理器上运行,但是许多进程可处于就绪或等待状态。下面是一个进程状态图:
【Android -- 面试】操作系统 & 计算机网络_计算机网络_03

进程调度策略

典型回答
CPU 调度是多道程序操作系统的基础。通过在进程间切换 CPU,操作系统可以使得计算机更加高效。每当 CPU 空闲时,操作系统就应从就绪队列中选择一个进程来执行。CPU 调度算法有以下几种:

  1. 先到先服务调度
  2. 最短作业优先调度
  3. 优先级调度
  4. 轮转调度
  5. 多级队列调度
  6. 多级反馈队列调度

先来先服务(FCFS)调度是最简单的调度算法,但是它会让短进程等待很长的进程。最短作业优先调度(SJF)调度可证明是最佳的,提供最短的平均等待时间,然而,SJF 的实现是很难的,因为预测下一个 CPU 执行的长度是难的。SJF 算法是通用优先级调度算法(简单分配 CPU 到具有最高优先级的进程)的一个特例。优先级和 SJF 的调度可能产生饥饿。老化技术阻止饥饿。

轮转(RR)调度更适合于分时(交互)系统。RR 调度为就绪队列的首个进程,分配 q 个时间单位,这里 q 是时间片。在 q 个时间单位之后,如果该进程还没有释放 CPU,那么它被抢占并添加到就绪队列的尾部。该算法的主要问题是选择时间片,如果时间片太大,那么 RR 调度就成了 FCFS 调度;如果时间片太小,那么由于上下文切换引起的调度开销就过大。

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

多级队列算法允许多个不同算法用于不同类型的进程。最常用的模型包括:使用 RR 调度的前台交互队列与使用 FCFS 调度的后台批处理队列。多级反馈队列允许进程在队列之间迁移。

知识延伸
对于支持线程的操作系统,操作系统实际调度的是内核级线程而非进程。

用户级线程是由线程库来管理的,而内核并不知道它们。用户级线程为了运行在 CPU 上,最终应映射到相关的内核级线程,但是这种映射可能不是直接的,可能采用轻量级进程(LWP)。

多处理器调度
对于多处理器系统,CPU 调度的一种方法是让一个处理器处理所有调度决定、I/O 处理以及其他系统活动,其他处理器只执行用户代码。这种非对称多处理很简单,因为只有一个处理器访问系统数据结构,减少了数据共享的需要。

第二种方法是使用对称多处理(SMP),即每个处理器自我调度。所有进程可能处于一个共同的就绪队列中,或每个处理器都有它自己的私有就绪进程队列。不管如何,调度这样进行:每个处理器的调度程序都检查共同就绪队列,以便选择执行一个进程。

大多数 SMP 系统试图避免将进程从一个处理器移到另一个处理器,而是试图让一个进程运行在同一个处理器上。这称为处理器的亲和性,即一个进程对它运行的处理器具有亲和性。

对于 SMP 系统,最重要的是保持所有处理器的负载平衡,以便充分利用多处理器的优点。负载平衡设法将负载平均分配到 SMP 系统的所有处理器。负载平衡通常有两种方法:推迁移和拉迁移。但是,负载平衡往往会抵消掉处理器亲和性的好处,也就是说,保持进程运行在同一个处理器上的好处是进程可以利用它在该处理器缓存内的数据。

接下来,我们讨论 Linux 和 Windows 的调度策略。重要的是要注意,我们使用的进程调度这一术语是泛指的。事实上,在讨论 Windows 系统时,采用内核线程调度,而在讨论 Linux 时,采用任务调度。

Linux 调度
完全公平调度程序(CFS)是默认的 Linux 调度算法。

Linux 系统的调度基于调度类,每个类都有一个特定的优先级。内核针对不同的调度类,采用不同的调度算法,以便满足系统与进程的需要。Linux 标准内核实现了两个调度类:采用 CFS 调度算法的默认调度类和实时调度类。

CFS 调度程序并不采用严格规则来为一个优先级分配某个长度的时间片,而是为每个任务分配一定比例的 CPU 处理时间。

Linux CFS 调度程序采用高效算法,以便选择运行下个任务。每个可运行的任务放置在红黑树上。

当一个任务变得可运行时,它被添加到树上。当一个任务变得不可运行时,它从树上删除。一般来说,得到较少处理时间的任务会偏向树的左侧;得到较多处理时间的任务会偏向树的右侧。由于红黑树是平衡的,找到最左侧结点需要 lg N 的·时间复杂度。从 CFS 调度程序角度而言,这也是具有最高优先级的任务。

Windows 调度
Windows 采用基于优先级的、抢占调度算法来调度线程。Windows 调度程序确保具有最高优先级的线程总是在运行的。用于处理调度的 Windows 内核部分称为调度程序。

调度程序采用 32 级的优先级方案,以便确定线程执行方案。

进程间通信的几种方式

典型回答
套接字 套接字为通信的端点。通过网络通信的每对进程需要使用一对套接字,即每个进程各有一个。每个套接字由一个 IP 地址和一个端口号组成。通常,套接字采用 CS 架构,服务器通过监听指定的端口,来等待特定服务。服务器在收到请求后,接受来自客户端套接字的连接,从而完成连接。

管道 管道提供了一个相对简单的进程间的相互通信,普通管道允许父进程和子进程之间的通信,而命名管道允许不相关进程之间的通信。

知识延伸
进程间通信有两种基本模型:共享内存消息传递

共享内存模型会建立起一块供协作进程共享的内存区域,进程通过向此共享区域读出或写入数据来交换信息。消息传递模型通过在协作进程间交换信息来实现通信。

下图给出了两个模型的对比:

很多系统同时实现了这两种模型。

消息传递对于交换较少数量的数据很有用,因为无需避免冲突。对于分布式系统,消息传递也比共享内存更易实现。共享内存可以快于消息传递,这是因为消息传递的实现经常采用系统调用,因此需要更多的时间以便内核介入。与此相反,共享内存系统仅在建立共享内存区域时需要系统调用;一旦建立共享内存,所有访问都可作为常规内存访问,无需借助内核。

对具有多个处理核的系统上,消息传递的性能要优于共享内存。共享内存会有高速缓存一致性问题,这是由共享数据在多个高速缓存之间迁移而引起的。随着系统处理核的日益增加,可能导致消息传递作为 IPC 的首选机制。

共享内存系统
采用共享内存的进程间通信,需要通信进程建立共享内存区域。通常,这一片共享内存区域驻留在创建共享内存段的进程地址空间内。其它希望使用这个共享内存段进行通信的进程应将其附加到自己的地址空间。回忆一下,通常操作系统试图阻止一个进程访问另一个进程的内存。共享内存需要两个或更多的进程同意取消这一限制;这样它们通过在共享区域内读出或写入来交换信息。数据的类型或位置取决于这些进程,而不是受控于操作系统。另外,进程负责确保,它们不向同一位置同时写入数据。

消息传递模型
消息传递提供一种机制,以便允许进程不必通过共享地址空间来实现通信和同步。对分布式环境(通信进程可能位于通过网络连接的不同计算机),这特别有用。

需要通信的进程应有一个方法,以便互相引用。它们可以使用直接或间接的通信。

对于直接通信,需要通信的每个进程必须明确指定通信的接受者或发送者。采用这种方案,原语 send() 和 receive() 定义如下:

send(P, message): 向进程 P 发送 message
receive(Q, message): 从进程 Q 接收 message

这种方案展示了寻址的对称性,即发送和接收进程必须指定对方,以便通信。这种方案的一个变形采用寻址的非对称性,即只要发送者指定接受者,而接受者不需要指定发送者。采用这种方案,原语 send() 和 receive() 定义如下:

send(P, message): 向进程 P 发送 message
receive(id, message): 从任何进程接收 message,这里变量 id 被设置成与其通信进程的名称

在间接通信中,通过邮箱或端口来发送和接收消息。邮箱可以抽象成一个对象,进程可以向其中存放消息,也可从中删除消息,每个邮箱都有一个唯一的标识符。一个进程可以通过多个不同邮箱与另一个进程通信,但是两个进程只有拥有一个共享邮箱时才能通信。原语 send() 和 receive() 定义如下:

send(A, message): 向邮箱 A 发送 message
receive(A, message): 从邮箱 A 接收 message

客户机 / 服务器通信
客户机 / 服务器通信的两种策略:套接字和管道。

套接字:

套接字为通信的端点。通过网络通信的每对进程需要使用一对套接字,即每个进程各有一个。每个套接字由一个 IP 地址和一个端口号组成。通常,套接字采用 CS 架构,服务器通过监听指定的端口,来等待特定服务。服务器在收到请求后,接受来自客户端套接字的连接,从而完成连接。

Java 提供三种不同类型的套接字。面向连接的 TCP 套接字用 Socket 类实现。无连接的 UDP 套接字使用 DatagramSocket 类。最后,MulticastSocket 类为 DatagramSocket 类的子类,多播套接字允许数据发送到多个接受者。

使用套接字的通信,虽然常用和高效,但是属于分布式进程之间的一种低级形式的通信。一个原因是,套接字只允许在通信线程之间交换无结构的字节流。客户机或服务器需要自己加上数据结构。

管道:

管道允许两个进程进行通信。管道是早期 UNIX 系统最早使用的一种 IPC 机制。管道为进程之间的相互通信提供了一种较为简单的方法,尽管也有一定的局限性。在实现管道时,应该考虑以下四个问题:

管道允许单向通信还是双向通信?
如果允许双向通信,它是半双工(数据在同一时间内只能按一个方向传输)的还是全双工(数据在同一时间内可在两个方向上传输)的?
通信进程之间知否有一定的关系(如父子关系)?
管道通信是否能够通过网络,还是只能在同一机器上进行?
下面就讨论两种常见类型的用于 UNIX 和 Windows 系统的管道:普通管道和命名管道。

普通管道

普通管道允许两个进程按标准的生产者和消费者方式进行通信:生产者向管道的一端(写入端)写,消费者从管道的另一端(读出端)读。因此,普通管道是单向的,只允许单向通信。如果需要双向通信,那么就要采用两个管道,而每个管道向不同方向发送数据。

在 UNIX 系统上,普通管道的创建采用函数:

pipe(int fd[])
这个函数创建了一个管道,以便通过文件描述符 int fd[] 来访问:fd[0] 为管道的读出端,而 fd[1] 为管道的写入端。Unix 将管道作为一种特殊类型的文件。因此,访问管道可以采用普通的系统调用 read() 和 write()。

普通管道只能由创建进程所访问。通常情况下,父进程创建了一个管道,并使用它与其子进程通信(该进程由 fork() 来创建)。如前面所述,子进程继承了父进程打开的文件的打开文件。由于管道是一种特殊类型的文件,因此子进程也继承了父进程的管道。

对于 Windows 系统,普通管道被称为匿名管道,它们的行为类似于 Unix 的管道:它们是单向的,通信进程之间具有父子关系。

采用普通管道的进程需要有父子关系,这意味着,这些管道只可用于同一机器的进程间通信。

命名管道

普通管道提供了一个简单的机制,允许一对进程通信。然鹅,只有当进程相互通信时,普通管道才存在。对于 Unix 和 Windows 系统,一旦进程已经完成通信并终止了,那么管道就不存在了。

命名管道提供了一个更强大的通信工具。通信可以是双向的,并且父子关系不是必需的。当建立一个命名管道后,多个进程多可用它通信。事实上,在一个典型的场景中,一个管道有多个写者。此外,当通信进程完成后,命名管道继续存在。虽然 Unix 和 Windows 系统都支持命名管道,但是实现细节具有很大不同。

对于 Unix,命名管道为 FIFO。一旦创建,它们表现为文件系统的典型文件。通过系统调用 mkfifo(),可以创建 FIFO;通过系统调用 open()、read()、write() 和 close(),可以操作 FIFO。FIFO 会一直存在,直到它被显式的从文件系统中删除。虽然 FIFO 允许双向通信,只允许半双工传输。如果数据要在两个方向上传输,那么通常使用两个 FIFO。此外,通信进程应位于同一台机器上,如果需要两个不同系统之间的通信,那么应该使用套接字。

与 UNIX 系统相比,Windows 系统的命名管道通信机制更加丰富。允许全双工通信,并且通信进程可以位于同一机器或不同机器。此外,UNIX 的 FIFO 只支持字节流的数据,而 Windows 系统允许字节流或消息流的数据。

TCP 与 UDP 的区别

【Android -- 面试】操作系统 & 计算机网络_计算机网络_04