MMAP介绍
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。
普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问。
传统Linux中 I/O 的问题
传统的 Linux 系统的标准 I/O 接口(read、write)是基于数据拷贝的,也就是数据都是 copy_to_user 或者 copy_from_user,这样做的好处是,通过中间缓存的机制,减少磁盘 I/O 的操作,但是坏处也很明显,大量数据的拷贝,用户态和内核态的频繁切换,会消耗大量的 CPU 资源,严重影响数据传输的性能.
统计表明,在Linux协议栈中,数据包在内核态和用户态之间的拷贝所用的时间甚至占到了数据包整个处理流程时间的57.1%。
零拷贝
什么是零拷贝?
零拷贝就是上述问题的一个解决方案,通过尽量避免拷贝操作来缓解 CPU 的压力。
零拷贝并没有真正做到“0”拷贝,它更多是一种思想,很多的零拷贝技术都是基于这个思想去做的优化。
原始数据拷贝操作 MMAP:Memory Mapping SENDFILE DMA辅助的SENDFILE通过 send 和 read 来收发数据包,你需要牢记以下两点:
对于 send 来说,返回成功仅仅表示数据写到发送缓冲区成功,并不表示对端已经成功收到。
对于 read 来说,需要循环读取数据,并且需要考虑 EOF 等异常条件。
网络编程中为什么要循环读取数据呢?
因为数据像流水一样,不会结束,所以叫做stream流。
TCP的报文会被封装成一个一个TCP包,每个包都有一个sequence序列号,每个包里包含了一定的字节,当这个包被接收端接收(放到接收缓冲区中),接收端发送一个ACK,这个ACK和sequence对应,这样服务端就可以知道哪些包被接收,哪些包没有被接收。
我们以400为包大小,发送了三个ACK,就可以认为1200字节发送结束。
服务端是不需要知道数据是否发送完毕的,因为TCP是一个流式的,没有办法知道客户端下个时刻还会不会发送数据,服务端只要告诉客户端我收到了1200字节就可以了。
你不妨思考一下:
1. 既然缓冲区如此重要,我们可不可以把缓冲区搞得大大的,这样不就可以提高应用程序的吞吐量了么?
2. 总结一下,一段数据流从应用程序发送端,一直到应用程序接收端,总共经过了多少次拷贝?
无限增大缓冲区肯定不行,write函数发送数据只是将数据发送到内核缓冲区,而什么时候发送由内核决定。
内核缓冲区总是充满数据,同时网络的传输大小MTU也会限制每次发送的大小,最后由于数据堵塞需要消耗大量内存资源,资源使用效率不高。
增大一些是可以提高系统的效率,一定程度上减少了write/send调用,减少了用户空间和内核之间的切换。
但是并不能增大吞吐量,毕竟内核的缓冲区并不能跟用户空间的缓冲区保持同步增大,把内核缓冲区总是满满的会增加粘包的频率和概率。
从为什么使用缓存这个角度考虑。
内核协议栈不确定用户一次要发多少数据,如果用户来一次就发一次,如果数据多还好说,如果少了,那网络I/O很频繁,而真正发送出去的数据也不多,所以为了减少网络I/O使用了缓存的策略。
为啥不能无限大呢,网卡一次发出去的数据报它是有一个最大长度的,所以你不管累积再多数据最后还是要分片发送的,这样一来缓冲区太大也没什么意义,而且数据传输也是有延时要求的,不可能总是在缓冲区里待着等数据,这样就总会有空出来的缓冲区存放新数据,所以无限大缓冲区也没意义,反而还浪费资源。
提及复制几次,是为了引出零拷贝。直接由用户缓冲区复制到网卡DMA区,减少了中间经由内核缓冲区中转的过程。
参考
8 张图,搞懂「零拷贝」
https://zhuanlan.zhihu.com/p/258513662
什么是 “零拷贝” ?
https://www.cnblogs.com/springforall/p/12143052.html
nginx之IO五种模型和select与epoll工作原理
https://www.cnblogs.com/struggle-1216/p/12021779.html
网络编程实战
https://time.geekbang.org/column/article/116043?utm_source=related_read&utm_medium=article&utm_term=related_read
网友评论