《操作系统原理》学习笔记,多进程和多线程的优缺点?IPC进程间通信的方式?

操作系统原理

1.处理器

每个处理器都有自己的指令系统(指令集),处理器的物理组成如下:

  • 运算器:实现任何指令的算数和逻辑运算,是计算机计算的核心。
  • 控制器:负责控制程序运行的流程,包括取指令,维护CPU的运行状态,CPU与内存的交互。
  • 寄存器:是指令在CPU内部运算过程中存放数据、内存地址以及指令信息的存储设备,在计算机存储系统中具有最快的存储速度。
  • 高速缓存:处于CPU和物理内存之间,用户多级存储结构,均衡CPU和内存的速度,一般由控制器中的内存管理单元(MMU)管理。

2.寄存器

寄存器(register)为CPU本身提供了一定的存储能力,分为:

  • 用户可见寄存器:可由用户使用。
  • 控制和状态寄存器:用于控制处理器操作,一般由管态下的操作系统代码使用。

根据作用进行分类:

  • 数据寄存器:也称通用寄存器,主要用于各种算数逻辑指令和访存指令
  • 地址寄存器:用于存放数据/指令的物理地址、线性地址或有效地址,用于特定的某种方式的寻址,如索引、段指针、栈指针等
  • 条件码寄存器:保存CPU操作结果的各种标记位,如算数运算产生的溢出、符号等。这些标记在条件分支指令中被测试,以控制程序指令的流向
  • 程序计数器(Program Counter,PC):记录了将要取出的指令的物理地址
  • 指令寄存器(Instruction Register,IR):包含最近取出的指令
  • 程序状态码(Program Status Word,PSW):它记录了CPU运行模式信息,用以表明CPU当前的工作状态,通常包括CPU的工作状态码(管态还是目态)、条件码(反应指令执行后的结果特征)、中断屏蔽码(是否允许中断)等

3.指令处理

处理指令的最简单方式包括两个步骤:cpu先从内存中读取一条指令,然后执行,这样单条指令的处理过程称为一个“指令周期”,程序的执行就是由许多指令周期组成。

典型的处理器中,处理器依据在PC中保存的指令地址,从内存中取出一条指令,并在取指令完成后根据指令类别自动将PC的值改为下一条指令,指令存放在指令寄存器,CPU将解释执行。

  • 访问内存指令:负责CPU和内存之间的数据传输
  • I/O指令:负责CPU和I/O模块之间的数据传送和命令发送
  • 算数逻辑指令:又称数据处理指令,用以执行有关数据的算术和逻辑操作
  • 控制转移指令:这种指令可以指定一个新的指令执行起点
  • CPU控制指令:用以修改处理器状态、改变CPU工作方式等

特权指令是指在指令系统中那些只能由操作系统使用的指令,多数系统将CPU的工作状分为管态和目态。前者一般指OS管理程序运行的状态,具有较高的特权级别,又称系统态、特权态,后者一般指用户程序运行时的状态,又称普通态、用户态。

CPU状态是动态改变的,状态切换可通过特权指令直接设置PSW。

当在用户态执行特权指令时,CPU将拒绝执行该指令,并形成一个“非法事件”的操作。中断机制识别该事件后,转交给操作系统处理。

4.关于进程

进程是具有一定独立功能的程序,关于某个数据集合上的一次运行活动。进程是系统进行资源分配和调度的一个独立单位,从OS的角度可将进程分为系统进程(执行操作系统程序,完成OS的某些功能)和用户程序运行后的用户进程(优先级低于系统进程)

进程的组成大致如下:

  • 指令(可执行代码)
  • 数据
  • PCB

PCB是一个用于描述进程基本情况,以及进程运行变化过程的数据结构。它是进程存在的唯一标志,当系统创建进程时,为进程设置PCB,再利用PCB对进程进行控制和管理。撤销进程时,系统回收PCB,进程随之消亡。PCB包含的数据内容可分为:

  • 调度信息:供进程调度时使用,描述进程的当前状态,包括进程名、进程号、存储信息、优先级、当前状态、 资源清单、家族关系、消息队列指针、当前打开的文件表等。
  • 现场信息:描述了进程当前的运行情况,由于每个进程都有自己的专属内存工作区,现场信息只记录那些可能会被其它进程改变的寄存器数据(比如程序状态字、时钟、界地址寄存器、程序计数器等)
