操作系统(六):网络系统

1. 零拷贝

1.1 DMA 技术

如果没有 DMA 技术,则一个I/O的过程是这样的:

  • CPU 发起读取数据的请求给磁盘设备控制器。
  • 磁盘设备控制器收到CPU的命令之后,就会将数据读取到自己的缓冲区中,之后,向CPU发起一个中断。
  • CPU 收到中断信号后,停下正在进行的工作,接着把磁盘控制器的缓冲区中的数据⼀次⼀个字节地读进自己的寄存器,然后再把寄存器⾥的数据写入到内存。
  • 整个过程,CPU阻塞等待的时间非常长。如果对于大文件,则更加无法想象。

所以,产生了直接存储器存取(Direct Memory Access,DMA) 技术。简单来说,就是可以使得设备在 CPU 不参与的情况下,能够自行完成把设备 I/O 的数据复制到内存中。

DMA方式工作过程如下

  • CPU发出IO请求指令,操作系统收到请求之后,发送给DMA,CPU通过设置DMA控制器的寄存器对它进行编程,通知它要读多少数据,要读到什么位置。之后CPU可以继续执行其他任务。
  • DMA把IO请求发送给磁盘设备控制器,之后磁盘设备控制器将数据读取到缓冲区中,读满之后,向DMA发送一个中断信号。
  • DMA收到中断信号后,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,该过程不需要CPU的参与。
  • 当DMA读取到了足够需要的数据,则对CPU发起一个中断,CPU则将数据从内核空间拷贝到用户空间,可以正常使用。

该过程,CPU阻塞等待的时间,大大减少,提高了CPU利用率。

1.2 传统网络中的文件传输

对于服务器端或者客户端要进行网络的文件传输,最简单的方式就是通过IO,从磁盘读取出数据,传输到读入到网卡。

在这里插入图片描述

  • 整个过程发生了四次内核态和用户态的切换,因为发生了两次系统调用,read() 和 write()。内核态和用户态的切换消耗大量时间。
  • 整个过程进行了四次数据的拷贝操作。显然效率很低。

1.3 零拷贝

通常 Linux 中实现零拷贝的技术有两种:

  • mmap + write
  • sendfile

1.3.1 mmap ( ) + write ( )

mmap() 系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

在这里插入图片描述

  • 通过mmap映射,使得通过DMA读取数据之后,不需要拷贝到用户空间,直接在内核缓冲区中执行write 系统调用拷贝到socket 缓冲区,减少一次数据拷贝。
  • 但是,因为还是需要两次系统调用,所以还是要进行四次用户态和内核态的切换。

1.3.2 sendfile( )

Linux 内核中提供了一个专门用于发送文件的系统调用函数sendfile( )。

	#include <sys/socket.h>
    /**
     *
     * @param out_fd 目的端的文件描述符
     * @param in_fd 源端的文件描述符
     * @param *offset 源端偏移量
     * @param count 需要传送数据的长度
     * @return 实际传送长度
     */
    ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

  • 通过sendfile 函数,可以替代read() 和 write() 函数,直接拷贝文件到socket缓冲区,这样只需要进行一次系统调用,也就是两次内核态和用户态的转换。

在这里插入图片描述

这也不是完全的零拷贝技术,如果网卡支持SG-DMA(The Scatter-Gather DirectMemory Access)技术,那么我们可以进一步减少将内核缓冲区拷贝到socket缓冲区的过程,而直接从内核缓冲区拷贝到网卡中。

可以通过该指令查看是否支持SG-DMA技术。
在这里插入图片描述

  • 如果支持,那么sendfile()系统调用就会发生一点变化:
  • 通过DMA将磁盘的数据拷贝到内核缓冲区中,接着,将文件的描述符以及数据长度发送到socket缓冲区之后,通过SG-DMA,可以直接将数据从内核缓冲区拷贝到网卡中,减少了一次数据拷贝。

在这里插入图片描述

这就是所谓的零拷贝(Zero copy) 技术,因为在整个过程中没有通过CPU来拷贝数据,而是通过DMA来传输。只需要进行两次内核态和用户态的切换,以及两次DMA的拷贝操作。

2. I/O 多路复用

2.1 I/O模型

  • 阻塞式IO ( blocking IO)模型

CPU发起IO系统调用之后,会一直阻塞等待数据准备完毕复制到用户空间之后,才继续下一步工作。

在这里插入图片描述

  • 非阻塞式IO ( non-blocking IO)模型

CPU发起一个IO系统调用之后,如果内核并没有将数据准备好,则返回一个EWOULDBLOCK错误码,CPU也不会一直阻塞等待,而是去做其他事,每过一段时间,再进行一次请求,直到将数据准备好之后,CPU将内核缓冲区的数据,复制到用户空间。

在这里插入图片描述

  • 异步IO( asynchronous IO )模型

