文章

操作系统学习笔记

内核态和用户态的区别

在内核态下,CPU可以执行所有的指令和访问所有的硬件资源,用于运行操作系统的内核程序应用,涉及到内存管理、进程管理、设备驱动程序控制以及系统调用等;而在用户态下,CPU只能执行部分指令,无法直接访问硬件资源,用于运行用户的程序应用;

区分内核态和用户态的主要原因:

  • 首先是为了确保用户程序无法直接访问硬件资源,避免恶意程序对系统资源造成破坏,保证操作系统的安全性

  • 其次是为了避免用户程序运行出现故障导致整个操作系统的崩溃,确保操作系统的稳定性

  • 最后是为了时系统内核和用户程序有明确的边界,便于系统的模块化和维护,保证系统的隔离性

线程和进程的区别

首先,从本质上说,进程是操作系统分配资源的基本单位,而线程是操作系统调度和执行的基本单位,进程包含了线程,线程是进程下的一个执行单元

其次,从开销方面上说,每个进程都有独立的代码和数据空间,程序之间会有较大的开销;而同一类线程是共享代码和数据空间的,每个线程会有独立的运行栈和程序计数器,线程之间的切换开销较小

然后,稳定性方面,进程之间不会相互影响,比如一个进程的子进程崩溃了不会影响到父进程的运行,但是一个进程下的线程崩溃了,就可能影响整个进程甚至导致崩溃

什么是协程

协程是用户态的轻量级线程,协程拥有自己的寄存器和上下文,并且与其他协程共享堆内存,协程之间的切换开销非常小,因为协程只需要保存和恢复相关的上下文,并且不需要进行内核级的上下文切换,但是协程的调度需要由用户自己完成,编程模型相对会复杂些

多线程的优劣

多线程的优点是可以进行并发处理多个任务,充分利用多核CPU提高程序的运行效率

缺点是线程之间存在资源竞争问题,需要引入锁去保证线程安全,同时加锁会增加一定的开销,并且如果调度不当还可能会发生死锁的问题,然后过多的线程不但不会进一步运行效率,反而会因消耗过多的资源和CPU频繁地进行调度导致程序处理效率大大降低

进程间的通信

首先进程间可以通过管道的方式进行通信,Linux 中提供了匿名管道和命名管道

  • 其中匿名管道如字面意思没有名称标识,它只存在于内存,通信的数据是无格式且大小受限的,一个管道只能进行单向通信,如果要实现双向通信,则需要创建两个管道,匿名管道随进程创建而建立,随进程终止而消失,并且只能用于存在父子关系的进程间的通信;
  • 而命名管道则突破了只能在亲缘关系间的进程的通信限制,在使用命名管道前,需要在文件系统中创建设备文件,然后毫无关系的进程就可以通过这个设备文件进行通信

不管用哪个类型的管道,数据都是缓存到内核内存中,并且数据都遵循先进先出的原则

然后进程也可以通过消息队列的方式进行通信,消息队列解决了管道通信的无格式的字节流问题,用户可以自定义消息体的数据类型,消息体采用链表的数据结构保存在内核中,发送方和接收方都需要确保数据类型一致才可以保证数据的正确性,消息队列虽然可以进行有格式的数据通信,但是它的通信速度并不是最及时的,因为在每次数据的写入和读取都需要经过用户态和内核态的拷贝过程

接着进程还可以通过共享内存的方式进行通信,共享内存可以解决消息队列用户态和内核态之间拷贝过程带来的开销问题,这个方式通过直接分配一个共享内存空间,需要进行通信的进程可以直接访问,像访问自己的空间一样快捷方便,不需要陷入内核态或系统调用,从而大大提高通信速度,但是共享内存存在资源竞争问题,通过信号量保护共享资源和PV操作实现互斥访问就可以解决这个问题,

前面的通信都是在同一主机上,如果需要不同主机上的进程通信,则可以通过socket通信进行,当然本地也可以进行socket通信,常用的通信方式,一个是基于TCP协议,一个是基于UDP协议

线程间共享资源访问控制方式

