linux缓冲区概念梳理

缓冲区

我们在linux的开发过程中经常会接触到缓冲的概念。缓冲一般与输入输出联系在一起,因为我们知道I/O设备的速度与内存和CPU的速度往往有好几个数量级的差异。如果在某一个进程执行时,涉及到I/O相关的操作,等待I/O的操作完全执行完成再继续执行,会让CPU和内存在大量时候都处于空闲的状态。故而需要引入缓冲区这一中间层,相当于CPU,内存 与 I/O设备之间的缓冲地带。对应用程序而言,将数据放入到缓冲区,就认为大功告成,后续将缓冲区中的数据具体交付到I/O设备,是由内核或者C库来负责调度完成的。这一中间层可以大大提高应用程序的执行效率。

具体而言,缓冲区也有很多分类,根据目标的I/O设备来区分,可以有文件缓冲,以及网络缓冲,这两块也是所有I/O操作中最为重要的;根据缓冲区所在的层次分,可以有用户态缓冲,以及内核态的缓冲,而对于不同的I/O设备而言,不同的层次的缓冲的具体含义又是不尽相同的;而因为缓冲区是与I/O设备相关的,自然又涉及到输入缓冲与输出缓冲,一般而言输入缓冲基本由内核来进行管理,我们涉及更多的一般是输出缓冲。

文件缓冲区
用户态文件缓冲

我们先从文件缓冲开始说。我个人是嵌入式工程师,下文一般以C来举例,有读过《unix环境高级编程》的人会记得,前几章会先后介绍两类文件的输入输出方式,一是直接I/O,也就是read/write系统调用,二是带缓冲区的I/O(也叫STD I/O),也就是fread/fwrite,我们在helloworld中接触的 printf和sscanf当然也属于这一类,只是限定了I/O的对象是终端标准输入输出。fread和fwrite由标准C库实现,这里的带缓冲区,自然是指用户态的缓冲区。

标准缓冲存在的意义,是因为write和read属于系统调用,每次执行都需要从用户态陷入内核态,而这是需要一定的开销的,如果一次陷入,只拷贝几个字节,下次也是几个字节,就会有比较大的一个内核态和用户态切换的时钟浪费。而带缓冲区的的I/O,对文件描述符fd进行了封装,得到了FILE类型的句柄,对写入的数据进行管理。

这里的缓冲方式有三种,分别是全缓冲,行缓冲和无缓冲,可以由setbuf和setvbuf系统调用通过改变fd对应的属性进行设置。

无缓冲顾名思义,与write是一样的,stderr标准错误默认使用的是无缓冲。行缓冲是在遇到换行符\n时刷入缓冲区。全缓冲执行刷入缓冲区的情况有三种,一是用户态缓冲区被填满,二是fflush调用冲刷缓冲区,三是fclose调用关闭FILE句柄。缓冲区大小由 stdio.h 头文件中的宏 BUFSIZ 定义,如果希望查看它的大小,包含头文件,直接输出它的值即可,默认是8192字节。此即为文件缓冲在用户态下的输出缓冲的基本特性。

内核态文件缓冲

而不管是通过write还是fwrite将数据写入到文件,仍旧是要经过一层内核的缓冲的,这层就是文件缓冲的内核缓冲区。我们通过free命令可以看到两块内容,一个是cache,一个是buffer。buffer就是写缓冲,为了解决写磁盘的效率。写时会先写到buffer块中,再定期地sync,也就是同步数据到磁盘的块上。这同时减少磁盘的擦写次数。而cache的存在自然是为了提高读磁盘中文件的效率,会按照一定的策略缓存磁盘上应用程序的页到内存中,根据临时局部性原理提高读取文件的效率。在某些时候,我们可以手动调用sync,将内存buffer中的文件落到磁盘上,避免在某些时候异常掉电或者其他原因导致的数据不一致性,linux系统中的update的系统守护进程会周期性地(一般每隔30秒)调用sync函数,另外在系统关机时自然也是会调用sync的。为了让应用程序保证数据一致性的处理,内核针对内核缓冲区的控制,提供了3个系统调用,sync,fsync和fdatasync,比如在数据库操作中,事务操作在实现中需要严格保证在返回时数据已经可靠落盘,并要应对掉电异常等场景,就会调用fsync或者fdatasync对写入到内核中的数据进行强制同步。此即为文件缓冲在内核态下的输出缓冲的基本特性。

