媒体网站模版,论坛搭建 wordpress,如何查询网站二级页面流量,电子商务网站建设干货4. 进程管理
进程、线程基础知识 什么是进程 我们编写的代码只是一个存储在硬盘的静态文件#xff0c;通过编译后就会生成二进制可执行文件#xff0c;当我们运行这个可执行文件后#xff0c;它会被装载到内存中#xff0c;接着 CPU 会执行程序中的每一条指令#xff0c;…4. 进程管理
进程、线程基础知识 什么是进程 我们编写的代码只是一个存储在硬盘的静态文件通过编译后就会生成二进制可执行文件当我们运行这个可执行文件后它会被装载到内存中接着 CPU 会执行程序中的每一条指令那么这个运行中的程序就被称为「进程」Process。 并行和并发 当进程要从硬盘读取数据时CPU 不需要阻塞等待数据的返回而是去执行另外的进程。当硬盘数据返回时CPU 会收到个中断于是 CPU 再继续运行这个进程。这种多个程序、交替执行的思想就有 CPU 管理多个进程的初步想法。 对于一个支持多进程的系统CPU 会从一个进程快速切换至另一个进程其间每个进程各运行几十或几百个毫秒。 虽然单核的 CPU 在某一个瞬间只能运行一个进程。但在 1 秒钟期间它可能会运行多个进程这样就产生并行的错觉实际上这是并发在一个CPU核心上运行多个任务。而并行是在多个CPU核心上运行同时多个任务。 进程的状态 运行状态Running该时刻进程占用 CPU就绪状态Ready可运行由于其他进程处于运行状态而暂时停止运行阻塞状态Blocked该进程正在等待某一事件发生如等待输入/输出操作的完成而暂时停止运行这时即使给它CPU控制权它也无法运行 还有另外两个基本状态 创建状态new进程正在被创建时的状态结束状态Exit进程正在从系统中消失时的状态 进程的状态变迁 NULL - 创建状态一个新进程被创建时的第一个状态 创建状态 - 就绪状态当进程被创建完成并初始化后一切就绪准备运行时变为就绪状态这个过程是很快的 就绪态 - 运行状态处于就绪状态的进程被操作系统的进程调度器选中后就分配给 CPU 正式运行该进程 运行状态 - 结束状态当进程已经运行完成或出错时会被操作系统作结束状态处理 运行状态 - 就绪状态处于运行状态的进程在运行过程中由于分配给它的运行时间片用完操作系统会把该进程变为就绪态接着从就绪态选中另外一个进程运行 运行状态 - 阻塞状态当进程请求某个事件且必须等待时例如请求 I/O 事件 阻塞状态 - 就绪状态当进程要等待的事件完成时它从阻塞状态变到就绪状态 在虚拟内存管理中通常会把阻塞状态的进程的物理内存空间换出到硬盘等需要再次运行的时候再从硬盘换入到物理内存。 那么就需要一个新的状态来描述进程没有占用实际的物理内存空间的情况这个状态就是挂起状态。这跟阻塞状态是不一样阻塞状态是等待某个事件的返回。 另外挂起状态可以分为两种 阻塞挂起状态进程在外存硬盘并等待某个事件的出现 就绪挂起状态进程在外存硬盘但只要进入内存即刻立刻运行 进程的控制结构 在操作系统中是用进程控制块process control blockPCB数据结构来描述进程的。PCB 是进程存在的唯一标识这意味着一个进程的存在必然会有一个 PCB如果进程消失了那么 PCB 也会随之消失。 PCB具体包含的信息 进程描述信息 进程标识符标识各个进程每个进程都有一个并且唯一的标识符用户标识符进程归属的用户用户标识符主要为共享和保护服务 进程控制和管理信息 进程当前状态如 new、ready、running、waiting 或 blocked 等进程优先级进程抢占 CPU 时的优先级 资源分配清单 有关内存地址空间或虚拟地址空间的信息所打开文件的列表和所使用的 I/O 设备信息。 CPU 相关信息 CPU 中各个寄存器的值当进程被切换时CPU 的状态信息都会被保存在相应的 PCB 中以便进程重新执行时能从断点处继续执行。 PCB是如何组织的 通常是通过链表的方式进行组织把具有相同状态的进程链在一起组成各种队列。比如 将所有处于就绪状态的进程链在一起称为就绪队列把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列另外对于运行队列在单核 CPU 系统中则只有一个运行指针了因为单核 CPU 在某个时间只能运行一个程序。 进程的控制 01 创建进程 操作系统允许一个进程创建另一个进程而且允许子进程继承父进程所拥有的资源。 创建进程的过程如下 申请一个空白的 PCB并向 PCB 中填写一些控制和管理进程的信息比如进程的唯一标识等为该进程分配运行时所必需的资源比如内存资源将 PCB 插入到就绪队列等待被调度运行 02 终止进程 进程可以有 3 种终止方式正常结束、异常结束以及外界干预信号 kill 掉。 当子进程被终止时其在父进程处继承的资源应当还给父进程。而当父进程被终止时该父进程的子进程就变为孤儿进程会被 1 号进程收养并由 1 号进程对它们完成状态收集工作。 终止进程的过程如下 查找需要终止的进程的 PCB如果处于执行状态则立即终止该进程的执行然后将 CPU 资源分配给其他进程如果其还有子进程则应将该进程的子进程交给 1 号进程接管将该进程所拥有的全部资源都归还给操作系统将其从 PCB 所在队列中删除 03 阻塞进程 当进程需要等待某一事件完成时它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待它只能由另一个进程唤醒。 阻塞进程的过程如下 找到将要被阻塞进程标识号对应的 PCB如果该进程为运行状态则保护其现场将其状态转为阻塞状态停止运行将该 PCB 插入到阻塞队列中去 04 唤醒进程 进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成所以处于阻塞状态的进程是绝对不可能叫醒自己的。 如果某进程正在等待 I/O 事件需由别的进程发消息给它则只有当该进程所期待的事件出现时才由发现者进程用唤醒语句叫醒它。 唤醒进程的过程如下 在该事件的阻塞队列中找到相应进程的 PCB将其从阻塞队列中移出并置其状态为就绪状态把该 PCB 插入到就绪队列中等待调度程序调度 进程的阻塞和唤醒是一对功能相反的语句如果某个进程调用了阻塞语句则必有一个与之对应的唤醒语句。 CPU上下文切换 在每个任务运行前CPU 需要知道任务从哪里加载又从哪里开始运行。操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器。CPU 寄存器和程序计数是 CPU 在运行任何任务前所必须依赖的环境这些环境就叫做 CPU 上下文。 CPU 上下文切换就是先把前一个任务的 CPU 上下文CPU 寄存器和程序计数器保存起来然后加载新任务的上下文到这些寄存器和程序计数器最后再跳转到程序计数器所指的新位置运行新任务。 系统内核会存储保持下来的上下文信息当此任务再次被分配给 CPU 运行时CPU 会重新加载这些上下文这样就能保证任务原来的状态不受影响让任务看起来还是连续运行。 进程的上下文切换 进程是由内核管理和调度的所以进程的切换只能发生在内核态。 所以进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源还包括了内核堆栈、寄存器等内核空间的资源。 通常会把交换的信息保存在进程的 PCB当要运行另外一个进程的时候我们需要从这个进程的 PCB 取出上下文然后恢复到 CPU 中这使得这个进程可以继续执行。 发生进程上下文切换有哪些场景 为了保证所有进程可以得到公平调度CPU 时间被划分为一段段的时间片这些时间片再被轮流分配给各个进程。这样当某个进程的时间片耗尽了进程就从运行状态变为就绪状态系统从就绪队列选择另外一个进程运行 进程在系统资源不足比如内存不足时要等到资源满足后才可以运行这个时候进程也会被挂起并由系统调度其他进程运行 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时自然也会重新调度 当有优先级更高的进程运行时为了保证高优先级进程的运行当前进程会被挂起由高优先级进程来运行 发生硬件中断时CPU 上的进程会被中断挂起转而执行内核中的中断服务程序 线程 **线程是进程当中的一条执行流程。**同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源但每个线程各自都有一套独立的寄存器和栈这样可以确保线程的控制流是相对独立的。 线程的优点 一个进程中可以同时存在多个线程 各个线程之间可以并发执行 各个线程之间可以共享地址空间和文件等资源 线程的缺点 当进程中的一个线程崩溃时会导致其所属进程的所有线程崩溃这里是针对 C/C 语言Java语言中的线程奔溃不会造成进程崩溃。 进程与线程的比较 进程是资源包括内存、打开的文件等分配的单位线程是 CPU 调度的单位进程拥有一个完整的资源平台而线程只独享必不可少的资源如寄存器和栈线程同样具有就绪、阻塞、执行三种基本状态同样具有状态之间的转换关系线程能减少并发执行的时间和空间开销 对于线程相比进程能减少开销体现在 线程的创建时间比进程快因为进程在创建的过程中还需要资源管理信息比如内存管理信息、文件管理信息而线程在创建的过程中不会涉及这些资源管理信息而是共享它们线程的终止时间比进程快因为线程释放的资源相比进程少很多同一个进程内的线程切换比进程切换快因为线程具有相同的地址空间虚拟内存共享这意味着同一个进程的线程都具有同一个页表那么在切换的时候不需要切换页表。而对于进程之间的切换切换的时候要把页表给切换掉而页表的切换过程开销是比较大的由于同一进程的各线程间共享内存和文件资源那么在线程之间数据传递的时候就不需要经过内核了这就使得线程之间的数据交互效率更高了 所以不管是时间效率还是空间效率线程比进程都要高。 线程的上下文切换 线程是调度的基本单位而进程则是资源拥有的基本单位。所以所谓操作系统的任务调度实际上的调度对象是线程而进程只是给线程提供了虚拟内存、全局变量等资源。 对于线程和进程我们可以这么理解 当进程只有一个线程时可以认为进程就等于线程 当进程拥有多个线程时这些线程会共享相同的虚拟内存和全局变量等资源这些资源在上下文切换时是不需要修改的 另外线程也有自己的私有数据比如栈和寄存器等这些在上下文切换时也是需要保存的。 所以线程的上下文切换的是什么这还得看线程是不是属于同一个进程 当两个线程不是属于同一个进程则切换的过程就跟进程上下文切换一样 当两个线程是属于同一个进程因为虚拟内存是共享的所以在切换时虚拟内存这些资源就保持不动只需要切换线程的私有数据、寄存器等不共享的数据 线程的实现 用户线程*User Thread*在用户空间实现的线程不是由内核管理的线程是由用户态的线程库来完成线程的管理 用户线程是基于用户态的线程管理库来实现的那么线程控制块*Thread Control Block, TCB* 也是在库里面来实现的对于操作系统而言是看不到这个 TCB 的它只能看到整个进程的 PCB。 所以用户线程的整个线程管理和调度操作系统是不直接参与的而是由用户级线程库函数来完成线程的管理包括线程的创建、终止、同步和调度等。 内核线程*Kernel Thread*在内核中实现的线程是由内核管理的线程 内核线程是由操作系统管理的线程对应的 TCB 自然是放在操作系统里的这样线程的创建、终止和管理都是由操作系统负责。 轻量级进程*LightWeight Process*在内核中来支持用户线程 轻量级进程*Light-weight processLWP*是内核支持的用户线程一个进程可有一个或多个 LWP每个 LWP 是跟内核线程一对一映射的也就是 LWP 都是由一个内核线程支持而且 LWP 是由内核管理并像普通进程一样被调度。 在大多数系统中LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。一般来说一个进程代表程序的一个实例而 LWP 代表程序的执行线程因为一个执行线程不像进程那样需要那么多状态信息所以 LWP 也不带有这样的信息。 调度 进程都希望自己能够占用 CPU 进行工作那么这涉及到前面说过的进程上下文切换。 一旦操作系统把进程切换到运行状态也就意味着该进程占用着 CPU 在执行但是当操作系统把进程切换到其他状态时那就不能在 CPU 中执行了于是操作系统会选择下一个要运行的进程。 选择一个进程运行这一功能是在操作系统中完成的通常称为调度程序scheduler。 调度时机 在进程的生命周期中当进程从一个运行状态到另外一状态变化的时候其实会触发一次调度。 另外如果硬件时钟提供某个频率的周期性中断那么可以根据如何处理时钟中断 把调度算法分为两类 非抢占式调度算法挑选一个进程然后让该进程运行直到被阻塞或者直到该进程退出才会调用另外一个进程也就是说不会理时钟中断这个事情。抢占式调度算法挑选一个进程然后让该进程只运行某段时间如果在该时段结束时该进程仍然在运行时则会把它挂起接着调度程序从就绪队列挑选另外一个进程。这种抢占式调度处理需要在时间间隔的末端发生时钟中断以便把 CPU 控制返回给调度程序进行调度也就是常说的时间片机制。 调度原则 CPU 利用率调度程序应确保 CPU 是始终匆忙的状态这可提高 CPU 的利用率系统吞吐量吞吐量表示的是单位时间内 CPU 完成进程的数量长作业的进程会占用较长的 CPU 资源因此会降低吞吐量相反短作业的进程会提升系统吞吐量周转时间周转时间是进程运行阻塞时间等待时间的总和一个进程的周转时间越小越好等待时间这个等待时间不是阻塞状态的时间而是进程处于就绪队列的时间等待的时间越长用户越不满意响应时间用户提交请求到系统第一次产生响应所花费的时间在交互式系统中响应时间是衡量调度算法好坏的主要标准。 调度算法 不同的调度算法适用的场景也是不同的。接下来说说在单核 CPU 系统中常见的调度算法。 先来先服务调度算法(First Come First Serve, FCFS) 顾名思义先来后到**每次从就绪队列选择最先进入队列的进程然后一直运行直到进程退出或被阻塞才会继续从队列中选择第一个进程接着运行。**但是当一个长作业先运行了那么后面的短作业等待的时间就会很长不利于短作业。 最短作业优先调度算法 最短作业优先*Shortest Job First, SJF*调度算法同样也是顾名思义它会优先选择运行时间最短的进程来运行这有助于提高系统的吞吐量。这显然对长作业不利很容易造成一种极端现象。 高响应比优先调度算法 高响应比优先 *Highest Response Ratio Next, HRRN*调度算法主要是权衡了短作业和长作业。每次进行进程调度时先计算「响应比优先级」然后把「响应比优先级」最高的进程投入运行。 时间片轮转调度算法 最古老、最简单、最公平且使用最广的算法就是时间片轮转*Round Robin, RR*调度算法。每个进程被分配一个时间段称为时间片*Quantum*即允许该进程在该时间段中运行。 如果时间片用完进程还在运行那么将会把此进程从 CPU 释放出来并把 CPU 分配给另外一个进程如果该进程在时间片结束前阻塞或结束则 CPU 立即进行切换 另外时间片的长度就是一个很关键的点 如果时间片设得太短会导致过多的进程上下文切换降低了 CPU 效率 如果设得太长又可能引起对短作业进程的响应时间变长。 最高优先级调度算法 调度程序能从就绪队列中选择最高优先级的进程进行运行这称为最高优先级*Highest Priority FirstHPF*调度算法。 进程的优先级可以分为静态优先级和动态优先级 静态优先级创建进程时候就已经确定了优先级了然后整个运行时间优先级都不会变化动态优先级根据进程的动态变化调整优先级比如如果进程运行时间增加则降低其优先级如果进程等待时间就绪队列的等待时间增加则升高其优先级也就是随着时间的推移增加等待进程的优先级。 该算法也有两种处理优先级高的方法非抢占式和抢占式 非抢占式当就绪队列中出现优先级高的进程运行完当前进程再选择优先级高的进程。抢占式当就绪队列中出现优先级高的进程当前进程挂起调度优先级高的进程运行。 多级反馈队列调度算法 多级反馈队列*Multilevel Feedback Queue*调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。 顾名思义 「多级」表示有多个队列每个队列优先级从高到低同时优先级越高时间片越短。「反馈」表示如果有新的进程加入优先级高的队列时立刻停止当前正在运行的进程转而去运行优先级高的队列 对于短作业可能可以在第一级队列很快被处理完。对于长作业如果在第一级队列处理不完可以移入下次队列等待被执行虽然等待的时间变长了但是运行时间也变更长了所以该算法很好的兼顾了长短作业同时有较好的响应时间。
进程间有哪些通信方式
由于每个进程的用户空间都是独立的不能相互访问这时就需要借助内核空间来实现进程间通信原因很简单每个进程都是共享一个内核空间。 管道 Linux 内核提供了不少进程间通信的方式其中最简单的方式就是管道管道分为「匿名管道」和「命名管道」。 $ ps auxf | grep mysql「|」竖线就是一个管道它的功能是将前一个命令ps auxf的输出作为后一个命令grep mysql的输入从这功能描述可以看出管道传输数据是单向的如果想相互通信我们需要创建两个管道才行。这种就是匿名管道。 匿名管道顾名思义它没有名字标识匿名管道是特殊文件只存在于内存没有存在于文件系统中shell 命令中的「|」竖线就是匿名管道通信的数据是无格式的流并且大小受限通信的方式是单向的数据只能在一个方向上流动如果要双向通信需要创建两个管道再来匿名管道是只能用于存在父子关系的进程间通信匿名管道的生命周期随着进程创建而建立随着进程终止而消失。 命名管道突破了匿名管道只能在亲缘关系进程间的通信限制因为使用命名管道的前提需要在文件系统创建一个类型为 p 的设备文件那么毫无关系的进程就可以通过这个设备文件进行通信。另外不管是匿名管道还是命名管道进程写入的数据都是缓存在内核中另一个进程读取数据时候自然也是从内核中获取同时通信数据都遵循先进先出原则不支持 lseek 之类的文件定位操作。 所谓的管道就是内核里面的一串缓存。从管道的一段写入的数据实际上是缓存在内核中的另一端读取也就是从内核中读取这段数据。另外管道传输的数据是无格式的流且大小受限。 消息队列 消息队列克服了管道通信的数据是无格式的字节流并且通信效率低的问题消息队列实际上是保存在内核的「消息链表」消息队列的消息体可以是用户自定义的数据类型发送数据时会被分成一个一个独立的消息体当然接收数据时也要与发送方发送的消息体的数据类型保持一致这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。 共享内存 共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销它直接分配一个共享空间每个进程都可以直接访问就像访问进程自己的空间一样快捷方便不需要陷入内核态或者系统调用大大提高了通信的速度享有最快的进程间通信方式之名。但是便捷高效的共享内存通信带来新的问题多进程竞争同个共享资源会造成数据的错乱。 信号量 那么就需要信号量来保护共享资源以确保任何时刻只能有一个进程访问共享资源这种方式就是互斥访问。信号量不仅可以实现访问的互斥性还可以实现进程间的同步信号量其实是一个计数器表示的是资源个数其值可以通过两个原子操作来控制分别是 P 操作和 V 操作。 信号 信号是异步通信机制信号可以在应用进程和内核之间直接交互内核也可以利用信号来通知用户空间的进程发生了哪些系统事件对于异常情况下的工作模式就需要用「信号」的方式来通知进程。 信号事件的来源主要有硬件来源如键盘 CltrC 和软件来源如 kill 命令一旦有信号发生**进程有三种方式响应信号 **。 执行默认操作 Linux 对每种信号都规定了默认操作例如 SIGTERM 信号就是终止进程的意思 捕捉信号 我们可以为信号定义一个信号处理函数。当信号发生时我们就执行相应的信号处理函数。 忽略信号 当我们不希望处理某些信号的时候就可以忽略该信号不做任何处理。有两个信号是应用进程无法捕捉和忽略的即 SIGKILL 和 SEGSTOP它们用于在任何时候中断或结束某一进程。 Socket 前面说到的通信机制都是工作于同一台主机如果要与不同主机的进程间通信那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信还可以用于本地主机进程间通信可根据创建 Socket 的类型不同分为三种常见的通信方式一个是基于 TCP 协议的通信方式一个是基于 UDP 协议的通信方式一个是本地进程间通信方式。
以上就是进程间通信的主要机制了。你可能会问了那线程通信间的方式呢
同个进程下的线程之间都是共享进程的资源只要是共享变量都可以做到线程间通信比如全局变量所以对于线程间关注的不是通信方式而是关注多线程竞争共享资源的问题信号量也同样可以在线程间实现互斥与同步
互斥的方式可保证任意时刻只有一个线程访问共享资源同步的方式可保证线程 A 应在线程 B 之前执行
字符串
题目关键点541. 反转字符串 II - 力扣LeetCode分组反转剑指 Offer 05. 替换空格 - 力扣LeetCode倒序替换151. 反转字符串中的单词 - 力扣LeetCode按照三个步骤进行反转剑指 Offer 58 - II. 左旋转字符串 - 力扣LeetCode先反转前n个再反转后n个最后一起反转541. 反转字符串 II - 力扣LeetCode k个为一组进行反转不足k个反转到len。每次向前2 * k的位置。
class Solution {public String reverseStr(String s, int k) {char [] c s.toCharArray();int len s.length() - 1;for(int i 0 ; i len ; i 2 * k){int start i;int end start k - 1 len ? len : start k - 1;while(start end){char temp c[start];c[start] c[end];c[end] temp;end -- ;start ;}}return new String(c);}}剑指 Offer 05. 替换空格 - 力扣LeetCode 扩充数组大小倒序替换降低时间复杂度。 class Solution {public String replaceSpace(String s) {StringBuilder sb new StringBuilder();for(int i 0 ; i s.length() ; i ){if( s.charAt(i)){sb.append( );}}if(sb.length() 0){return s;}int left s.length() - 1;s sb.toString();int right s.length() - 1;char ch [] s.toCharArray();while(left 0){if(ch[left] ){ch[right -- ] 0;ch[right -- ] 2;ch[right] %;}else{ch[right] ch[left];}left --;right--;}return new String(ch);}
}151. 反转字符串中的单词 - 力扣LeetCode 先翻转整个数组再翻转单个单词清除多余空格 class Solution {StringBuffer sb new StringBuffer();public String reverseWords(String s) {sb removeSpace(s);reverseString(sb , 0 , sb.length() - 1);reverseWord(sb);return sb.toString();}StringBuffer removeSpace(String s){char ch [] s.toCharArray();int end s.length() - 1;int start 0;while(ch[start] ) start ;while(ch[end] ) end --;for(int i start ; i end ; i){char c ch[i];if(c ! || ch[i - 1] ! ){sb.append(c);}}return sb;}void reverseString(StringBuffer s , int left , int right){while(left right){char temp s.charAt(left);s.setCharAt( left , s.charAt(right) );s.setCharAt( right , temp);right --;left;}}void reverseWord(StringBuffer sb){int start 0 ;int end start 1;int n sb.length();while(start n){while(end n sb.charAt(end) ! ){end ;}reverseString(sb , start , end - 1);start end 1;end start 1;}}
}剑指 Offer 58 - II. 左旋转字符串 - 力扣LeetCode 要求不使用额外空间所以通过以下三步就可以实现字符串左旋 反转区间为前n的子串反转区间为n到末尾的子串反转整个字符串 class Solution {public String reverseLeftWords(String s, int n) {char ch [] s.toCharArray();int start 0 ;int end n - 1;//反转前n个reverse(ch ,start , end);start n;end s.length() - 1;//反转n到最后一个reverse(ch , start , end);//全部反转reverse(ch , 0 , s.length() - 1);return new String(ch);}void reverse(char [] ch , int start , int end){while(start end){char temp ch[start];ch[start] ch[end];ch[end] temp;start ;end --;}}
}AOF持久化是怎么实现的
Redis每执行一条写命令就把该命令以追加的方式写入到文件中当重启Redis的时候先读取文件中的命令之后执行它就相当于恢复了缓存数据。这种方式就是AOF中的持久化功能。AOF日志中只会记录写操作命令不会记录读操作命令。
AOF日志写入全过程 Redis 执行完写操作命令后会将命令追加到 server.aof_buf 缓冲区然后通过 write() 系统调用将 aof_buf 缓冲区的数据写入到 AOF 文件此时数据并没有写入到硬盘而是拷贝到了内核缓冲区 page cache等待内核将数据写入硬盘具体内核缓冲区的数据什么时候写入到硬盘由内核决定。 先执行写命令然后再追加到AOF中日志的好处 第一个好处避免额外的检查开销保证记录在 AOF 日志里的命令都是可执行并且正确的。 第二个好处不会阻塞当前写操作命令的执行因为当写操作命令执行成功后才会将命令记录到 AOF 日志。 当然AOF持久化功能也有潜在的风险 第一个风险执行写操作命令和记录日志是两个过程那当 Redis 在还没来得及将命令写入到硬盘时服务器发生宕机了这个数据就会有丢失的风险。 第二个风险前面说道由于写操作命令执行成功后才记录到 AOF 日志所以不会阻塞当前写操作命令的执行但是可能会给「下一个」命令带来阻塞风险。 如果在将日志内容写入到硬盘时服务器的硬盘的 I/O 压力太大就会导致写硬盘的速度很慢进而阻塞住了也就会导致后续的命令无法执行。认真分析一下其实这两个风险都有一个共性都跟「 AOF 日志写回硬盘的时机」有关 Redis 提供了 3 种写回硬盘的策略控制的就是上面说的第三步的过程。在 redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填 Always这个单词的意思是「总是」所以它的意思是每次写操作命令执行完后同步将 AOF 日志数据写回硬盘 Always 策略的话可以最大程度保证数据不丢失但是由于它每执行一条写操作命令就同步将 AOF 内容写回硬盘所以是不可避免会影响主进程的性能 Everysec这个单词的意思是「每秒」所以它的意思是每次写操作命令执行完后先将命令写入到 AOF 文件的内核缓冲区然后每隔一秒将缓冲区里的内容写回到硬盘 Everysec 策略的话是折中的一种方式避免了 Always 策略的性能开销也比 No 策略更能避免数据丢失当然如果上一秒的写操作命令日志没有写回到硬盘发生了宕机这一秒内的数据自然也会丢失。 No意味着不由 Redis 控制写回硬盘的时机转交给操作系统控制写回的时机也就是每次写操作命令执行完后先将命令写入到 AOF 文件的内核缓冲区再由操作系统决定何时将缓冲区内容写回硬盘。 No 策略的话是交由操作系统来决定何时将 AOF 日志内容写回硬盘相比于 Always 策略性能较好但是操作系统写回硬盘的时机是不可预知的如果 AOF 日志内容没有写回硬盘一旦服务器宕机就会丢失不定数量的数据。 这 3 种写回策略都无法能完美解决「主进程阻塞」和「减少数据丢失」的问题因为两个问题是对立的偏向于一边的话就会要牺牲另外一边。 深入到源码后你就会发现这三种策略只是在控制 fsync() 函数的调用时机。当应用程序向文件写入数据时内核通常先将数据复制到内核缓冲区中然后排入队列然后由内核决定何时写入硬盘。如果想要应用程序向文件写入数据后能立马将数据同步到硬盘就可以调用 fsync() 函数这样内核就会将内核缓冲区的数据直接写入到硬盘等到硬盘写操作完成后该函数才会返回。 Always 策略就是每次写入 AOF 文件数据后就执行 fsync() 函数Everysec 策略就会创建一个异步任务来执行 fsync() 函数No 策略就是永不执行 fsync() 函数; AOF重写机制 Redis 为了避免 AOF 文件越写越大提供了 AOF 重写机制当 AOF 文件的大小超过所设定的阈值后Redis 就会启用 AOF 重写机制来压缩 AOF 文件。 AOF 重写机制是在重写时读取当前数据库中的所有键值对然后将每一个键值对用一条命令记录到「新的 AOF 文件」等到全部记录完后就将新的 AOF 文件替换掉现有的 AOF 文件。 重写机制的妙处在于尽管某个键值对被多条写命令反复修改最终也只需要根据这个「键值对」当前的最新状态然后用一条命令去记录键值对代替之前记录这个键值对的多条命令这样就减少了 AOF 文件中的命令数量。最后在重写工作完成后将新的 AOF 文件覆盖现有的 AOF 文件。 为什么不复用现有的AOF文件 如果 AOF 重写过程中失败了现有的 AOF 文件就会造成污染可能无法用于恢复使用。所以 AOF 重写过程先重写到新的 AOF 文件重写失败的话就直接删除这个文件就好不会对现有的 AOF 文件造成影响。 AOF后台重写 写入AOF日志的操作是在主进程完成的写入的内容不多不太影响性能。 但是触发AOF重写之后这时是需要读取所有缓存的键值对数据并为每个键值对生成一条命令然后将其写入到新的 AOF 文件重写完后就把现在的 AOF 文件替换掉。这个过程是很耗时的所以重写的操作不能在主进程中。 Redis 的重写 AOF 过程是由后台子进程 *bgrewriteaof* 来完成的这么做可以达到两个好处 子进程进行 AOF 重写期间主进程可以继续处理命令请求从而避免阻塞主进程 子进程带有主进程的数据副本数据副本怎么产生的后面会说这里使用子进程而不是线程因为如果是使用线程多线程之间会共享内存那么在修改共享内存数据的时候需要通过加锁来保证数据的安全而这样就会降低性能。 而使用子进程创建子进程时父子进程是共享内存数据的不过这个共享的内存只能以只读的方式而当父子进程任意一方修改了该共享内存就会发生「写时复制」于是父子进程就有了独立的数据副本就不用加锁来保证数据安全。 主进程在通过 fork 系统调用生成 bgrewriteaof 子进程时操作系统会把主进程的「页表」复制一份给子进程这个页表记录着虚拟地址和物理地址映射关系而不会复制物理内存也就是说两者的虚拟空间不同但其对应的物理空间是同一个。 这样一来子进程就共享了父进程的物理内存数据了这样能够节约物理内存资源页表对应的页表项的属性会标记该物理内存的权限为只读。 不过当父进程或者子进程在向这个内存发起写操作时CPU 就会触发写保护中断这个写保护中断是由于违反权限导致的然后操作系统会在「写保护中断处理函数」里进行物理内存的复制并重新设置其内存映射关系将父子进程的内存读写权限设置为可读写最后才会对内存进行写操作这个过程被称为「写时复制(*Copy On Write*)」。 写时复制顾名思义在发生写操作的时候操作系统才会去复制物理内存这样是为了防止 fork 创建子进程时由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。 有两个阶段会导致阻塞父进程 创建子进程的途中由于要复制父进程的页表等数据结构阻塞的时间跟页表的大小有关页表越大阻塞的时间也越长 创建完子进程后如果子进程或者父进程修改了共享数据就会发生写时复制这期间会拷贝物理内存如果内存越大自然阻塞的时间也越长 触发重写机制后主进程就会创建重写 AOF 的子进程此时父子进程共享物理内存重写子进程只会对这个内存进行只读重写 AOF 子进程会读取数据库里的所有数据并逐一把内存数据的键值对转换成一条命令再将命令记录到重写日志新的 AOF 文件。 但是子进程重写过程中主进程依然可以正常处理命令。如果此时主进程修改了已经存在 key-value就会发生写时复制注意这里只会复制主进程修改的物理内存数据没修改物理内存还是与子进程共享的。 所以如果这个阶段修改的是一个 bigkey也就是数据量比较大的 key-value 的时候这时复制的物理内存数据的过程就会比较耗时有阻塞主进程的风险。 还有个问题重写 AOF 日志过程中如果主进程修改了已经存在 key-value此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了这时要怎么办呢 为了解决这种数据不一致问题Redis 设置了一个 AOF 重写缓冲区这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。 在重写 AOF 期间当 Redis 执行完一个写命令之后它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。
也就是说在 bgrewriteaof 子进程执行 AOF 重写期间主进程需要执行以下三个工作:
执行客户端发来的命令将执行后的写命令追加到 「AOF 缓冲区」将执行后的写命令追加到 「AOF 重写缓冲区」
当子进程完成 AOF 重写工作扫描数据库中所有数据逐一把内存数据的键值对转换成一条命令再将命令记录到重写日志后会向主进程发送一条信号信号是进程间通讯的一种方式且是异步的。
主进程收到该信号后会调用一个信号处理函数该函数主要做以下工作
将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中使得新旧两个 AOF 文件所保存的数据库状态一致新的 AOF 的文件进行改名覆盖现有的 AOF 文件。
信号函数执行完后主进程就可以继续像往常一样处理命令了。
在整个 AOF 后台重写过程中除了发生写时复制会对主进程造成阻塞还有信号处理函数执行时也会对主进程造成阻塞在其他时候AOF 后台重写都不会阻塞主进程。