线程间的共享资源是通过锁进行访问控制的,Linux 提供了互斥锁、读写锁、条件变量、自旋锁和信号量

  • 先说说互斥锁,当一个线程访问某一共享资源时,会尝试对其进行加锁,如果加锁成功,则进行后续的操作,如果当前资源已经被其他线程加了锁,则需要进行阻塞等待,直至锁被释放
  • 再说说读写锁,它由读锁和写锁两部分组成,当线程需要读取资源时加读锁,当线程需要写数据时加写锁,当资源没有被加写锁时,所有线程在读取的时候不会被阻塞,而当有线程加写锁成功后,所有线程在尝试读取或写时都会被阻塞进入等待,读写锁相比于互斥锁更适合读多写少的场景
  • 接着说下自旋锁,自旋锁的实现方式是通过一个CAS,即Campare And Swap,原子操作完成加锁的,在线程尝试为一个资源加锁时,首先会查看锁是否是空闲的,如果是则将其设置为当前线程持有,否则线程会进入一个循环忙等状态,直到线程加锁成功,自旋锁相比于互斥锁,开销会小些
  • 然后说下条件变量的方式,这个方式是通过线程间共享的全局变量结合互斥锁实现的,当线程需要改变条件变量前会先锁住互斥量,接着线程会把自己放到等待条件的线程列表中,等待其他线程使条件成立
  • 最后说下信号量,我们可以将信号量理解为资源的数量,通过PV两个原子操作进行控制,P操作消耗一个资源,V操作生产一个资源,当资源数小于0时,P操作将进入阻塞等待,直到V操作唤醒,V操作不会被阻塞

进程调度算法

先说说非抢占先来先调度算法,这个算法每次从就绪队列中选择最先入队的进程,然后一直执行,直到这个进程退出或被阻塞,才会进行下一个进程的执行,这个算法的问题在于如果一个进程进行长作业的话,后面的短作业进程需要等待较长时间,对长作业来说有利,当对短作业来说不利

然后是最短作业优先调度算法,这个算法会优先执行运行时间最短的进程,这显然对短作业有利,但对长作业不利,容易出现长作业进程进入饥饿状态

接着为了很好的权衡短作业和长作业,有了高响应比优先调度算法,每次进行调度时会先计算 等待时间与要求服务时间的和 再与 要求服务时间 相比 得到 一个响应比优先级, (公式: \(优先级 = \frac{等待时间 + 要求服务时间}{要求服务时间}\) ), 如果当等待时间相等时,要求服务时间越小,响应比越高,这样就有利于短作业被优先选择执行,当要求服务时间相等时,等待时间越大,响应比越高,兼顾了长作业进程

再而就是时间片轮转调度算法,每个进程都会被分配一个运行时间片,当这个运行时间片用完了之后,就会将这个进程从CPU中释放出来,重新再入队,然后CPU再执行下一个进程,如果这个进程出现了阻塞或提前结束,CPU 则会立即切换到下一个进程执行,这个算法需要把控时间片在一个合适的大小,过短会导致进程上下文切换过于频繁从而降低CPU效率,过长则会引起短作业进程响应时间过长

还有根据优先级进行调度的最高优先级调度算法,这个算法分静态优先级和动态优先级,采取静态优先级时,在创建进程时就确定了优先级,在之后都不会变化,采取动态优先级时,根据运行时间和等待时间进行增加和降低优先级,运行时间越长,优先级越低,等待时间越长,优先级越高,同时这个算法还分非抢占式和抢占式,在非抢占式中,只有在运行完一个进程后才能选择下一个进程执行,而在抢占式中,则当出现优先级比当前运行的更高的进程时,调度会优先执行级别更高的进程

最后是多级反馈队列调度算法,这个算法综合了前面的时间片轮转调度算法和最高优先级调度算法,多级表示有多个队列,每个队列按照优先级从高到低排序,同时优先级越高时间片越短,反馈表示如果出现了优先级更高的队列时,立刻停止当前的执行队列,转为执行级别更高的队列

死锁是如何发生的,如何避免

首先同一共享资源要不允许多个线程同时持有,其次,在一个线程持有一个共享资源下,在尝试获取一个被其他线程持有的资源时会进入等待状态,然后,一个线程在使用资源的过程中不允许被其他线程获取,最后是发生了两个线程获取资源的顺序构成了环形链,如果同时满足这四个条件就会出现死锁的情况

要避免死锁,只需要不满足前面说的任意一个条件就可以了,一个常见且可用的做法就是使用资源有序分配法,来破坏环路等待等条件,这个做法简单来说,就是两个线程都总以同样的顺序获取资源,就避免环路的出现