CPU发送一个异步IO系统调用请求之后,马上返回。内核处理收到的IO请求,包括准备数据,将数据复制到用户空间的缓冲区中,都由操作系统来完成,当完成操作以后,再通知CPU已经完成,可以对数据进行操作。

在这里插入图片描述

  • IO 复用(IO multiplexing)模型

将CPU阻塞在select、poll或者epoll中的一个系统调用上,而不是阻塞在IO操作的系统调用上,并且一个选择器可以注册多个套接字接口,通过选择器轮询到连接有 I/O 请求就进行处理。

多路复用的优势在于: ① 一个进程(线程)可以监听多个连接,提高效率节省资源。② 操作系统通过选择器的一次系统调用替换了原本需要定时的轮询系统调用。

2.2 select( ) / poll( )

  • select

    • select 实现多路复用的方式是,将已连接的 Socket(不仅限于套接字,所有描述符都可以) 都放到⼀个文件描述符集合。
    • 然后调用 select 函数将文件描述符集合都拷贝到内核中。
    • 内核通过遍历文件描述符,检查到有IO事件,则将该socket 标记为可读或者可写。
    • 接着将所有文件描述符集合都拷贝进用户空间。用户再通过遍历文件描述符集合找到该socket,进行操作。
    • 所以,总共需要两次对文件描述符集合的遍历,以及两次对文件描述集合的拷贝。
    • select 使用固定长度的 bitsmap(默认最大为1024),来表示文件描述符集合。所以支持复用的个数是有限的。
  • poll

    • poll 和 select 没有本质区别。
    • poll 使用链表来代替bitmap存储文件描述符,突破了文件描述符个数的限制。
    • poll不会改变文件描述符,而select 会改变文件描述符(对有数据的描述符置位)没法对描述符进行复用。
    • 都需要通过遍历来选择描述符,并且都需要对整个文件描述符的进行两次拷贝。
    • select 时间采用的是ns,而poll 和 epoll 则是ms,所以实时性更强。

2.3 epoll( )

epoll 主要通过两个方面解决select 和 poll 的问题:

  • ① epoll 在内核中使用红黑树来跟踪进程所有的文件描述符,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,可以进行高效的添加和删除 socket。
  • ② epoll 使用事件驱动的机制,内核里维护了⼀个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数,内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

在这里插入图片描述

epoll 主要通过三步进行IO的多路复用:

  • 创建一个epoll的句柄,传入的size参数并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
int epoll_create(int size)
  • 注册连接以及关心的事件到epoll句柄中。
    /**
     *
     * @param epfd epoll_create()的返回值,也就是 epoll 的句柄
     * @param op 对文件描述符事件操作的类型:
     *           ① 添加 EPOLL_CTL_ADD,② 删除 EPOLL_CTL_DEL,③ 修改 EPOLL_CTL_MOD
     * @param fd 需要监听的文件描述符
     * @param epoll_event 需要监听的具体事件:                  
     * @return
     */
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

/**
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
*/
  • 获取发生事件的文件描述符。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

另外,epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。

  • 边缘触发(ET)
    • 当 epoll_wait 检测到描述符事件发生并将此事件通知进程,进程只会苏醒一次并且必须立即处理该事件,如果不处理,下次再调用 epoll_wait 则不会响应通知该事件。
  • 水平触发(LT)
    • 当 epoll_wait 检测到描述符事件发生并将此事件通知进程,进程可以不立即处理该事件,之后会不断的通知该事件,直到事件完成读写操作。

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

select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。

另外,使用 I/O 多路复用尽量使用非阻塞IO进行搭配使用。特别是在边缘触发的情况下,因为在边缘触发模式只会通知一次,并且进程也不知道需要读取多少数据,如果数据量很大需要循环的读取,那么使用阻塞I/O会在数据没有准备好的时候也阻塞CPU,导致消耗大量的时间。还有当错误的检测到可读取状态时,使用阻塞I/O会导致CPU一直阻塞。

3. 高性能网络模式:Reactor 和 Proactor

3.1 Reactor 模式

Reactor 译为反应堆,指的是,当有一个事件产生,Reactor 就会有相应的反应。即,通过多路复用监听事件,收到事件后,根据事件的类型,分发给某个线程处理。

3.1.1 单 Reactor 单线程(进程)

单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服务

在这里插入图片描述
可以看到线程里有 Reactor、Acceptor、Handler 这三个对象:

  • Reactor 对象的作用就是监听和分发事件;
  • Acceptor 对象的作用是获取连接;
  • Handler 对象的作用是处理业务;

该方案的具体执行过程也很容易理解:

  • 客户端发送请求到Reactor,Reactor 通过select 监听事件,并通过dispatch 分发事件。
  • 如果是连接建⽴的事件,则交由 Acceptor 对象进⾏处理,Acceptor 对象会通过 accept 方法 获取连接,并创建⼀个 Handler 对象来处理后续的响应事件。
  • 如果是业务事件,则通过 Handler 来进行处理响应。

