LINUX网络
网络模型
协议栈
接受网络包的流程
发送网络包的流程
与接收流程相反。
零拷贝
为什么要有DMA技术?
没有DMA技术前的I/O过程:
DMA技术:
传统文件传输有多糟糕?
期间发生了4次用户态与内核态的上下文切换。 还发生了4次数据拷贝。
如何优化文件传输的性能?
减少用户态与内核态的上下文切换的次数?
- 减少系统调用的次数。
减少数据拷贝的次数?
- 用户的缓冲区没必要存在。
如何实现零拷贝?
mmap + write
sendfile
零拷贝技术
若网卡支持SG-DMA技术(和普通的DMA不同),则:
需要注意的是,此时进程无法对文件内容进一步加工,例如压缩数据再发送。
PageCache
内核态的缓存区,实际上就是磁盘高速缓存(PageCache).
PageCache的主要优点:
- 缓存最近被访问的磁盘数据;
- 缓存尽可能多的I/O请求在PageCache中,最后合并成一个更大的I/O请求发送给磁盘,这样做是为了减少磁盘的寻址操作。
- 预读功能(虽然read方法每次只读取32KB,但内核会把其后面的32~64KB也读取到PageCache);
但是,在传输大文件的时候,PageCache会不起作用:
- PageCache长时间被大文件占据,其他热点的小文件则无法充分使用PageCache;
- PageCache没有享受到缓存带来的好处(文件过大PageCache很快被占满),但却耗费DMA多拷贝到PageCache一次;
大文件传输的实现?
最初:
异步I/O:
可以看出,异步I/O并没有涉及到PageCache。 绕开PageCache的I/O叫做直接I/O,使用PageCache的I/O则叫做缓存I/O.
针对大文件的传输,应该使用异步I/O+直接I/O来代替零拷贝技术。
I/O多路复用
最基本的Socket模型
bind():绑定了一个IP地址和端口。
- 绑定IP地址: 一台机器可以有多个网卡,每个网卡有对应的IP地址。
- 绑定端口:内核收到TCP报文,通过TCP头里面的端口号,来找到我们的应用程序。
connect():客户端在创建好Socket后,调用connect()发起连接,并指明服务端的IP地址和端口号,然后TCP三次握手。
服务器内核维护了两个队列:
- TCP半连接队列:未完成三次握手的连接;
- TCP全连接队列:已完成三次握手的连接。
- 在 TCP 三次握⼿过程中,当服务器收到客户端的 SYN 包后,内核会把该连接存储到半连接队列;
- 然后再向客户端发送 SYN+ACK 包,接着客户端会返回 ACK;
- 服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其增加到全连接队列 ,等待进程调⽤ accept() 函数时把连接取出来。
accept():当TCP全连接队列不为空时,服务端的accept()会从中拿出一个已连接Socket返回给应用程序,后续数据传输都用这个Socket.
注意:监听的Socket和已连接Socket是两个。
如何服务更多的用户
TCP连接由四元组唯一确认:本机IP,本机端口,对端IP,对端端口.
最大TCP连接数=客户端IP数X客户端端口数。
但这不可能。因为:
- 文件描述符:Socket是一个文件,也就会对应一个文件描述符。在linux下,单个进程打开的文件描述符有限。
- 系统内存:每个TCP连接在内核中都有对应的数据结构,意味着每个连接都会占用一定的内存。
多进程模型
子进程只需要关心 已连接Socket; 父进程只需要关心 监听Socket;
缺点:进程的上下文切换"包袱"很重。
多线程模型
线程池(提前创建若干个线程),当新连接建立后,把已连接Socket放入队列,然后线程池里的线程负责从队列中取出。
缺点:其实还是有问题的,一台机器维护1万个连接,相当于维护1万个线程,好难。
I/O多路复用
一个进程虽然任一时刻只能处理一个请求,但是处理每个请求时若只耗时1ms,那么1s内就可以处理1000个请求;所以也叫做时分多路复用。
select/poll
- 将已连接Socket放入一个文件描述符集合(用户态),并拷贝到内核里;
- 在内核中遍历检查是否有网络事件发生;若检查到,则标记为可读/可写;
- 把整个集合拷贝回用户态,在用户态遍历找到标记后的Socket,对其处理。
区别:select使用固定长度的BitsMap表示文件描述符集合,poll使用动态数组(链表)。
epoll
- 在内核中,使用红黑树跟踪所有待检测的文件描述符。因此,不需要拷贝整个集合,只需要传入一个待检测的socket.
- 内核里维护了一个链表记录事件发生的Socket。只将有事件发生的Socket集合传递给应用程序。
epoll支持边缘触发和水平触发。
- 边缘触发:服务器只会从epoll_wait中苏醒一次;
- 水平触发:服务器不断从epoll_wait中苏醒,直到内核缓冲区数据被read函数读完。
- 边缘触发效率比水平触发效率高:因为可以减少epoll_wait调用次数(系统调用存在上下文的切换);
- I/O多路复用一般和非阻塞I/O搭配使用;多路复用API返回的事件不一定可读写,如果使用阻塞I/O可能会程序阻塞。
高性能网络模式
演进
网络高性能基于I/O多路复用。但是面向过程的方式开发效率不高。 基于面向对象的思想,对I/o多路复用作了一层封装:Reactor模式.
Reactor模式主要有两部分组成:
- Reactor负责监听和分发事件,包括连接和读写事件;
- 处理资源池负责处理事件,如read->业务->send;
Reactor模式灵活在于:
- 单Reactor 单进程/线程;
- 单Reactor 多进程/线程;
- 多Reactor 单进程/线程;(可笑)
- 多Reactor 多进程/线程;
Reactor
单Reactor 单进程/线程
- Reactor监听并分发事件,分发对象要看收到的事件类型;
- 如果是连接建立事件,则由Acceptor处理Acceptor会获取连接并创建一个Handler对象处理后续响应事件;
- Handler完成业务流程。
缺点:
- 只有一个进程,无法利用多核CPU;
- Handdler对象在业务处理时,整个进程无法处理其他连接事件。
适用于业务处理快速的场景,不适用于计算机密集型场景。
单Reactor 多线程
Handler不再负责业务处理,通过read读取数据后,将数据发给子线程的Processor处理; Processor处理完后,将结果发给主线程的Handler,再通过send将结果发送给client。
缺点:一个Reactor承担所有事件的监听和响应,且只在主线程中运行;面对高并发场景容易称为性能瓶颈。
多Reactor 多进程/线程
Proactor
概念
Reactor是非阻塞同步网络模式; Proactor是异步网络模式。
阻塞I/O:
等待的是 内核数据准备好 和 数据从内核拷贝到用户态
非阻塞I/O:
最后一次read调用需要等待数据从 内核拷贝到用户态。
因此,阻塞I/O与非阻塞I/O都是同步调用。 真正的异步I/O是两个过程都不需要等待。
区别
- Reactor非阻塞同步:感知到有事件发生(就绪可读),需应用进程主动调用read将socket接收缓存中的数据读到应用进程内存中;
- Proactor异步:由操作系统完成读写工作;完成后通知应用进程。
Reactor模式基于待完成的I/O事件; 而Proactor模式基于已完成的I/O事件。
Proactor模式
Processor 完成I/O操作后通知Proactor; Proactor 根据不同事件类型回调不同的Handler进行业务处理; Handler 完成业务处理。
网络的性能指标
通过是以4个指标来衡量:
- 带宽: 链路最大传输速率,单位是b/s。
- 延时: 请求数据包发送后,收到对端响应所需要的时间延迟。
- 吞吐率: 单位时间内成功传输的数据量,单位是b(比特)/s或者B(字节)/s;吞吐受带宽限制。
- PPS: 表示以网络包为单位的传输速率,一般用于评估对网络包的转发能力。
其他:
- 网络可用性: 网络是否能正常通信;
- 并发连接数: TCP连接数量;
- 丢包率: 丢失数据包数量占发送数据组的比例;
- 重传率: 重传数据包的比例;