IO 缓冲区管理实例详解
linux系统io中write原型为 ssize_t write(int filedes, const void * buff, size_t nbytes) ;
当调用write写数据的时候,调用完成后write直接返回,但是磁盘是个慢速设备,操作系统会将数据保存在内核中的缓冲区中,并负责异步地将数据写至磁盘。当然如果此时系统宕机了则会丢失数据。write是系统调用,每次调用都会陷入内核,所以选取一个合适的块长度buffsize,并尽量减少它的调用可以优化效率。在ANSI C的标准IO中我们调用printf/fprintf/fputs等会以流的方式进行处理,我们只需要写入流中,而不用像write一样选择一个buffsize,因为标准IO库帮我们处理了很多细节,例如缓冲区分配,以优化长度执行IO等。这样的话就会减少wirte/read系统调用的数量,提高效率。但是与此同时会引入另外一个问题:数据拷贝,例如当使用函数fgets和fputs时,通常需要经过两次缓冲区:一次是标准IO缓冲区,还有一次是调用read和write的内核缓冲区。但是总的来说使用标准IO相对于系统IO来说接口简单,且效率相当。
标准IO提供了三种类型的缓冲区:全缓存,行缓存和不带缓存,全缓存只有在缓冲区满时才会主动flush,通常用在对一个磁盘文件IO。行缓存在缓冲区中遇到换行符就会flush,还有一种情况是需要从标准输入输出得到输入数据时也会flush缓冲区,行缓存一般用在交互的终端中。不带缓存则相当于直接 write系统调用输出,标准出错流stderr通常是不带缓存的,这就使得出错信息可以尽快显示出来。除了默认的flush条件外,显式调用fflush函数和程序正常终止时也会flush缓冲区。我们可以使用setbuf/setvbuf来更改默认的缓冲区长度,参见APUE 5.4节。
在使用标准IO的程序中,当我们将一个标准输出重新定向到一个文件时,会将行缓存变为全缓存,在某些情况下可能会导致一些非预期错误,比如调用printf(“*****\n”)时,当以交互方式运行该程序时,会正常输出。但是当将标准输出重新定向到一个文件时,缓冲区区变为全缓存,printf就不会正常输出,该行数据仍在缓冲区中。如果此时再fork一个子进程,数据空间被复制到子进程中时,该缓冲区数据也被复制到子进程中。接着在子进程中如果输出则会刷新之前在缓冲区的内容,产生一些非预期的输出。
在网络编程中,应该直接使用系统IO,标准IO为提升性能而引入缓冲机制增加了网络应用程序的复杂性。并且,某种意义上说标准IO流是全双工的,能同时执行输入和输出,然而对流的限制和对套接字的限制,有时候会互相冲突。(参见CSAPP P611)
某些高级的网络库中(比如说muduo库)在使用系统IO的基础上会创建自己的缓冲区,帮助用户屏蔽系统IO的某些不便,例如调用write发送大量数据的时候,发送缓冲区满时需要应用层等待,read接收数据的时候粘包和数据接受的缓慢。当增加应用层缓冲区后,由网络库处理这些实现细节,简化用户操作。
Linux还提供了零拷贝技术来减少内存拷贝,进而提升效率,我们知道利用read/write从磁盘发送数据到网卡会经过四次拷贝操作:当应用程序需要访问某块数据的时候,操作系统内核会先检查这块数据是不是因为前一次对相同文件的访问而已经被存放在操作系统内核地址空间的缓冲区内,如果在内核缓冲区中找不到这块数据,Linux 操作系统内核会先将这块数据从磁盘读出来放到操作系统内核的缓冲区里。如果这个数据读取操作是由 DMA 完成的,那么在 DMA 进行数据读取的这一过程中,CPU 只需要进行缓冲区管理,以及创建和处理 DMA ,除此之外,CPU 不需要再做更多的事情,DMA 执行完数据读取操作之后,会通知操作系统做进一步的处理。Linux 操作系统会根据 read系统调用指定的应用程序地址空间的地址,把这块数据存放到请求这块数据的应用程序的地址空间中去,待用户对数据完成操作后,操作系统需要将数据再一次从用户应用程序地址空间的缓冲区拷贝到与网络堆栈相关的内核缓冲区中去,这个过程也是需要占用 CPU 的。数据拷贝操作结束以后,数据会被打包,然后发送到网络接口卡上去。从上面的描述可以看出,在这种传统的数据传输过程中,数据至少发生了四次拷贝操作,即便是使用了 DMA 来进行与硬件的通讯,CPU 仍然需要访问数据两次。
(ps:记得之前看过一个面试题说是printf输出过程经过几次缓冲区,现在大家明白了吧!)
使用零拷贝技术可以避免数据在系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区进行拷贝。有时候,应用程序在数据传输的过程中不需要对数据进行访问,传输的数据可以不用复制到用户应用区,直接通过内核发送到网卡就可以,这样可以提高性能,而此时就需要零拷贝技术。linux下可以用mmap,sendfile,splice实现零拷贝。
以上就是IO缓冲区管理的实例详解的详细内容,更多请关注php中文网其它相关文章!
推荐阅读
-
IO 缓冲区管理实例详解
-
[软考] 关键路径法 CPM 详解与应用,实例解析集锦(信息系统项目经理--时间管理)
-
[软考]计划评审技术 PERT 详解与应用,实例解析集锦(信息系统项目经理--时间管理)--开场白
-
Oracle Crystal Ball - 风险管理神器 (理论与实战深度解析 - 中级实例详解)
-
【Netty】「萌新入门」(七)ByteBuf 的性能优化-堆内存的分配和释放都是由 Java 虚拟机自动管理的,这意味着它们可以快速地被分配和释放,但是也会产生一些开销。 直接内存需要手动分配和释放,因为它由操作系统管理,这使得分配和释放的速度更快,但是也需要更多的系统资源。 另外,直接内存可以映射到本地文件中,这对于需要频繁读写文件的应用程序非常有用。 此外,直接内存还可以避免在使用 NIO 进行网络传输时发生数据拷贝的情况。在使用传统的 I/O 时,数据必须先从文件或网络中读取到堆内存中,然后再从堆内存中复制到直接缓冲区中,最后再通过 SocketChannel 发送到网络中。而使用直接缓冲区时,数据可以直接从文件或网络中读取到直接缓冲区中,并且可以直接从直接缓冲区中发送到网络中,避免了不必要的数据拷贝和内存分配。 通过 ByteBufAllocator.DEFAULT.directBuffer 方法来创建基于直接内存的 ByteBuf: ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); 通过 ByteBufAllocator.DEFAULT.heapBuffer 方法来创建基于堆内存的 ByteBuf: ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); 注意: 直接内存是一种特殊的内存分配方式,可以通过在堆外申请内存来避免 JVM 堆内存的限制,从而提高读写性能和降低 GC 压力。但是,直接内存的创建和销毁代价昂贵,因此需要慎重使用。 此外,由于直接内存不受 JVM 垃圾回收的管理,我们需要主动释放这部分内存,否则会造成内存泄漏。通常情况下,可以使用 ByteBuffer.clear 方法来释放直接内存中的数据,或者使用 ByteBuffer.cleaner 方法来手动释放直接内存空间。 测试代码: public static void testCreateByteBuf { ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass); ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); System.out.println(heapBuf.getClass); ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); System.out.println(directBuf.getClass); } 运行结果: class io.netty.buffer.PooledUnsafeDirectByteBuf class io.netty.buffer.PooledUnsafeHeapByteBuf class io.netty.buffer.PooledUnsafeDirectByteBuf 池化技术 在 Netty 中,池化技术指的是通过对象池来重用已经创建的对象,从而避免了频繁地创建和销毁对象,这种技术可以提高系统的性能和可伸缩性。 通过设置 VM options,来决定池化功能是否开启: -Dio.netty.allocator.type={unpooled|pooled} 在 Netty 4.1 版本以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现; 这里我们使用非池化功能进行测试,依旧使用的是上面的测试代码 testCreateByteBuf,运行结果如下所示: class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf 可以看到,ByteBuf 类由 PooledUnsafeDirectByteBuf 变成了 UnpooledUnsafeDirectByteBuf; 在没有池化的情况下,每次使用都需要创建新的 ByteBuf 实例,这个操作会涉及到内存的分配和初始化,如果是直接内存则代价更为昂贵,而且频繁的内存分配也可能导致内存碎片问题,增加 GC 压力。 使用池化技术可以避免频繁内存分配带来的开销,并且重用池中的 ByteBuf 实例,减少了内存占用和内存碎片问题。另外,池化技术还可以采用类似 jemalloc 的内存分配算法,进一步提升分配效率。 在高并发环境下,池化技术的优点更加明显,因为内存的分配和释放都是比较耗时的操作,频繁的内存分配和释放会导致系统性能下降,甚至可能出现内存溢出的风险。使用池化技术可以将内存分配和释放的操作集中到预先分配的池中,从而有效地降低系统的内存开销和风险。 内存释放 当在 Netty 中使用 ByteBuf 来处理数据时,需要特别注意内存回收问题。 Netty 提供了不同类型的 ByteBuf 实现,包括堆内存(JVM 内存)实现 UnpooledHeapByteBuf 和堆外内存(直接内存)实现 UnpooledDirectByteBuf,以及池化技术实现的 PooledByteBuf 及其子类。 UnpooledHeapByteBuf:通过 Java 的垃圾回收机制来自动回收内存; UnpooledDirectByteBuf:由于 JVM 的垃圾回收机制无法管理这些内存,因此需要手动调用 release 方法来释放内存; PooledByteBuf:使用了池化机制,需要更复杂的规则来回收内存; 由于池化技术的特殊性质,释放 PooledByteBuf 对象所使用的内存并不是立即被回收的,而是被放入一个内存池中,待下次分配内存时再次使用。因此,释放 PooledByteBuf 对象的内存可能会延迟到后续的某个时间点。为了避免内存泄漏和占用过多内存,我们需要根据实际情况来设置池化技术的相关参数,以便及时回收内存; Netty 采用了引用计数法来控制 ByteBuf 对象的内存回收,在博文 「源码解析」ByteBuf 的引用计数机制 中将会通过解读源码的形式对 ByteBuf 的引用计数法进行深入理解; 每个 ByteBuf 对象被创建时,都会初始化为1,表示该对象的初始计数为1。 在使用 ByteBuf 对象过程中,如果当前 handler 已经使用完该对象,需要通过调用 release 方法将计数减1,当计数为0时,底层内存会被回收,该对象也就被销毁了。此时即使 ByteBuf 对象还在,其各个方法均无法正常使用。 但是,如果当前 handler 还需要继续使用该对象,可以通过调用 retain 方法将计数加1,这样即使其他 handler 已经调用了 release 方法,该对象的内存仍然不会被回收。这种机制可以有效地避免了内存泄漏和意外访问已经释放的内存的情况。 一般来说,应该尽可能地保证 retain 和 release 方法成对出现,以确保计数正确。