缓冲区设计
一、简介
在网络通讯中,用户态缓冲区和内核态缓冲区的大小设定对于优化网络性能和确保数据传输可靠性至关重要。下图是网路通讯的内核缓冲区使用情况:
数据的读写都需要进行系统调用,从用户态切换到内核态去接收数据,结束后需要切换回用户态;
二、网络数据的传输过程:
- 发送
- 应用程序通过系统调用将用户数据拷贝sk_buff,并放到socket的发送缓冲区中
- 网络协议栈从socket法案送缓冲区中取出sk_buffer,并克隆一个新的sk_buffer(用于丢失重传)
- 向下依次增加TCP/UDP头部、IP头部、帧头(MAC头)、帧尾,(tcp层进行数据分段、IP层进行数据分片)
- 触发软件中断,通知网卡驱动程序有新的网络数据需要发送
- 网卡驱动程序从发送队列中取出sk_buffer写到ringbuffer(内存DMA区域)
- 网卡发送数据,发送成功后触发硬件中断,释放sk_buffer和ringbuffer没存
- 当接收到tcp报文的ack应答后释放sk_buffer
- 接收
- 网卡接收到数据包,通过DMA协处理器将数据写入内存ringbuffer结构中
- 网卡向cpu发起硬件中断,cpu收到中断请求,根据中断表查找中断处理函数,进行中断处理
- 中断处理函数将屏蔽中断,发起软件中断
- 内核ksoftirqd软件中断线程负责软件中断处理,该线程从ringbuffer中逐个取出数据帧到sk_buffer
- 从帧头取出ip协议,去掉帧头帧尾
- 根据协议五元组找到socket,并将数据取出放到sicket的接收缓冲区中,软件中断处理结束后开启硬件中断
- 应用程序通过系统调用将socket中的接收缓冲区的数据拷贝到用户层缓冲区
用户态缓冲区(User-Space Buffer)
用户态缓冲区是指在应用程序地址空间中分配的缓冲区,用于存储待发送或已接收的数据。调整用户态缓冲区的大小可以影响应用程序处理数据的能力,特别是在高吞吐量或低延迟要求的场景中。
-
在C/C++中,可以通过setsockopt函数来调整TCP或UDP套接字的发送(SO_SNDBUF)和接收(SO_RCVBUF)缓冲区大小。例如:
1int snd_buff_size = 8192; // 自定义的发送缓冲区大小 2int rcv_buff_size = 16384; // 自定义的接收缓冲区大小 3setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &snd_buff_size, sizeof(snd_buff_size)); 4setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcv_buff_size, sizeof(rcv_buff_size));
-
注意,实际设置的缓冲区大小可能会受到操作系统限制,并且可能不是精确设置的值。有时,内核可能会调整至最接近的允许值,通常是用户请求值的两倍。
三、内核态缓冲区(Kernel-Space Buffer)
内核态缓冲区是操作系统内核管理的一部分,用于在网络接口和用户空间之间暂存数据。它的大小直接影响到网络数据包的处理效率,尤其是在网络拥塞或高流量情况下。
-
对于Linux系统,可以通过修改内核参数来调整某些类型的内核缓冲区大小,例如通过编辑
/etc/sysctl.conf
文件或使用sysctl
命令动态调整。例如,调整网络接收缓冲区的默认最小值和最大值:# 动态调整 sudo sysctl -w net.core.rmem_default=xxxx # 设定默认接收缓冲区大小 sudo sysctl -w net.core.rmem_max=yyyy # 设定最大接收缓冲区大小 # 或者永久修改,在/etc/sysctl.conf中添加: net.core.rmem_default=xxxx net.core.rmem_max=yyyy
-
对于特定设备,如串口,可以通过Linux命令行工具(如
stty
)或者编程方式来调整其缓冲区大小。
设置原则
- 缓冲区大小的选择应基于实际应用场景,包括网络带宽、期望的延迟、以及可能遇到的网络拥塞情况。
- 通常,较大的缓冲区可以减少数据包丢失,但在高流量下可能导致更高的延迟,因为数据在缓冲区中排队等待处理的时间更长。
- 较小的缓冲区可以降低延迟,但在网络拥塞时更容易导致数据包丢失。
- 最佳实践是通过测试和监控来确定最适合的缓冲区大小,有时这可能需要多次调整和验证。
四、缓冲区的设计
- read/write阻塞io,通过阻塞线程的方式等待io就绪
- reactor网络模型,io多路复用检测多路连接io是否就绪,在事件循环中依次处理操作io
- proactor网络模型,异步io,内核完成检测io以及操作io,等待完成后向用户层抛出完成通知
以上这几种网络都对用户态的缓冲区设计没有影响
1、设计方案:
-
定长的buffer
- 优点:结构简单
- 缺点:频繁挪动数据;没有扩容和缩容机制
-
环形缓冲区ringbuffer
- 优点:环形结构,不需要挪动数据
- 缺点:空间不连续;没有扩容和缩容机制
结构设计入下
-
chainbuffer
-
优点:不需要挪动数据,可以实现动态扩容和缩容
结构设计图如下:
-
2、环形缓冲区
下面主要介绍一下ringbufeer,其数据结构为
struct ringbuffer_s {
uint32_t size; // 环形结构的大小
uint32_t tail; // 尾指针
uint32_t head; // 头指针
uint8_t * buf; // 数据指针
};
创建环形缓冲区
// 用于将num值扩展大最近比num值大的2^n的值,用于后面计算优化(将取余操作优化为位运算操作)
static inline uint32_t roundup_power_of_two(uint32_t num) {
if (num == 0) return 2;
int i = 0;
for (; num != 0; i++)
num >>= 1;
return 1U << i;
}
buffer_t * buffer_new(uint32_t sz) {
if (!is_power_of_two(sz)) sz = roundup_power_of_two(sz);
// 创建buffer时大小要加上结构体本身的大小
buffer_t * buf = (buffer_t *)malloc(sizeof(buffer_t) + sz);
if (!buf) {
return NULL;
}
buf->size = sz;
buf->head = buf->tail = 0;
buf->buf = (uint8_t *)(buf + 1);
return buf;
}
添加数据
int buffer_add(buffer_t *r, const void *data, uint32_t sz) {
if (sz > rb_remain(r)) {
return -1;
}
uint32_t i;
// 当前位置到尾部的空间大小是否满足插入数据的大小
i = min(sz, r->size - (r->tail & (r->size - 1)));
// 插入到数据尾部
memcpy(r->buf + (r->tail & (r->size - 1)), data, i);
// 剩余的数据插到头部
memcpy(r->buf, data+i, sz-i);
r->tail += sz;
return 0;
}
数据获取移除
// 数据移除只需要移动指针的位置即可
int buffer_remove(buffer_t *r, void *data, uint32_t sz) {
assert(!rb_isempty(r));
uint32_t i;
sz = min(sz, r->tail - r->head);
i = min(sz, r->size - (r->head & (r->size - 1)));
// 将数据拷贝到data中
memcpy(data, r->buf+(r->head & (r->size - 1)), i);
memcpy(data+i, r->buf, sz-i);
// 更新数据头指针位置,移除数据
r->head += sz;
return sz;
}
数据空间位置调整
// 将数据挪动到头部位置,使得数据保持连续
uint8_t * buffer_write_atmost(buffer_t *r) {
// 使用位操作代替取余操作,size必须要是2^n
uint32_t rpos = r->head & (r->size - 1);
uint32_t wpos = r->tail & (r->size - 1);
if (wpos < rpos) {
// 数据不连续,挪动数据,使其保持连续
uint8_t* temp = (uint8_t *)malloc(r->size * sizeof(uint8_t));
memcpy(temp, r->buf+rpos, r->size - rpos);
memcpy(temp+r->size-rpos, r->buf, wpos);
free(r->buf);
r->buf = temp;
return r->buf;
}
// 返回数据起始位置
return r->buf + rpos;
}
专属学习链接:https://xxetb.xetslk.com/s/36yiy3