还有是采用银行家算法在分配资源给进程前,进行判断进程的安全性,通过判断当前剩余资源是否满足进程的最大需求,如果满足,则将进程加入到安全序列中,并将进程持有的资源回收,然后不断重复这个判断和回收的过程,看最后是否能让所有进程都加入到安全序列中,安全序列一定不会发生死锁,所以就可以有效的避免死锁的发生

什么是乐观锁、悲观锁

乐观锁是假设多个事务之间很少发生冲突,在读取数据时不会加锁,而是在更新数据时检查数据的版本是否匹配,若匹配则执行更新,否则则认为发生了冲突,乐观锁是适用于读多写少的场景,可以有效降低锁的竞争,从而提高并发性能

而悲观锁是假设多个事务之间会频繁发生冲突,在读取数据时会加上锁,以避免其他事务对数据进行修改,直到当前事务完成操作后才释放,悲观锁适用于写多的场景,可以保证数据的一致性,避免发生数据冲突

使用虚拟内存有什么好处

首先,操作系统会为每个进程都分配独立的虚拟内存,使得进程不直接与物理内存交互,虚拟内存可以设置的比实际物理内存大,让进程觉得自己拥有了足够大的连续内存空间;因为程序运行符合局部性原理,CPU访问内存会有明显的重复倾向,所以对于不经常访问的内存,就可以将其换出内存,放到硬盘上的swap交换区,这样就实现了虚拟内存可以设置的比物理内存大的效果

其次,每个进程都有自己的页表,并且页表是私有的,所以进程的页表是无法相互访问的,这样就解决了多进程之间地址冲突的问题

最后,虚拟内存的页表项中,除了记录物理地址之外,还有一些特殊标记属性,比如控制某一个页的读写权限等,为操作系统提供了更好的安全性

解释下内存分页和页表

内存分页指的是将整个虚拟和物理内存空间分成一段段固定尺寸的大小,分出来的一个连续且尺寸固定的内存空间就称作为,在Linux中,一页大小通常为 4KB,

因为页与页之间是紧密排列的,所以不会产生外部碎片,由于分页机制分配的内存最小单位是一页,即使程序使用的内存空间不足一页,还是会最少分配一页空间,从而造成内存浪费,产生内部碎片

虚拟地址与物理地址之间通过页表来完成映射,页表存储在内存中,由MMU内存管理单元完成虚拟地址和物理地址的转换工作

在进行虚拟地址和物理地址的转换时,首先把虚拟地址切分为页号和页内偏移量,然后从虚拟地址中取出页号,在页表中查询得到相应的物理页号,最后,直接根据物理页号加上前面的页内偏移量就得到了实际的物理地址

解释下段表

在内存分段机制中,会将程序分成若干的逻辑段,每一段都一个段号,虚拟地址可以通过段表与物理地址进行映射,每个段号在段表上都有一个项,可以根据段号查询到一个基地址和一个偏移量,将基地址加上偏移量就可以得到实际的物理地址

栈和堆有什么区别

首先从分配方式上说,栈是静态分配,主要用于存储函数的局部变量和函数的调用信息;而堆则是动态分配,主要用于存储动态的数据结构和对象

其次从内存管理上说,栈是由编译器自动分配和释放,遵循先进后出的原则,变量的生命周期由其作用域决定,函数调用时分配内存,函数返回时释放内存;而堆则需要由程序员手动分配和释放,如果管理不当,则会出现内存泄漏或溢出,有些语言如Golang,会提供垃圾回收机制去完成对堆的内存管理,不需要程序员手动管理,大大提高了编码效率

最后从大小和速度上说,栈的大小有限,但是内存的分配和释放较,而堆的内存空间大小通常比栈要大,但是动态分配和释放需要额外的时间开销

在父进程fork子进程时,会复制哪些内容

fork阶段会复制父进程的页表,如果发生了写时复制,还会复制相应的物理内存

解释下写时复制

写时复制

在主进程fork子进程时,操作系统会为子进程复制一份父进程的页表,页表对应的物理内存的权限会被设置为只读,子进程的页表映射的物理空间和父进程的一致,这个过程不会复制物理内存,也就是说父子进程分别拥有不同的虚拟空间,但其对应的物理空间是同一个,这样一来就可以节省物理内存资源,同时也节省了复制的时间开销