单 Reactor 单线程的优点和缺点:

  • 优点:
    • 在同一个进程中完成,实现简单,层次分明。
    • 不需要考虑多线程的竞争、通信的问题。
  • 缺点:
    • 只有一个线程(进程),无法充分利用多核CPU。
    • Handler 对象在业务处理时,整个线程是⽆法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟。
    • 单个线程的可靠性问题,如果陷入死循环或者意外停止等,将造成整个系统的不可用。

所以,单 Reactor 单线程的方案不适用于CPU密集型的场景,只适用于业务处理非常快速,或者IO密集型的场景。

例如,Redis 是由 C 语⾔实现的,它采用的正是单 Reactor 单线程的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单线程的方案。

3.1.2 单 Reactor 多线程(进程)

单 Reactor 多线程,1 个前台接待员,多个服务员,接待员只负责接待

在这里插入图片描述
单 Reactor 多线程的改动如下:

  • Handler 只负责响应事件,不做具体的业务处理,通过 read 读取数据后,会将数据分发给 worker 线程池的某个线程处理业务。
  • worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler。
  • Handler 收到响应后,通过 send 将结果返回给客户端。

单Reactor 多线程的优点和缺点:

  • 优点:
    • 使用多线程,充分利用CPU多核的性能。
    • 将业务处理通过其他线程完成,减少 Handler 的阻塞时间。提高整个系统的性能。
  • 缺点:
    • 多线程的情况会涉及线程安全问题以及线程间通信的问题。
    • ⼀个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈。

3.1.3 多 Reactor 多线程(进程)

主从 Reactor 多线程,多个前台接待员,多个服务生
在这里插入图片描述

多 Reactor 多线程模式,也成为主从 Reactor 模式,即通过主从的多个Reactor 代替单个Reactor。

具体工作过程如下:

  • Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件。
  • 当 Acceptor 处理连接事件后,MainReactor 将连接分配给子线程 SubReactor(SubReactor 可以有多个,图中没有体现),Subreactor 将连接加入到连接队列进行监听,并创建 Handler 进行各种事件处理。
  • 当有事件发生时,Subreactor 就会调用对应的 Handler 处理, Handler 通过 read 读取数据,分发给后面的 worker 线程处理。
  • worker 线程池分配独立的线程进行业务处理,并返回结果给 Handler。
  • Handler 收到响应的结果后,再通过 send 将结果返回给客户端。

多 Reactor 多线程的优点和缺点:

  • 优点:
    • 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
    • 父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据,处理完数据之后,可以直接发送给客户端。
  • 缺点:
    • 编程复杂度提高。
Netty 的多 Reactor 多线程模式。

在这里插入图片描述

Netty 的具体组件可以看下面这篇博客
Netty 组件解析

  • Netty 中抽象出两组线程组 BossGroup 和 WorkerGroup 。
  • BossGroup 相当于一个或多个 Reactor 主线程:
    • 轮询客户端的 accept 事件。
    • 如果发生连接请求,则建立连接,生成 NioSocketChannel 并且将其感兴趣的事件注册到 WorkerGroup 中某个线程的选择器 Selector 上 。
  • WorkerGroup 相当于 Reactor 子线程:
    • 其中每个线程维护一个 Selector 轮询注册的IO事件。
    • 如果事件发生,则会通过 pipline 进行业务处理,pipline 中维护了很多处理器 Handler 。

3.2 Proactor 模式

Reactor 是同步非阻塞网络模式,而 Proactor 是异步网络模式。

  • Reactor 是同步非阻塞网络模式,感知的是就绪的可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要用户进程主动调⽤ read 系统调用来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
  • Proactor是异步网络模式,感知的是已完成的读写事件。 在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像Reactor那样还需要应用进程主动发起 read/write来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。

在这里插入图片描述

Proactor 模式的工作流程:

  • Proactor Initiator 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核;
  • Asynchronous Operation Processor 负责处理注册请求,并处理 I/O 操作;
  • Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor;
  • Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理;
  • Handler 完成业务处理;

另外, 在 Linux 下的异步 I/O 是不完善的, aio 系列函数是由 POSIX 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步,并且仅仅支持基于本地文件的 aio 异步操作,网络编程中的 socket 是不支持的,这也使得基于 Linux 的高性能网络程序都是使用 Reactor 方案。

而 Windows 里实现了一套完整的支持 socket 的异步编程接口,这套接口就是 IOCP,是由操作系统级别实现的异步 I/O,真正意义上异步 I/O,因此在 Windows 里实现高性能网络程序可以使用效率更高的 Proactor 方案。

4. Socket

Socket 是传输层协议的接口,需要实现传输层协议之上的所有功能,可以说是 TCP/IP 对外的一个窗口。

参考

https://dongzl.github.io/netty-handbook/#/_content/chapter01
https://www.zhihu.com/question/26943938

《UNIX网络编程 卷1:套接字联网API》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值