网络缓冲
与文件缓冲的核心区别

与文件平起平坐的输入输出设备,自然是网络设备。在介绍网络缓冲区时,我们优先介绍,网络缓冲在内核层面的缓冲区,因为在用户态层面,网络缓冲并没有像文件缓冲那样,提供一套通用的输入输出缓冲机制。绝大部分用户态下的网络缓冲机制,是主流的一些网络库各自独立实现的,这我会在之后简单介绍几种策略。

内核态网络缓冲

针对网络缓冲在内核层面的输入输出缓冲区,其实更为准确的说法应该是发送缓冲区和接收缓冲区。因为这里的输入输出设备,一般指代的是已经与对端建立连接TCP四元组套接字。

我们知道TCP有滑动窗口控制协议,会控制目前在途的TCP分节的数量,而不是用户态给多少,内核态就一股脑儿地发多少,与此同时,一段时间内的吞吐量还受到对端处理程序的处理能力的限制,如果一直处于忙碌状态,则无法从接收缓冲区中将数据包接收到内核态中进行处理,发送这端的套接字如果缓冲区也已经填满,就会在用户态下呈现为无法发送的状态。即使假定对端处理能力非常强,网络设备(网卡)的发送速率一般是有限的,比如目前常见家用网络设备的带宽的是100Mbps和1000Mbps。用户态填入到内核缓冲区的数据被发送出去的速度,也是受这个限制的,不仅如此,一台主机上有几十个套接字已经建立连接是非常常见的,而网卡一般只有一张,故而某个特定的socket四元组还需要与所有的的套接字 网卡的输入输出带宽。故而必然会需要内核缓冲区的存在,让用户态调用send调用给下来的,但是还没有办法发送出去的数据暂存下来,这里缓冲区设计的初衷可能与 涉及到文件的磁盘I/O为了效率和提高磁盘读写寿命的初衷是不同的。

关于网络缓冲区的配置,在系统整体层面或者是单个套接字层面,都有默认的经验值。针对单个套接字的发送和接收缓冲区的大小,可以通过 setsockopt接口和 SO_SNDBUF选项,以及SO_RCVBUF选项来控制。

阻塞与非阻塞的区别

网络缓冲区与文件缓冲区一样,是为了应对I/O设备与内存和CPU的速度不匹配而存在,在网络内核缓冲区已满的情况下,显然用户态是无法继续写入的,这与文件缓冲区不同,但很相似。在针对网络套接字描述符而言,在内核已满的情况,就存在两种不同的场景,阻塞与非阻塞,阻塞顾名思义就是陷入到内核态,等待到缓冲区空闲后,将要写入到的内容拷贝到内核的空闲缓冲区,而非阻塞则是在内核缓冲区已满时直接返回,EAGAINBUF,指示发送缓冲区已满,相当于套接字不可写,以等待用户态下的程序后续的处理。而对于文件描述符而言,就我所知是不存在这种限制的。

用户态下的网络缓冲区

对于有大量请求需要发送的场景,显然会出现大量缓冲区满的情况,如果此时套接字为阻塞,则程序会经常处于阻塞的状态,执行效率显然会大大下降。故而在我的接触的网络库,比如libevent库和mqtt的库,都提供了针对非阻塞套接字的异步的发送方式。即针对网络套接字在用户态下的缓冲区。

这里以MQTT为例,MQTT中的发送为MQTT_Asyncsend,在调用后会立即返回,具体的套接字发送流程为内部封装,实现时将待发送的地址和长度加入到队列中,在套接字可写时,从发送队列的头部取出节点,将内容写入到套接字,如果写入完成,则将节点移出待发送队列,并清理之前留存的缓冲区。在不可写时,则通过多路复用等待套接字可操作,或转而进行其他线程的处理。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值