如果此时父进程或子进程对这个内存发起写操作,CPU会触发写保护中断,因为违反了只读权限,然后操作系统会在写保护中断处理函数中进行物理内存的复制,并重新设置其内存的映射关系,将父子进程的内存读写权限改为可读写,最后中断返回后,才可以对内存进行写操作,这个过程就被成为写时复制

简单来说,写时复制就是只有在发生写操作的时候,操作系统才进行复制物理内存

在分配内存时,如果内存不足会发生什么

缺页中断

在进行申请内存分配时,实际上申请的是虚拟内存,此时并不会分配物理内存,当程序读写到申请的虚拟内存时,CPU会去访问这个虚拟内存,此时如果发现这个虚拟内存找不到映射的物理内存,CPU就会触发缺页中断,由用户态转为内核态,将中断处理交由内核的Page Fault Handler 缺页中断处理函数 处理

缺页中断处理函数内部首先会判断当前是否存在空闲的物理内存空间,如果有,则就直接分配,然后建立虚拟内存与物理内存之间的映射关系

如果没有空闲的物理空间,内核就会开始内存回收工作,唤醒 kswapd 线程启动后台回收,后台回收是异步操作,不会阻塞进程的执行,当后台回收跟不上内存分配时,内核就会开始直接回收,直接回收是同步操作,会阻塞进程的执行

如果进行内存回收后,剩下的空间仍然无法满足申请的内存时,内核就会触发 OOM (Out Of Memory)机制,内核会根据算法杀死一个占用物理内存较高的进程,释放内存资源,如果内存仍然不足,则会继续选择下一个占用较高的进程,直至内存足够为止

内存回收的内容主要是 文件页匿名页

  • 文件页指的是内核缓存的磁盘数据,也就是 Buffer,和内核缓存的文件数据,也就是 Cache,其中被应用程序修改过但还未写回磁盘的页被称为脏页,回收脏页时需要先写回磁盘后再进行内存释放,而回收干净页时就是直接释放内存
  • 匿名页指的是像堆栈数据这些没有实际载体的内存,这部分内存很可能在之后会被再次访问,所以不能就直接释放,而是通过 Swap 机制,将他们写到磁盘的交换区中,然后再释放内存

回收文件页和匿名页都是基于 LRU 算法,也就是先回收最近不常使用的内存,LRU算法维护了 active_list 和 inactive_list 者两个链表,越接近链表尾部,则就表示内存页越不常访问,系统就可以根据活跃程度优先回收不活跃的内存

页面置换有哪些算法

页面置换的功能简单来说,就是当出现了缺页异常,并且此时物理内存已满时,选择其中一个物理页换出到磁盘,然后把需要访问的页面从磁盘换入到物理页中

先说说最理想的最佳页面置换算法,这个算法的思路是,置换未来最长时间不访问的页面,由于程序访问页面时是动态的,无法预知每个页面在下一次访问前的等待时间,所以这个算法在实际系统上是无法实现的,只能用于衡量算法的效率

然后再说说先进先出置换算法,这个算法的思路是选择在内存驻留时间最长的页面进行置换

接着再说下最近最久未使用的置换算法,这个算法的思路是选择最长时间没有被访问的页面进行置换,在实现时需要维护一个所有页面的链表,最近最多使用的放在表头,最少使用的在表尾,每次访存都需要更新一整个链表,先找到一个页面,删除它,然后在把它移动到表头,这些更新操作是一个非常耗时的过程,开销会比较大

然后是综合了先进先出和最近最久未使用算法的时钟页面置换算法,这个算法的思路是将所有页面都保存在一个环形链表中,有一个表指针指向其中的一个页面,当发生缺页时,会先检查页面的访问位,如果为0就直接淘汰置换为新的页面,然后将新页面的访问位设置为1,如果为1则就设置为0,清除访问位,然后移动到下一个页面,继续检查,直到找到访问位为0的页面

最后说下最不常用算法,这个算法的思路是为每个页面都设置一个计数器,每当被访问到时计数器就加1,当发生缺页时就淘汰计数器值最小的一个,这个算法存在时间上的问题,如果某一个页面过去频繁访问,计数器值很高,但接下来出现了一个新的高频访问的页面,但这个页面的计数器值没有过去的一个大,在接下来的缺页异常置换中,新的页面就会被淘汰掉,有个办法可以解决这个问题,就是定期将过去访问的页面的访问次数除以2,这样随着时间经过,过去高频访问的页面被淘汰的概率就越来越大

