进程与线程

541 阅读8分钟

一、区别与联系

1.1 进程

  1. 进程的本质就是操作系统执行的一个程序。 每个进程可以有自己独立的存储空间,在许多操作系统中,与一个进程有关的所有信息,除了该进程自身地址空间的内容以外,均存放在操作系统的一张表中,称为 进程表(process table),进程表是数组或者链表结构,当前存在每个进程都要占据其中的一项。

  1. 如果一个进程能够创建一个或多个进程(称为子进程),而且这些进程又可以创建子进程,则很容易找到进程树,如下所示

  1. 操作系统启动时有一个主线程,然后读取操作系统用户表里的用户,分别为每个用户创建一个进程(如果用户没有登录系统则此进程睡眠)。用户登陆后会使用属于自己用户的进程来处理任务或者fork子进程。

1.2 线程

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

  1. 用户级线程和内核级线程之间的主要差别在于性能。用户级线程的切换需要少量的机器指令(想象一下Java程序的线程切换),而内核线程需要完整的上下文切换,修改内存映像,使高速缓存失效,这会导致了若干数量级的延迟。另一方面,在使用内核级线程时,一旦线程阻塞在 I/O 上就不需要在用户级线程中那样将整个进程挂起。\

  2. 从进程 A 的一个线程切换到进程 B 的一个线程,其消耗要远高于运行进程 A 的两个线程(涉及修改内存映像,修改高速缓存),内核对这种切换的消耗是了解到,可以通过这些信息作出决定。

1.3 进程和线程的区别

  1. 进程是系统资源分配的最小单位,线程是程序执行的最小单位;
  2. 进程使用独立的数据空间,而线程共享进程的数据空间。
  1. 线程要比进程更轻量级,由于线程更轻,所以它比进程更容易创建,也更容易撤销。在许多系统中,创建一个线程要比创建一个进程快 10 - 100 倍。
  2. 轻量级进程 (线程) 和普通进程的区别是可以共享同一内存地址空间、代码段、全局变量、同一打开文件集合。

1.4 进程/线程切换

运行态,运行态指的就是进程实际占用 CPU 时间片运行时

就绪态,就绪态指的是可运行,但因为其他进程正在运行而处于就绪状态

阻塞态,除非某种外部事件发生,否则进程不能运行\

  1. 操作系统最底层的就是调度程序,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。

1.5 线程切换代价

  1. 要保存当前线程的上下文,并加载下一个要执行线程的上下文。
  2. 如果多个线程都是 CPU 密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的 I/O 处理,拥有多个线程能在这些活动中彼此重叠进行,从而会加快应用程序的执行速度。

二、进程/线程调度

2.1 批处理中的调度

批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。

2.1.1 先来先服务

先到先得。。。 按照请求顺序为进程分配 CPU。 最基本的,会有一个就绪进程的等待队列。当第一个任务从外部进入系统时,将会立即启动并允许运行任意长的时间。它不会因为运行时间太长而中断。当其他作业进入时,排到就绪队列尾部。

2.1.2 最短作业优先

假设运行时间已知。提前给所有作业按照执行时间排序。按照最短作业优先执行。

需要注意的是,在所有的进程都可以运行的情况下,最短作业优先的算法才是最优的。

2.1.3 最短剩余时间优先

最短作业优先的可抢占版本。使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。当一个新作业到达时,其整个时间同当前进程的剩余时间做比较。如果新的进程比当前运行进程需要更少的时间,当前进程就被挂起,而运行新的进程。这种方式能够使短期作业获得良好的服务。

2.2 交互系统中的调度

交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。

2.2.1 轮询调度(时间片轮转)

用一个队列保存要执行的进程,时间片大小固定,如果时间片用完且当前进程没有执行完成,就把该进程排到最后,等待下一次时间片轮到它。

2.2.2 优先级调度

每个进程都被赋予一个优先级,优先级高的进程优先运行。 为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

2.2.3 多级反馈队列

一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要切换进程 100 次。

多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同。 比如1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。

可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

三、线程实现方式

3.1 在内核空间中实现线程

所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程。

3.2 在用户空间中实现线程

进程在自己的内存空间内维护线程表,线程的切换不需要经过系统调用。

  1. 优势:节约资源,节省上下文切换的开销。不需要对内存告诉缓存进行刷新,效率高。
  2. 缺点:无法实现阻塞系统调用。比如线程阻塞等待用户输入时,用户即使输入内核空间也无法唤醒用户空间的线程。

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

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

这就是协程!!!

四、Linux的进程间通信(IPC)

4.1 管道(Pipe)

  1. 在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。 管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。

  2. shell 中的 管线 pipelines 就是用管道实现的,当 shell 发现输出 'sort <f | head' ,它会创建两个进程,一个是 sort,一个是 head,sort,会在这两个应用程序之间建立一个管道使得 sort 进程的标准输出作为 head 程序的标准输入。sort 进程产生的输出就不用写到文件中了,如果管道满了系统会停止 sort 以等待 head 读出数据。

4.2 消息队列(MessageQueue)

消息队列是用来描述内核寻址空间内的内部链接列表。可以按几种不同的方式将消息按顺序发送到队列并从队列中检索消息。每个消息队列由 IPC 标识符唯一标识。消息队列有两种模式,一种是 严格模式, 严格模式就像是 FIFO 先入先出队列似的,消息顺序发送,顺序读取。还有一种模式是 非严格模式,消息的顺序性不是非常重要。

4.3 共享内存

两个进程之间还可以通过共享内存进行进程间通信,其中两个或者多个进程可以访问公共内存空间。两个进程的共享工作是通过共享内存完成的,一个进程所作的修改可以对另一个进程可见(很像线程间的通信)。

4.4 Signal

通过向一个或多个进程发送 异步事件信号 来实现,信号可以从键盘或者访问不存在的位置等地方产生;信号通过 shell 将任务发送给子进程。 你可以在 Linux 系统上输入 kill -l 来列出系统使用的信号,下面是我提供的一些信号

  1. 进程可以选择忽略发送过来的信号,但是有两个是不能忽略的:SIGSTOP 和 SIGKILL 信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操作,SIGKILL 信号会通知当前进程应该被杀死。
  1. 除此之外,进程可以选择它想要处理的信号,进程也可以选择阻止信号,如果不阻止,可以选择自行处理,也可以选择进行内核处理。
  • SIGINT

当用户希望中断进程时,操作系统会向进程发送 SIGINT 信号。用户输入 ctrl - c 就是希望中断进程。

4.5 先入先出队列 FIFO

先入先出队列 FIFO 通常被称为 命名管道(Named Pipes),命名管道的工作方式与常规管道非常相似,但是确实有一些明显的区别。未命名的管道没有备份文件:操作系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具有支持文件和独特 API ,命名管道在文件系统中作为设备的专用文件存在。当所有的进程通信完成后,命名管道将保留在文件系统中以备后用。命名管道具有严格的 FIFO 行为

4.6 UnixSocket

还有一种管理两个进程间通信的是使用 socket,socket 提供端到端的双相通信。一个套接字可以与一个或多个进程关联。就像管道有命令管道和未命名管道一样,套接字也有两种模式,套接字一般用于两个进程之间的网络通信,网络套接字需要来自诸如 TCP(传输控制协议) 或较低级别 UDP(用户数据报协议) 等基础协议的支持。


参考1:leetcode-cn.com/leetbook/re…
参考2:github.com/CyC2018/CS-…

参考3:leetcode-cn.com/leetbook/re…