系统将所有进程的PCB排成若干队列(线性方式、索引方式、链表方式等)

5.关于线程

进程是一个可拥有资源的独立单位,同时又是一个可以独立调度和分派的基本单位。引入线程之后,进程是拥有资源的单位,线程作为运行调度单位。

每个线程拥有一个唯一的标识符和线程描述表,可执行相同程序。同进程中的线程共享该进程的内存存储空间,相互通信无需调用内核,同意进程中,线程切换不会引起进程切换,不同进程中的线程切换将会引起进程切换。

  • 用户级线程:这种线程不依赖于内核,只存在于用户态中,对它的操作不会通过系统调用来实现,内核也不知道它的存在。同时它可以在不支持线程的OS上进行实现(线程的调度由进程内的一个运行时系统进行维护)。
  • 内核级线程:这种线程依赖于内核,在内核中保留了线程控制块和所有线程的线程表,通过系统调用对线程表的更新完成线程的各种操作,线程表中保存了每个线程的寄存器、状态和其它信息(PCB的子集)

6.进程/线程模型

6.1 三状态模型

“三状态进程模型”下的进程分为运行、等待、就绪三种状态:

  • 运行状态:指进程已获得CPU,并且在CPU上执行的状态
  • 就绪状态(Ready):指进程已经具备运行条件,但由于没有获得CPU而不能运行的状态
  • 等待状态(Wait):指进程等待某种事件发生,而暂时不能运行的状态。

6.2 五状态模型

“五状态模型”分为创建、运行、就绪、阻塞、结束五种状态。

IPC,进程间通信

参考:https://ost.51cto.com/posts/3330

进程通信( InterProcess Communication,IPC)就是指进程之间的信息交换。常见的进程通信机制:

  • 管道(也称作共享文件)
  • 消息队列(也称作消息传递)
  • 共享内存(也称作共享存储)
  • 信号量和 PV 操作
  • 信号
  • 套接字(Socket)
    《操作系统原理》学习笔记,多进程和多线程的优缺点?IPC进程间通信的方式?
    IPC进程通信

1.匿名管道

Linux 管道使用竖线 | 连接多个命令,这被称为管道符。

$ command1 | command2 

以上这行代码就组成了一个管道,它的功能是将前一个命令(command1)的输出,作为后一个命令(command2)的输入。管道中的数据只能单向流动,也就是半双工通信,如果想实现相互通信(全双工通信),则需要创建两个管道。

另外,通过管道符 | 创建的管道是匿名管道,用完了就会被自动销毁。并且,匿名管道只能在具有亲缘关系(父子进程)的进程间使用。也就是说,匿名管道只能用于父子进程之间的通信。

在 Linux 的实际编码中,是通过 pipe 函数来创建匿名管道的,若创建成功则返回 0,创建失败就返回 -1:

// 该函数拥有一个存储空间为 2 的文件描述符数组:
// fd[0] 指向管道的读端,fd[1] 指向管道的写端
// fd[1] 的输出是 fd[0] 的输入

int pipe (int fd[2]);

对于管道两端的进程而言,管道就是一个文件(管道也被称为共享文件机制),但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。

管道的本质就是内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作。

2.有名管道

匿名管道由于没有名字,只能用于父子进程间的通信。为了克服这个缺点,提出了有名管道,也称做 FIFO,因为数据是先进先出的传输方式。

所谓有名管道也就是提供一个路径名与之关联,这样,即使与创建有名管道的进程不存在亲缘关系的进程,只要可以访问该路径,就能够通过这个有名管道进行相互通信。

# 使用 Linux 命令 mkfifo 来创建有名管道:
$ mkfifo myPipe

# myPipe 就是这个管道的名称,往 myPipe 这个有名管道中写入数据:
$ echo "hello" > myPipe

# 执行这行命令后,程序就阻塞了,因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。
执行另外一个命令来读取这个有名管道里的数据
$ cat < myPipe
hello 

3.共享内存

共享内存就是允许不相干的进程将同一段物理内存连接到它们各自的地址空间中,使得这些进程可以访问同一个物理内存,这个物理内存就成为共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

《操作系统原理》学习笔记,多进程和多线程的优缺点?IPC进程间通信的方式?
共享内存

