linux内核零拷贝技术

Posted HeroKern

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux内核零拷贝技术相关的知识,希望对你有一定的参考价值。

1. 设计思想

零拷贝技术主要用于磁盘数据通过网络进行交互,常见用法卸载磁盘文件从网络发送出去。常规的卸载文件方法流程如下所示。

从上图可以看出软件流程一共复制了4次数据,内核态到用户态切换4次。
读操作(复制两次,上下文切换两次):
1.用户进程通过 read() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)
2.CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)
3。CPU将读缓冲区(read buffer)中的数据拷贝到用户空间(user space)的用户缓冲区(user buffer)
4.上下文从内核态(kernel space)切换回用户态(user space),read 调用执行返回。
写操作(复制两次,上下文切换两次):
1.用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)
2.CPU 将用户缓冲区(user buffer)中的数据拷贝到内核空间(kernel space)的网络缓冲区(socket buffer)
3.CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。(虽然网络驱动会分层解析网络数据帧,但网络数据是通过sk_buff指针缓存在各个层级进行数据,所以这里网络协议层不存数据拷贝)
4.上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返
从上面可以看到用户态和内核态之间数据拷贝流程走了两次,熟悉linux内核调度结构的知道内核态和应用态数据拷贝是比较费时,同时拷贝数据是个冗余过程,仅仅从应用态过了一圈。所以引入了零拷贝技术。

在数据传输的过程中,避免数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝。有的时候,应用程序在数据进行传输的过程中不需要对数据进行访问,那么,将数据从 Linux 的页缓存拷贝到用户进程的缓冲区中就可以完全避免,传输的数据在页缓存中就可以得到处理。在某些特殊的情况下,这种零拷贝技术可以获得较好的性能。Linux 中提供类似的系统调用主要有 mmap(),sendfile() 以及 splice()。

mmap():
和传统的区别就是read操作变成了mmap,之后用户空间会和内核态共享同一块内核缓冲区,读入的数据都在这个内核缓冲区里面。写入的话还是和原来一样。这个函数在linux设计中用的比较多,结合/dev/mem使用可以直接在应用态访问硬件物理地址,或者将文件映射到内存等。

sendfile():
sendfile对于mmap来说更加优化了一步,数据从缓冲复制到到socket直接都是在内核空间一次性完成的,用户空间只是发起了sendfile的调用,减少了复制和上下文切换的开销。这个文件只能针对真正的文件从网络卸载,对于自定义文件系统是不支持的,对于高速存储用的比较少。

splice():
splice又在上面两位前辈的基础上变更更加强大,之前sendfile只能是内核缓冲区向socket复制数据,而splice直接让讷河缓冲区和socket之间建立了一个管道可以直接相互交换数据。
linux零拷贝技术本次采用的mmap+write方式实施,数据走向流程图如下所示。

将内核读缓存通过mmap函数映射到应用态,实现内核态和应用态共享该空间。通过mmap函数后会减少一次数据拷贝,上下文切换不变,但是通过异步设计思想可以规避掉上下文切换耗时。

2 代码设计

在内核态分配20个连续的DMA缓存空间,分别标号为1~20,读磁盘和通过网络发送数据实现异步方式,只要有空余的缓存空间,读线程一直读数据到20个缓存空间中,网络发送线程一直发送数据。读数据和发送数据通过循环队列实现,可以使用互斥锁实现循环队列,也可以使用无锁高效互斥队列。
2.1 mmap函数
zero_copy_vir_addr=mmap(NULL,zero_copy_block_size*zero_copy_block_num,PROT_READ|PROT_WRITE,MAP_SHARED,dev_fd,zero_copy_phy_addr);
if (zero_copy_vir_addr == NULL)
DBG_PRINTF("%s mmap fail!!\\n",FUNCTION);
return -1;

zero_copy_phy_addr是内核态分配的DMA缓存空间的物理地址,注意需要保证cache一致性,该地址在程序初始化时通过ioctl查询到应用态使用,zero_copy_vir_addr是映射该物理地址得到的虚拟地址,在write socket函数中使用。

2.2 读磁盘函数

应用态通过计算需要读写的扇区,然后通过read函数将需要读写的扇区写到内核态,驱动read函数实现如下:

1.拷贝需要读取的扇区信息
2.计算可以使用的20个缓存中的ID值,并且填充SQ队列,将缓存地址填入到SQ的PRP中
3.数据读取完成,返回数据存放的ID值,读写队列控制在应用态控制

2.3 应用态读函数

应用态读线程函数计算需要读取的扇区信息,同时获取是否有空闲的DMA 缓存ID,如果没有就休眠,一直等到有空闲为止。GetCanUseWriteAddr函数就是用来判断是否有可以使用的DMA 缓存 ,网络发送函数中会释放DMA 缓存。

2.4 网络发送函数

SOLO_Read函数用于读取是否有可以使用的数据完成块,注意这个函数是非阻塞的,所以下面有usleep休眠,如果使用了pthread_mutex_lock+ pthread_cond_wait函数互斥的话,那就是阻塞方式获取空闲缓存,我使用的高效无锁互斥,牺牲CPU换取高性能。

3 总结
在linux零拷贝设计中,需要注意cache一致性问题。没有使用零拷贝技术,linux平台下网络卸载速率只有70MB/s,使用零拷贝技术速度可以到达107MB/s。(千兆网络link状态下),并且通过上位机验证数据正确。需要技术支持联系vx:yolov8。

以上是关于linux内核零拷贝技术的主要内容,如果未能解决你的问题,请参考以下文章

linux内核零拷贝技术

linux内核零拷贝技术

[转]浅谈Linux下的零拷贝机制

Linux 中的零拷贝技术 转

Netty零拷贝

Linux系统I/O操作与零拷贝