|
本文探讨Linux中 主要的几种零拷贝技术 以及零拷贝技术 适用的场景 。为了迅速建立起零拷贝的概念,我们拿一个常用的场景进行引入: $ j! N' e, L3 k 01 引文, ]# L2 q/ {( } 0 d9 N* b2 W; C K! c 在写一个服务端程序时(Web Server或者文件服务器),文件下载是一个基本功能。这时候服务端的任务是:将服务端主机磁盘中的文件不做修改地从已连接的socket发出去,我们通常用下面的代码完成: 3 [' m" C6 I8 ]3 q
接下来,write系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中,最后socket再把内核缓冲区的内容发送到网卡上。说了这么多,不如看图清楚:" S+ s6 b3 e; W: w, D
数据拷贝 从上图中可以看出,共产生了四次数据拷贝,即使使用了DMA来处理了与硬件的通讯,CPU仍然需要处理两次数据拷贝,与此同时,在用户态与内核态也发生了多次上下文切换,无疑也加重了CPU负担。 $ X) t/ l+ N, w U! s6 d 在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性。 ; i- p) M; Q+ l3 S) T L& s" l# \) Z) Q6 s; N 02 什么是零拷贝技术(zero-copy)?' e# ^& T+ H' x$ \ 8 r) T3 W9 L$ K' A9 B 零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让CPU解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。! M4 h6 U, V. y' C 5 |5 ]2 ^# Q6 H$ E* @+ r 我们继续回到引文中的例子,我们如何减少数据拷贝的次数呢?一个很明显的着力点就是减少数据在内核空间和用户空间来回拷贝,这也引入了零拷贝的一个类型:: b7 n/ ?4 y1 ^. c7 } 让数据传输不需要经过 user space。 " ]0 Y( G, x: [& d! Y ' F+ F6 ]+ u- M C 03 使用 mmap1 b; x6 ?# e8 ]" ~5 r5 w 我们减少拷贝次数的一种方法是调用mmap()来代替read调用:! n8 m j0 d. ~
mmap 使用mmap替代read很明显减少了一次拷贝,当拷贝数据量很大时,无疑提升了效率。但是使用mmap是有代价的。当你使用mmap时,你可能会遇到一些隐藏的陷阱。例如,当你的程序map了一个文件,但是当这个文件被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被SIGBUS信号终止。SIGBUS信号默认会杀死你的进程并产生一个coredump,如果你的服务器这样被中止了,那会产生一笔损失。# ~8 @3 D4 K- U4 }; J; a4 i7 B 9 {0 K8 G/ n# M: r 通常我们使用以下解决方案避免这种问题: & r$ L7 l* b$ { ! p4 O, n* `5 P# J* u, t 1. 为SIGBUS信号建立信号处理程序 . M6 h3 E/ E8 Z! k) P4 B 当遇到SIGBUS信号时,信号处理程序简单地返回,write系统调用在被中断之前会返回已经写入的字节数,并且errno会被设置成success,但是这是一种糟糕的处理办法,因为你并没有解决问题的实质核心。9 U: f( g2 U' i: e N- p2 \ # K2 C3 ^ P7 k6 Z 2. 使用文件租借锁0 `" Z; z' n1 x5 p1 ^ $ u) m7 V& {3 m * O; a9 Z7 O1 Q2 [4 w, @0 P4 }" Z3 I 通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,当其它进程想要截断这个文件时,内核会向我们发送一个实时的RTSIGNALLEASE信号,告诉我们内核正在破坏你加持在文件上的读写锁。这样在程序访问非法内存并且被SIGBUS杀死之前,你的write系统调用会被中断。write会返回已经写入的字节数,并且置errno为success。5 n3 I! J7 k! b 我们应该在mmap文件之前加锁,并且在操作完文件后解锁:0 r. i9 ?. Z) h7 S3 I( f1 ^: E# ]
$ n/ @3 j$ e$ R8 N 从2.1版内核开始,Linux引入了sendfile来简化操作:
sendfile系统调用过程 在我们调用sendfile时,如果有其它进程截断了文件会发生什么呢?假设我们没有设置任何信号处理程序,sendfile调用仅仅返回它在被中断之前已经传输的字节数,errno会被置为success。如果我们在调用sendfile之前给文件加了锁,sendfile的行为仍然和之前相同,我们还会收到RTSIGNALLEASE的信号。1 n) S- u8 v0 L+ S8 }8 s0 P 目前为止,我们已经减少了数据拷贝的次数了,但是仍然存在一次拷贝,就是页缓存到socket缓存的拷贝。那么能不能把这个拷贝也省略呢?1 D4 l* Z6 W4 | 借助于硬件上的帮助,我们是可以办到的。之前我们是把页缓存的数据拷贝到socket缓存中,实际上,我们仅仅需要把缓冲区描述符传到socket缓冲区,再把数据长度传过去,这样DMA控制器直接将页缓存中的数据打包发送到网络中就可以了。" \0 O( u5 T$ G$ v( s& b 7 S0 m( Y/ s' C n _2 g3 E* M 1 R3 \# |# O$ K 总结一下,sendfile系统调用利用DMA引擎将文件内容拷贝到内核缓冲区去,然后将带有文件位置和长度信息的缓冲区描述符添加socket缓冲区去,这一步不会将内核中的数据拷贝到socket缓冲区中,DMA引擎会将内核缓冲区的数据拷贝到协议引擎中去,避免了最后一次拷贝。
带DMA的sendfile _7 H2 W7 A. d 不过这一种收集拷贝功能是需要硬件以及驱动程序支持的。 + Q4 `3 Y( E# d( `" } ! Y8 h5 @( K+ L' k 05 使用splice 8 V/ V3 f7 J, h4 c) H! j sendfile只适用于将数据从文件拷贝到套接字上,限定了它的使用范围。Linux在2.6.17版本引入splice系统调用,用于在两个文件描述符中移动数据:. r# S8 ~5 E; H9 q. {6 t7 q
9 F- s2 @" o/ ^8 P# Z , {3 g7 f- N) w
2 G" }& h+ z0 `8 P' g* e ! w4 ^/ D8 S8 h' ?0 @/ F& G 由于篇幅原因,本文不详细介绍写时复制。大概描述下就是:如果多个程序同时访问同一块数据,那么每个程序都拥有指向这块数据的指针,在每个程序看来,自己都是独立拥有这块数据的,只有当程序需要对数据内容进行修改时,才会把数据内容拷贝到程序自己的应用空间里去,这时候,数据才成为该程序的私有数据。如果程序不需要对数据进行修改,那么永远都不需要拷贝数据到自己的应用空间里。这样就减少了数据的拷贝。写时复制的内容可以再写一篇文章了。。。" C8 }3 B2 `9 Y; \ 2 Q7 i+ I, a6 \ A8 R8 Y9 X' ] ( k& a- J7 x+ A& g3 V' ]5 \' v0 c+ a 除此之外,还有一些零拷贝技术,比如传统的Linux I/O中加上O_DIRECT标记可以直接I/O,避免了自动缓存,还有尚未成熟的fbufs技术,本文尚未覆盖所有零拷贝技术,只是介绍常见的一些,如有兴趣,可以自行研究,一般成熟的服务端项目也会自己改造内核中有关I/O的部分,提高自己的数据传输速率。 ( ~8 c4 E3 o( N* C* ?; l( |6 n% Y! G |
微信公众号
手机版