4.Socket

Socket 起源于 Unix,原意是插座,在计算机通信领域,Socket 被翻译为套接字,它是计算机之间进行通信的一种约定或一种方式。通过 Socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

思考点

1.场景

  • 简单的异步任务执行可以直接通过管道向子进程投递异步任务,然后触发事件执行任务。
  • Redis的订阅与发布也可以实现上述的功能,但它的优势在于它可以一对多,PUBLISH之后可以同时触发多个订阅的事件,并且可以在任意进程内进行订阅。
  • 专门的消息队列组件,则适用于对消息队列要求比较高的场景(消息确认、消费者组等)
  • 在大多数操作系统中,父进程和子进程通常被分配相同的 CPU 时间片。时间片是操作系统用来调度进程的一种机制,它将 CPU 时间划分为较小的块,并按照某种策略分配给不同的进程。每个进程被分配一个时间片,在该时间片结束后,操作系统会剥夺该进程的 CPU 使用权,并将其分配给其他进程。所以多进程程序获得的CPU时间片比单进程程序多。
为什么不使用管道作为消息队列的通信接口,而是使用Redis List?
处理异步事件的时候,假设只有一个子进程,可以直接使用管理进行通信,然后触发事件。但是一个进程的处理能力是有限的,所以我们往往需要多个进程同时处理异步任务。多个进程同时读取Redis队列时,由于Redis的单线程模型,所以只有一个进程会拿到这个消息

2.问题

  • Mysql一个链接的内存占用?(PHP协程创建了100个不进行操作的连接,占用内存7.8m)
  • 主从复制,数据同步延时的问题?
  • 数据量过大时,避免使用join,通过协程并发查询?
  • 将单个查询分解成多个查询,通过协程并发查询?
  • 读是共享锁,写是排它锁,考虑读写分离?
  • 创建协程,单个协程需要的内存大小(底层默认分配2M(C)虚拟内存+8K(PHP)内存(PHP-7.2或更高版本)。这里的虚拟内存是指操作系统并不会立即分配2M物理内存,系统会根据在内存实际读写时发生缺页中断,再分配实际内存)?
  • 测压过程中如何一步步排查问题所在?
  • 自定义进程的数量,消费者消费的速度、消费者被阻塞的时长?
  • 索引降维和连接池预热?
  • 数据量过大时,直接分库,通过封装中间层,来实现现有业务的无缝迁移。
  • 多个进程分配到的时间片会更多,通过多进程处理异步任务。多进程封装成一个进程组,然后通过原子计数器将任务平均派发到每一个进程。
  • 协程机制的优点之一就是有阻塞就会自动处理别的任务,比如Mysql被阻塞就去处理Redis读取,Redis被阻塞就去Mysql;

3.Swoole协程

  • 协程没有IO等待 正常执行PHP代码,不会产生执行流程切换
  • 协程遇到IO等待 立即将控制权切,待IO完成后,重新将执行流切回原来协程切出的点
  • 协程并行协程依次执行,同上一个逻辑
  • 协程嵌套执行流程由外向内逐层进入,直到发生IO,然后切到外层协程,父协程不会等待子协程结束
  • Swoole的协程在底层实现上是单线程的,因此同一时间只有一个协程在工作,协程的执行是串行的。这与线程不同,多个线程会被操作系统调度到多个CPU并行执行。
  • 一个协程正在运行时,其他协程会停止工作。当前协程执行阻塞IO操作时会挂起,底层调度器会进入事件循环。当有IO完成事件时,底层调度器恢复事件对应的协程的执行。
  • 对CPU多核的利用,仍然依赖于Swoole引擎的多进程机制。
  • Swoole遇到需要等待的IO才会切换,比如SQL查询、比如网络请求等待响应、比如读取文件等找到那个文件。
  • 从Mysql获取返回的结果集,这个需要CPU来计算,需要写内存,所以Swoole协程的单线程模型是串行来的,所以对于结果集数据量级过大的查询,效果不明显。
  • 搞明白什么时候会发生协程切换,业务无关的任务可以异步处理

4.关于队列

队列可以通过客户重要性等条件因素,进行优先级划分。优先级越高的越先处理(Redis的zSet类型数据)。

如何及时处理的区别就在于,队列可以进行中间操作,根据任务进行不同的异步处理。