什么是中断,有哪些中断,有什么作用

中断是指CPU停下当前的工作认任务,去处理其他事务,处理完成后回来继续完成原来的工作这一过程,中断流程具体来说,

  • 首先,当外部设备或软件需要处理器的响应时,会发出中断信号,处理器接收到中断信号后,会停止当前执行的指令,并进行保存当前执行现场,如程序计数器、寄存器状态等
  • 接着,处理器会根据中断向量表跳转到中断处理程序的入口地址,
  • 然后,处理器开始进行中断处理
  • 最后,处理器完成中断处理后,进行恢复现场,完成中断返回

中断分为外部中断和内部中断,

  • 外部中断又可再分为可屏蔽和不可屏蔽,
    • 可屏蔽的外部中断通常来说是来自 INTR(Interrupt Request) 线上的外部设备中断请求,比如硬盘、打印机和网卡等,此类中断不会影响系统的运行,可随时进行处理或者不处理;
    • 而不可屏蔽的请求通常来说是来自 NMI (None-Maskable Interrupt) 线上的中断请求,比如电源掉电、硬件线路故障等,这些中断会影响系统的运行,无法被操作系统或软件方式进行屏蔽;
  • 然后内部中断可分为陷阱、故障和终止,
    • 陷阱中断一般是在编写程序时有意设下的指令,在执行陷阱指令后,CPU会将调用特定的程序进行处理,比如系统调用、程序调试功能等,处理完后会返回到陷阱指令的下一条指令
    • 故障中断是在引起故障的指令还在执行时,CPU检测到一类意外错误而触发的中断,如果能处理修正这个意外错误,CPU将重新执行引起故障的指令,否则不继续处理直接进行报错,常见的故障中断为缺页中断,有专门的缺页处理程序,在成功处理后会返回到引起故障的指令继续执行
    • 终止中断是在执行指令时发生了致命错误而导致,并且不可修复,程序也无法继续执行,中断处理完后不会返回到源程序,而是直接结束程序的运行

中断的作用在于它使得操作系统具备应对突发情况的处理能力,提高了CPU的工作效率,如果没有中断,CPU就只能按照原来的程序编写的先后顺序,对各个外设进行查询和处理,即轮询的工作方式,这种方式实际上工作效率很低,也不能及时对紧急事件进行响应

了解过哪些I/O模型

  • 首先说下阻塞I/O模型,在这个模型下,应用程序发起的I/O操作后会被阻塞,直到操作完成才返回结果,适用于对实时性要求不高的场景
  • 接着说下非阻塞I/O模型,这个模型下,程序发起I/O操作后立即返回,不会被阻塞,但需要轮询或使用 select、poll或epoll 等系统调用来检查I/O完成的情况,适用于需要进行多路复用的场景,比如需要同时处理多个 socket 连接的服务器程序
  • 然后是I/O多路复用模型,通过 select、poll或epoll 等系统调用,应用程序可以等待多个I/O操作,但一个I/O操作准备就绪时,应用程序就会被通知,适用于同时处理多个I/O操作的场景,比如高并发的服务端程序
  • 还有信号驱动的I/O模型,应用发起I/O操作后,可以继续执行其他内容,当I/O完成后,操作系统会向应用发送一个信号来通知其已完成,这个适用于需要异步处理的场景,利于提高并发能力
  • 最后是异步I/O模型,应用程序发起I/O操作后,同样可以继续执行其他内容,I/O操作由内核完成,在操作完成后会通知应用程序

有哪些方式可以处理并发请求

首先,可以简单的通过单线程方式进行处理,一次处理一个请求,完成后进行下一个请求,当性能较低

然后,也可以通过多进程或多线程的方式处理,可以按需或事先创建进程或线程,一个请求用一个进程或一个线程来完成,但这个方式一旦遇到成千上万的请求时,会导致消耗过多的资源,从而引起系统崩溃

那么,我们可以采用I/O多路复用的方式,只使用一个进程同时处理多个请求,能够进行资源复用,避免创建多个进程导致的上下文切换带来的开销

如果希望充分利用多核CPU资源,我们可以采用多路复用多线程的方式,这个方式可以避免一个进程服务于过多的请求,将大量的请求分发给不同的进程下的线程处理

