Linux 网络 IO
# Linux 网络 IO
# 网络协议栈
# 历史
早期各家电脑厂商的网络通信系统都是自己开发自己使用的,相互不兼容。1975年后为了共享资源,人们开发并制定了 TCP/IP 协议簇。随后在1982年 ISO 组织才制定了学术上更加完备的 OSI 七层网络模型标准。
但是 TCP/IP 出现时机早,又在应用中发展完善,使得其成为网络领域的事实性标准,而 OSI 则只是教科书标准。
# 定义
网络协议栈是指网络中各层协议的总和。
它对网络中信息传输的过程进行分层建模,端到端之间的通信也抽象为了由上层协议到底层协议,然后通过信道传输到对端后,再由底层协议到上层协议。
应用层:为应用程序提供服务并规定应用程序通信的相关细节。
表示层:将数据转换为标准传输格式,确定编码格式。
会话层:处理连接的建立、断开以及连接的方式。
传输层:保证传输的可靠性。
网络层:寻址和路由,将数据传送至具体的网络地址。
数据链路层:硬件层次的数据处理,将数据传送至具体的物理地址。
物理层:物理信号到01数字信号的转换。
# 内核态协议栈
# socket编程
服务端socket编程
int main()
{
fd = socket(AF_INET, SOCK_STREAM, 0);
bind(fd, ...);
listen(fd, ...);
cfd = accept(fd, ...);
read(cfd, ...);
dosometing();
write(cfd, ...);
close(fd);
close(cfd);
}
客户端socket编程
int main()
{
fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, ...);
write(fd, ...);
read(fd, ...);
dosometing();
close(fd);
}
# 网络IO
- 原始IO
用户进程向 内核 发起 read 系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。
内核 在接收到指令以后对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区。
数据准备完成以后,磁盘向 内核 发起 I/O 中断。
内核 收到 I/O 中断以后将磁盘缓冲区中的数据拷贝到内核缓冲区,然后再从内核缓冲区拷贝到用户缓冲区。
用户进程由内核态切换回用户态,解除阻塞状态。
- DMA IO
用户进程向 内核 发起 read 系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。
内核 在接收到指令以后对 DMA 磁盘控制器发起调度指令。
DMA 磁盘控制器对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区,CPU 全程不参与此过程。
数据读取完成后,DMA 磁盘控制器会接受到磁盘的通知,将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
DMA 磁盘控制器向 内核 发出数据读完的信号,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区。
用户进程由内核态切换回用户态,解除阻塞状态。
- 零拷贝优化
mmap地址映射
mmap 是 Linux 提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址。
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
// addr:指定映射的虚拟内存地址,可以设置为 NULL,让 Linux 内核自动选择合适的虚拟内存地址。
// length:映射的长度。
// prot:映射内存的保护模式,可选值如下:
// PROT_EXEC:可以被执行。
// PROT_READ:可以被读取。
// PROT_WRITE:可以被写入。
// PROT_NONE:不可访问。
// flags:指定映射的类型,常用的可选值如下:
// MAP_FIXED:使用指定的起始虚拟内存地址进行映射。
// MAP_SHARED:与其它所有映射到这个文件的进程共享映射空间(可实现共享内存)。
// MAP_PRIVATE:建立一个写时复制(Copy on Write)的私有映射空间。
// MAP_LOCKED:锁定映射区的页面,从而防止页面被交换出内存。
// ...
// fd:进行映射的文件句柄。
// offset:文件偏移量(从文件的何处开始映射)。
e.g.
int fd = open(filepath, O_RDWR, 0644); // 打开文件
void* addr = mmap(NULL, 8192, PROT_WRITE, MAP_SHARED, fd, 4096); // 对文件进行映射
用户进程通过 mmap() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
将内核空间的读缓冲区(read buffer)与用户空间的缓存区(user buffer)进行内存地址映射。
内核利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
上下文从内核态(kernel space)切换回用户态(user space),mmap 系统调用执行返回。
用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
CPU将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
内核利用DMA控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。
sendfile系统调用
sendfile 是 Linux 提供的一种在两个通道之间进行的数据传输方法。
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
// out_fd:待写入内容的文件描述符
// in_fd:待读出内容的文件描述符
// offset:文件的偏移量
// count:需要传输的字节数
// return:
// 成功:返回传输的字节数
// 失败:返回-1并设置errno
用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
内核 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
CPU 将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
内核 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。
splice系统调用
splice 是 Linux 提供的一种在两个文件描述符(至少一个是管道)之间进行高效通信的方法。
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);
// fd_in:待输入数据的文件描述符
// off_in:从数据流何处开始读取数据
// fd_out:待输出数据的文件描述符
// off_out:从数据流何处开始输出数据
// len:指定移动数据的长度
// flags:指定移动数据的长度
// SPLICE_F_MOVE:按整页内存移动数据
// SPLICE_F_NONBLOCK:非阻塞
// SPLICE_F_MORE:提示内核后续调用将读取更多数据。
用户进程通过 splice() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
内核利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
CPU 在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline)。
内核利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
上下文从内核态(kernel space)切换回用户态(user space),splice 系统调用执行返回。
系统调用 | CPU拷贝 | DMA拷贝 | 上下文切换 |
---|---|---|---|
read+write | 2 | 2 | 4 |
mmap+write | 1 | 2 | 4 |
sendfile | 1 | 2 | 2 |
splice | 0 | 2 | 2 |
Unix 内核网络协议栈最早是专为电话系统设计,后来演变为通用网络通信协议。但是随着网络传输数据量的日益庞大,Unix的处理设计已不再适合大数据时代。
两种解决方案:
将协议栈上移到用户态
将协议栈下移到专用硬件
# 用户态协议栈
用户态直接 I/O 使得应用进程可以运行在用户态(user space)下直接访问硬件设备,数据直接跨过内核进行传输。内核在数据传输过程除了进行必要的虚拟存储配置工作之外,不参与任何其他工作。
优势:可以根据具体情况定制网络协议栈的实现,从而避免内核的各种多余资源开销。
劣势:需要在用户态实现全套网络协议栈。
# DPDK(数据平面开发套件)
- UIO(Linux Userspace I/O)
提供用户态下的驱动程序支持,也就是说网卡驱动是运行在用户空间的,减少了报文在用户态和内核态的多次拷贝。
- 用户空间轮询模式(PMD)
linux 系统是通过中断的方式告知 CPU 有数据包到达。当网络的流量越来越大,linux 系统会浪费越来越多的时间去处理中断,浪费很多CPU资源。
DPDK 采用用户空间的轮询模式驱动。网卡通过 DMA 将数据包传输至事先分配好的用户态缓冲区,而后应用程序通过不断轮询的方式可以读取数据包并在原地址上直接处理,不需要中断。
- 大页内存
Linux 操作系统的页大小一般为 4K ,所以当应用程序占用的内存比较大的时候,会需要较多的页表,开销比较大,而且容易未命中缓存。
DPDK 缓冲区提供了 Hugepage 大页内存,可以将页大小扩大至2MB甚至1GB(需要硬件支持),从而使得应用程序需要更少的页,从而需要更少的TLB,增加了缓存命中率且降低了地址转换的时间开销。
- CPU亲和性
在一个多核处理器的机器上,每个 CPU 核心本身都存在自己的缓存,缓冲区里存放着线程使用的信息。如果线程没有绑定CPU核,那么线程可能被 Linux 系统调度到其他的 CPU 上,从而使得 CPU 的 cache 命中率就降低了。如果将线程绑定到某个 CPU 后,线程就会一直在指定的 CPU 上运行,操作系统不会将其调度到其他的 CPU 上,节省了调度的性能消耗以及缓存的命中率,从而提升了程序执行的效率。
内存池和无锁环形缓存管理
网络存储优化
……
# F-stack+DPDK
将 FreeBSD 的网络协议栈模块移植到用户态。
# VPP+DPDK
定制加速网络协议栈的处理过程。
VPP 设计的核心是数据包处理图(将网络协议栈抽象为节点图)。
以组为单位处理网络包——提高相关代码的缓存命中率
以图形节点的方式解构网络协议栈——便于拓展
……
同时,VPP还可以通过修改 LD_PRELOAD 环境变量的方法替换系统链接库,将使用 POSIX 套接字API的应用程序底层网络切换到VPP上。(不用修改应用源码)
LD_PRELOAD是Linux系统中的一个环境变量,用于在程序运行时动态加载指定的共享库。LD_PRELOAD的作用是在程序运行前,将指定的共享库加载到程序的内存中。这样,程序在运行时会优先使用该共享库中的符号,而不是系统默认的符号。LD_PRELOAD可以用于替换程序本身的函数,增加程序的功能或者调试程序。
vppvcl 内部实现了相关的 posix api 函数。要注意的是,vcl和应用不是运行在同一个进程;VPP的协议栈处理完数据后,是将数据放入一段共享内存中,vcl库从共享内存取出数据给到应用。使用教程 (opens new window)
# RDMA 远程直接内存访问
将数据包的发送接收下派给HCA(网卡)实现,即将网络协议栈的相关计算放在网卡硬件中执行。
# DPU 数据处理器
将数据相关的流程化处理(解压缩、加解密……)集成到一颗芯片上,从而形成 CPU负责通用控制任务、GPU负责多线程并行计算任务、DPU负责大规模流程化数据处理任务的新结构。