网络是用物理链路将各个孤立的主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。通信是人与人之间通过某种媒体进行的信息交流与传递。网络通信是通过网络将各个孤立的设备进行连接,通过信息交换实现人与人,人与计算机,计算机与计算机之间的通信。
以上是对网络通讯的定义。网络通讯最重要的是通讯协议,我们接触到的有tcp/ip,spx,udp;ftp,telnet,swtp,nfs,http。前三个位于传输层,后几个位于应用层。一般我们接触不到。那是因为操作系统都帮我们处理好了。操作系统确实优秀。那我们来聊聊操作系统为我们带来哪些通讯方式。
首先是底层硬盘和内存之前的通讯。
PIO和DMA(I/O设备和内存之间数据传输方式)
PIO我们拿磁盘来说,很早以前,磁盘和内存之间的数据传输是需要CPU控制的,也就是说如果我们读取磁盘文件到内存中,数据要经过CPU存储转发,这种方式称为PIO。显然这种方式非常不合理,需要占用大量的CPU时间来读取文件,造成文件访问时系统几乎停止响应。
DMADMA(直接内存访问,Direct Memory Access)取代了PIO,它可以不经过CPU而直接进行磁盘和内存的数据交换。在DMA模式下,CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据的传送即可,DMA控制器通过系统总线来传输数据,传送完毕再通知CPU,这样就在很大程度上降低了CPU占有率,大大节省了系统资源,而它的传输速度与PIO的差异其实并不十分明显,因为这主要取决于慢速设备的速度。
缓存IO和直接IO缓存IO缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。如图 2-7
读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。
写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令。
缓存I/O的优点:
1.在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全.
2.可以减少读盘的次数,从而提高性能。
缓存I/O的缺点:
在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样,数据在传输过程中需要在应用程序地址空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
2.直接IO
直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。
直接IO的缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载会非常缓存。通常直接IO与异步IO结合使用,会得到比较好的性能。(异步IO:当访问数据的线程发出请求之后,线程会接着去处理其他事,而不是阻塞等待)如图 2-8
mmap与sendfile1.mmap
描述:
linux设计了mmap() api来实现内存映射。而“零拷贝”技术正是打破了操作系统的设计理念,从CPU层面打破Linux内核与用户空间的阻隔,使其内存共享。
Linux内核提供一种访问磁盘文件的特殊方式,它可以将内存中某块地址空间和我们要指定的磁盘文件相关联,从而把我们对这块内存的访问转换为对磁盘文件的访问,这种技术称为内存映射(Memory Mapping)
当要访问内存中的一段数据时,转换为访问文件的某一段数据。 这种方式的目的同样是减少数据在用户空间和内核空间之间的拷贝操作。当大量数据需要传输的时候,采用内存映射方式去访问文件会获得比较好的效率。
映射分为两种方式
1.文件映射:磁盘文件映射进程的虚拟地址空间,使用文件内容初始化物理内存。
2.匿名映射:初始化全为0的内存空间。
而对于映射关系是否共享又分为
1.私有映射:多进程之间共享,修改不反应到磁盘实际文件是一个COW的映射方式。
2.共享映射:多进程之间数据共享,修改反应到磁盘实际文件中。
因此总结起来有4种组合
1、私有文件映射 多个进程使用同样的物理内存页进行初始化,但是各个进程对内存文件的修改不会共享,也不会反应到物理文件中
2、私有匿名映射 mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存(malloc分配大内存会调用mmap)。 例如开辟新进程时,会为每个进程分配虚拟的地址空间,这些虚拟地址映射的物理内存空间各个进程间读的时候共享,写的 时候会copy-on-write。
3、共享文件映射 多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件 的修改会反应到实际物理文件中,他也是进程间通信(IPC)的一种机制。
4、共享匿名映射 这种机制在进行fork的时候不会采用写时复制,父子进程完全共享同样的物理内存页,这也就实现了父子进程通信(IPC).
这里值得注意的是,mmap只是在虚拟内存分配了地址空间,只有在第一次访问虚拟内存的时候才分配物理内存。
在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时,通过查找页表,发现虚拟内存对应的页没有在物理内存中缓存,则产生”缺页”,由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存,注意是只加载缺页,但也会受操作系统一些调度策略影响,加载的比所需的多。
从图中可以看出,mmap要比普通的read系统调用少了一次copy的过程。因为read调用,进程是无法直接访问kernel space的,所以在read系统调用返回前,内核需要将数据从内核复制到进程指定的buffer。但mmap之后,进程可以直接访问mmap的数据(page cache)。
mmap应用
可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。
2.sendfile
传统方式:sendfile系统调用在两个文件描述符之间直接传递数据(完全在内核中操作),由操作系统直接将数据从内核页缓存中复制到网卡(socket)缓存区,实现零拷贝。如图2-21
DMA方式:上面介绍的 sendfile() 技术在进行数据传输仍然还需要一次多余的数据拷贝操作,通过引入一点硬件上的帮助,仅有的一次数据拷贝操作也可以避免。为了避免操作系统内核造成的数据副本,需要用到一个支持收集操作的网络接口。 sendfile() 利用 DMA 引擎将文件内容拷贝到内核缓冲区;然后将带有文件位置和长度信息的缓冲区描述符添加到 socket 缓冲区中去,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,DMA 引擎会将数据直接从内核缓冲区拷贝到协议引擎中去,这样就避免了最后一次数据拷贝。
sendfile应用
适合较大的静态文件传输。由于不会涉及到用户内存,所以相对于mmap,占用内存少。也正因为没有经过用户内存,所以sendfile不适合对传输数据有变化场景。
上述的几种I/O操作对比:
1.传统I/O 硬盘—>内核缓冲区—>用户缓冲区—>内核socket缓冲区—>协议引擎
2.mmap 硬盘—>内核缓冲区映射到用户缓冲区—>内核socket缓冲区—>协议引擎
3.sendfile 硬盘—>内核缓冲区—>内核socket缓冲区—>协议引擎
4.sendfile( DMA 收集拷贝) 硬盘—>内核缓冲区—>协议引擎
socket缓冲区先了解一下socket(套接字)的概念。socket是一种通讯机制。凭借这种机制不同主机之间的进程可以进行通信。我们可以用socket中的相关函数来完成通信过程。socket有三个特性,域、类型、协议。
域:指internet网络;类型:流套接字、数据报套接字和原始套接字。协议:tcp和udp
每个socket创建后都会被分配两个缓冲区,输入缓冲区和输出缓冲区。
1.缓冲区
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由tcp协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
2.使用write()/send()发送数据
阻塞模式
如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据;
非阻塞模式
send()函数的过程仅仅是将数据拷贝到协议栈的缓冲区而已,如果缓冲区可用空间不够,则尽可能拷贝,返回成功拷贝的大小;如果缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN。
3.使用read()/recv()读取数据
阻塞模式
首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来;
非阻塞模式
接收数据时perror时常遇到“Resource temporarilyunavailable”的提示,errno代码为11(EAGAIN)。这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏socket的同步,继续循环接着recv就可以。
Socket缓冲区还是存在堆外内存中。而不是网卡。网卡中的储存区用来缓存接收到的分组以及要发送出去的分组。网卡速率越快,说明能将缓存的分组推上链路的速度越快,分组的传输时延越低。
补充Java的NIO通过transferTo方法提供零拷贝。Java NIO中有三种ByteBuffer
HeapByteBuffer:ByteBuffer.allocate()使用的就是这种缓冲区,叫堆缓冲区,因为它是在JVM堆内存的,支持GC和缓存优化。但是它不是页对齐的,也就是说如果要使用JNI的方式调用native代码时,JVM会先将它拷贝到页对齐的缓冲空间。
DirectByteBuffer:ByteBuffer.allocateDirect()方法被调用时,JVM使用C语言的malloc()方法分配堆外内存。由于不受JVM管理,这个内存空间是页对齐的且不支持GC,和native代码交互频繁时使用这种缓冲区能提高性能。不过内存分配和销毁的事就要靠你自己了。
MappedByteBuffer:FileChannel.map()调用返回的就是这种缓冲区,这种缓冲区用的也是堆外内存,本质上其实就是对系统调用mmap()的封装,以便通过代码直接操纵映射物理内存数据。
从linux设计上,每个buffer都对应三个指针,有时间把这块也在复习下总结出来。
参考:https://www.jianshu.com/u/852e19e5b833
-
扫码下载安卓APP
-
微信扫一扫关注我们
微信扫一扫打开小程序
手Q扫一扫打开小程序
-
返回顶部
发表评论