0%

网络IO

你了解过哪些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操作通过操作系统内核完成I/O操作,应用程序只需等待通知即可。适用于需要大量并发连接和高性能的场景,减少系统调用次数,提高调用效率

服务器处理并发请求有哪几种方式

  • 单线程web服务器方式:web服务器一次处理一个请求,结束后读取并处理下一个请求,性能较低
  • 多进程/多线程服务器:web服务器生成多个进程或线程并行处理用户请求,进程或线程可以按需或事先生成。有的web服务器为每个用户请求生成一个线程,进行响应,不过并发请求达到成千上万时,多个同时运行的线程会消耗大量的系统资源,
  • I/O多路复用web服务器:web服务器可以I/O多路复用,达到一个线程就能监听多个客户端的I/O事件
  • 多路复用多线程web服务器:将多线程和多路复用结合起来的web服务器,避免让一个线程服务于过多的用户请求,并能充分利用多CPU所提供的计算能力

讲一下I/O多路复用

I/O多路复用是一种多线程IO处理方式,指的是复用一个线程,处理多个socket中的事件。能够实现资源复用,防止创建过多线程导致的上下文切换开销

select,poll,epoll的区别是什么

select,poll,epoll都是用于I/O多路复用的机制

select

select的实现方式是将已连接的socket放入文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生(遍历),当检测到网络事件,将此socket标记为可读/可写,接着把整个文件描述符集合拷贝回用户态,用户态找到可读/可写socket(遍历)对其进行处理

对于select需要遍历两次文件描述符集合,一次是在用户态,一次是在内核态,还会发生两次拷贝文件描述符集合

select使用定长的bitmap表示文件描述符集合,而且所支持的文件描述符个数是有限的,在linux中有FD_SETSIZE限制,默认1024

poll

poll不再用bitmap存储所关注的文件描述符,而是采用动态数组,采用链表形式组织,突破了文件描述符的个数,还会受到系统描述符的限制

其余poll与select差别不大

epoll

  • epoll在内核中使用红黑树来跟踪进程所用待检测的文件描述符,把需要监控的socket通过epoll_ctl()函数加入到内核的红黑树中。epoll因为在内核维护了红黑树,可以保存所有待检测的socket,所以只需要传入一个socket,减少了内核和数据空间大量的数据拷贝和内存分配
  • epoll使用事件驱动机制,内核里维护一个链表来记录就绪事件,当某个socket有事件发生时,内核通过回调函数将其加入到这个就绪队列中,当用户调用epoll_wait()时,只会返回有事件发生的文件描述符的个数,而不需要返回整个epoll集合

epoll监听线程的个数上限为系统描述符的个数,且监听线程数很多时性能不会大幅降低,是解决C10K问题的核心技术

C10K问题:同时处理 10,000 个并发连接

epoll的边缘触发和水平触发有什么区别

epoll支持两种事件触发机制:边缘触发和水平触发

  • 边缘触发:当被监控的socket描述符上有可读事件发生时,服务器端只会从epoll_wait()苏醒一次,即使进程没有调用read函数从内核读取数据,也依然只苏醒一次,因此程序要保证一次性把内核缓冲区的数据读取完毕
  • 水平触发:当被监控的socket描述符上有可读事件发生时,服务器不断从epoll_wait中苏醒,直到内核缓冲区数据被read函数读完才结束,目的是告诉我们有数据需要读取

如果使用边缘触发,I/O事件发生时只会通知一次,因此我们会循环从文件描述符读写数据,如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序没办法往下继续执行,因此边缘触发通常与非阻塞式I/O一起使用

如果使用水平触发,当内核通知文件描述符可读写时,接下来还会检测他的状态,看他是否还是可读写。

一般边缘触发比水平触发效率高,因为边缘触发可以减少epoll_wait的系统调用次数

零拷贝是什么

传统的I/O工作方式,从硬盘读写数据,然后通过网卡向外发送,需要进行四次数据拷贝(磁盘->内核态内存->用户态内存->内核态内存->网卡),为了提高文件传输的性能,出现了零拷贝技术

通过一次系统调用合并了磁盘读取和网络发送两个操作(磁盘->内核态->网卡),将4次拷贝减少到2次,同时避免CPU拷贝和用户态->内核态切换,大幅提升I/O效率。

redis、nignx、netty是依赖什么实现的高性能

主要依赖Reactor模式实现的高性能,这是在I/O多路复用的基础上实现的基于事件驱动的网络模型

Reactor模式是灵活多变的,可以应对不同的业务场景:

  • Reactor的数量可以有一个也可以有多个
  • 处理资源池可以是单线程也可以是多线程

Redis

redis 6.0之前使用单Reactor单线程模式(6.0之后单 Reactor + 多 I/O 线程)。因为全部工作都在一个线程内完成,不需要考虑多线程竞争问题。

这种方案有两个缺点:

  • 只有一个线程,无法充分利用多核CPU性能
  • Handler对象在业务处理时,整个线程是无法链接其他事件的,如果业务处理耗时比较长,那就造成了响应延迟

因此单线程Reactor不适合计算密集型的场景,只适用于业务处理非常快的场景

netty

多Reactor多线程模式

优势是:

  • 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理
  • 主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无需返回数据,直接就可以在子线程将处理结果发给客户端

nginx

nginx是多Reactor多进程模式,不过与标准情况有些差异

主进程仅仅用来初始化socket,并没有创建mainReactor来accept连接,而是子进程的Reactor来accept连接,通过锁(同一时间只允许一个 Worker进程监听端口)来控制一次只有一个子进程进行accept(防止惊群效应),子进程accept新连接后就放到自己的Reactor里进行处理,不会再非配给其他的子进程

惊群效应:多个等待者同时争抢同一个资源,导致无效竞争和资源浪费