说下 select、poll 和 epoll 的区别

首先 select 实现多路复用的方式是,将已连接的 socket 都存储在一个文件描述集合中,然后通过 select 系统调用将集合从用户态拷贝到内核中,在内核中进行遍历这个集合来检查是否有 socket 连接发送网络事件,如果有发生事件,则会修改这个集合,标记对应的 socket,接着再把集合拷贝会用户态里,用户态再遍历这个集合找到标记的 socket 进行处理

其次 poll 和 select 区别在于 select 使用的是一个固定长度 BitsMap,个数上限为1024,而 poll 的方式使用的是一个动态数组突破了这个个数限制,但还是会受到系统文件描述符的限制,这两者都是使用的线性结构存储已连接的 socket 集合,每次都需要通过遍历来找到可读或可写的 Socket 进行处理,时间复杂度都是O(n),同时也需要用户态与内核态之间的文件拷贝和切换,但并发数上来时,性能的损耗会呈指数级增长

最后 epoll 就很好的解决了上面两个方式的问题,

  • epoll 在内核里使用的是红黑树存储待检测的 Socket ,通过 epoll_ctl() 函数可以将 Socket 添加到红黑树中,红黑树增删改的时间复杂度是 O(logn) ,相比于 select 或 poll 采用线性结构的 O(n),在面对大量需要检测的 Socket 时,epoll 的处理效率也不会大幅降低,同时,在每次进行添加时,只需要传入这一个待检测的 Socket 即可,而不需要像 select 或 poll 一样传入一整个集合,减少了拷贝的数据量和内存分配
  • 然后,epoll 使用的是事件驱动的机制,内核里维护了一个就绪链表,当某个 Socket 发生事件时,会通过回调函数的方式将其加入到这个就绪链表中,用户可以通过调用 epoll_wait() 函数来获取已就绪的 Socket,这样就不需要像 select 或 poll 方式那样轮询整个集合,大大提高检测效率

说下 epoll 的边缘触发和水平触发

当使用边缘触发模式时,当被监控的 Socket 集合上有事件发生时,内核会将事件加入到就绪队列中,服务器端只会调用一次 epoll_wait 唤醒,即使程序没有从内核读取数据,这里就需要进程在收到通知后立即地将内核缓存区中的数据读取完毕,我们可以通过循环的方式不断去读取数据,直到没有数据可读,确保不错过可读的数据,这里的循环读取是放在一个非阻塞的 I/O 处理中进行的,以避免程序卡在 I/O 操作中而无法继续执行其他内容

而当使用水平触发模式时,当监控到 Socket 集合上有事件发生时,服务器端会不断调用 epoll_wait 唤醒,直到内核缓存区的数据被读取完,在这个模式下就不需要一次性读取完,逻辑更宽容

这两种模式中,边缘触发的效率相对会高些,因为不需要多次的系统调用,减少了上下文的切换开销

说下Reactor模式

Reactor模式由 Reactor 和处理资源池两个核心部分组成,

  • Reactor 负责事件监听和分发
  • 处理资源池负责处理事件

Reactor模式可以根据业务的需要,设定一个或多个 Reactor,处理资源池也可以是一个或多个进程或线程

Redis 就是采用了单进程 Reactor 模式,Reactor 与客户端建立连接,然后监听事件把事件分发给 Handler,因为是在同一个进程下执行,所以处理期间不能在处理别的事务,好处是不需要进行多进程的通信,也不需要担心多进程的资源资源,实现简单,但缺点是无法充分利用多核CPU的性能,在Handler处理的业务如果耗时过长,会导致较大的响应延迟,不过由于Redis 的业务处理都是在内存中完成,操作速度相当快,性能瓶颈并不是CPU

然后 Nginx 则采用了多进程 Reactor 模式,Nginx 的方案和标准的有些差异,差异在于 Nginx 在主进程中只是做 Socket 初始化,没有创建 mainReactor 来进行与客户端的连接,而是由子进程的 Reactor 来进行连接,并且这里通过锁去控制一次只有一个进程进行处理,以防止出现同时唤醒多个子进程而造成不必要的资源浪费的惊群现象,子进程接收到连接后放入到自己的 Reactor 下进行处理,之后不会再分配给其他子进程

本文由作者按照 CC BY 4.0 进行授权