用户态缓存:链式缓冲区(Chain Buffer)

目录

[链式缓冲区(Chain Buffer)简介](#链式缓冲区(Chain Buffer)简介)

为什么选择链式缓冲区?

代码解析

[1. 头文件与类型定义](#1. 头文件与类型定义)

[2. 结构体定义](#2. 结构体定义)

[3. 宏定义与常量](#3. 宏定义与常量)

[4. 环形缓冲区的基本操作](#4. 环形缓冲区的基本操作)

[5. 其他辅助函数](#5. 其他辅助函数)

[6. 数据读写操作的详细实现](#6. 数据读写操作的详细实现)

[7. 总结](#7. 总结)

[8. 结合之前的内容](#8. 结合之前的内容)

[9. 具体应用解析](#9. 具体应用解析)

[10. 综合应用](#10. 综合应用)

[11. 总结](#11. 总结)


链式缓冲区(Chain Buffer)简介

链式缓冲区是一种通过链接多个缓冲区块来动态管理数据的结构。相比于固定大小的环形缓冲区,链式缓冲区具有更高的灵活性和可扩展性,特别适用于需要处理不同大小数据包的场景。它通过将数据分散存储在多个缓冲区块中,减少了内存浪费和数据移动的需求。

为什么选择链式缓冲区?

  • 灵活性和可扩展性:链式缓冲区能够动态地添加或移除缓冲区块,适应不同的数据量需求。
  • 减少数据移动:通过分散存储数据,避免了大规模的数据拷贝操作,提高了数据处理效率。
  • 高效内存利用:根据实际数据量动态分配缓冲区块,减少了内存浪费。

代码解析

让我们逐步解析你提供的链式缓冲区代码,理解其各个部分的功能和实现细节。

1. 头文件与类型定义

cpp 复制代码
#ifndef _chain_buffer_h
#define _chain_buffer_h
#include <stdint.h>

typedef struct buf_chain_s buf_chain_t;
typedef struct buffer_s buffer_t;

// Function declarations
buffer_t * buffer_new(uint32_t sz);
uint32_t buffer_len(buffer_t *buf);
int buffer_add(buffer_t *buf, const void *data, uint32_t datlen);
int buffer_remove(buffer_t *buf, void *data, uint32_t datlen);
int buffer_drain(buffer_t *buf, uint32_t len);
void buffer_free(buffer_t *buf);
int buffer_search(buffer_t *buf, const char* sep, const int seplen);
uint8_t * buffer_write_atmost(buffer_t *p);

#endif

1.1 头文件保护符

cpp 复制代码
#ifndef _chain_buffer_h
#define _chain_buffer_h
...
#endif
  • 作用:防止头文件被多次包含,避免重复定义错误。

1.2 类型定义

cpp 复制代码
typedef struct buf_chain_s buf_chain_t;
typedef struct buffer_s buffer_t;
  • 作用 :为结构体 buf_chain_sbuffer_s 定义别名 buf_chain_tbuffer_t,简化后续代码的书写。

2. 结构体定义

cpp 复制代码
struct buf_chain_s {
    struct buf_chain_s *next;
    uint32_t buffer_len;
    uint32_t misalign;
    uint32_t off;
    uint8_t *buffer;
};

struct buffer_s {
    buf_chain_t *first;
    buf_chain_t *last;
    buf_chain_t **last_with_datap;
    uint32_t total_len;
    uint32_t last_read_pos; // for sep read
};

2.1 buf_chain_s 结构体

  • next (struct buf_chain_s *):指向下一个缓冲区块,实现链式结构。
  • buffer_len (uint32_t):缓冲区块的总大小,以字节为单位。
  • misalign (uint32_t):缓冲区块的未对齐偏移量,用于优化内存访问。
  • off (uint32_t):当前缓冲区块中有效数据的长度,以字节为单位。
  • buffer (uint8_t *):指向实际数据存储区的指针。

2.2 buffer_s 结构体

  • first (buf_chain_t *):链表的第一个缓冲区块。
  • last (buf_chain_t *):链表的最后一个缓冲区块。
  • last_with_datap (buf_chain_t **):指向链表中最后一个有数据的缓冲区块的指针的指针,用于快速定位添加新数据的位置。
  • total_len (uint32_t):缓冲区中当前存储的总数据量,以字节为单位。
  • last_read_pos (uint32_t):用于分隔符读取的上次读取位置,优化搜索性能。

3. 宏定义与常量

cpp 复制代码
#define CHAIN_SPACE_LEN(ch) ((ch)->buffer_len - ((ch)->misalign + (ch)->off))
#define MIN_BUFFER_SIZE 1024
#define MAX_TO_COPY_IN_EXPAND 4096
#define BUFFER_CHAIN_MAX_AUTO_SIZE 4096
#define MAX_TO_REALIGN_IN_EXPAND 2048
#define BUFFER_CHAIN_MAX 16*1024*1024  // 16M
#define BUFFER_CHAIN_EXTRA(t, c) (t *)((buf_chain_t *)(c) + 1)
#define BUFFER_CHAIN_SIZE sizeof(buf_chain_t)
  • CHAIN_SPACE_LEN(ch) :计算缓冲区块 ch 中剩余的可用空间。
  • MIN_BUFFER_SIZE:缓冲区块的最小大小,设置为1024字节。
  • MAX_TO_COPY_IN_EXPAND:扩展缓冲区时最多复制的字节数。
  • BUFFER_CHAIN_MAX_AUTO_SIZE:自动扩展缓冲区块的最大大小。
  • MAX_TO_REALIGN_IN_EXPAND:扩展时最大允许重新对齐的字节数。
  • BUFFER_CHAIN_MAX:缓冲区块的最大大小,设置为16MB。
  • BUFFER_CHAIN_EXTRA(t, c):宏,用于获取缓冲区块中的实际数据存储区的指针。
  • BUFFER_CHAIN_SIZE:缓冲区块结构体的大小。

4. 环形缓冲区的基本操作

4.1 创建缓冲区

cpp 复制代码
buffer_t * buffer_new(uint32_t sz) {
    (void)sz;
    buffer_t * buf = (buffer_t *) malloc(sizeof(buffer_t));
    if (!buf) {
        return NULL;
    }
    memset(buf, 0, sizeof(*buf));
    buf->last_with_datap = &buf->first;
    return buf;
}
  • 功能:创建并初始化一个新的链式缓冲区。
  • 步骤
    1. 分配内存,初始化结构体 buffer_t
    2. 使用 memset 清零结构体成员。
    3. 初始化 last_with_datap 指针为指向 first,表示当前没有数据存储。
    4. 返回缓冲区指针。

注意 :虽然函数接收一个大小参数 sz,但在当前实现中未使用(通过 (void)sz; 忽略)。这是因为链式缓冲区通过动态添加缓冲区块来适应不同的数据量需求,而不依赖于单一的固定大小。

4.2 获取缓冲区长度

cpp 复制代码
uint32_t buffer_len(buffer_t *buf) {
    return buf->total_len;
}
  • 功能:返回缓冲区中当前存储的数据长度。
  • 实现 :直接返回 total_len

4.3 释放缓冲区

cpp 复制代码
void buffer_free(buffer_t *buf) {
    buf_chain_free_all(buf->first);
}
  • 功能:释放链式缓冲区中的所有缓冲区块,并释放缓冲区结构体本身。
  • 步骤
    1. 调用 buf_chain_free_all 释放所有缓冲区块。
    2. 注意 :当前实现未释放 buffer_t 本身的内存,可能需要在调用者中进行释放。

4.4 添加数据到缓冲区

cpp 复制代码
int buffer_add(buffer_t *buf, const void *data_in, uint32_t datlen) {
    buf_chain_t *chain, *tmp;
    const uint8_t *data = data_in;
    uint32_t remain, to_alloc;
    int result = -1;
    if (datlen > BUFFER_CHAIN_MAX - buf->total_len) {
        goto done;
    }

    if (*buf->last_with_datap == NULL) {
        chain = buf->last;
    } else {
        chain = *buf->last_with_datap;
    }

    if (chain == NULL) {
        chain = buf_chain_insert_new(buf, datlen);
        if (!chain)
            goto done;
    }

    remain = chain->buffer_len - chain->misalign - chain->off;
    if (remain >= datlen) {
        memcpy(chain->buffer + chain->misalign + chain->off, data, datlen);
        chain->off += datlen;
        buf->total_len += datlen;
        // buf->n_add_for_cb += datlen;
        goto out;
    } else if (buf_chain_should_realign(chain, datlen)) {
        buf_chain_align(chain);

        memcpy(chain->buffer + chain->off, data, datlen);
        chain->off += datlen;
        buf->total_len += datlen;
        // buf->n_add_for_cb += datlen;
        goto out;
    }
    to_alloc = chain->buffer_len;
    if (to_alloc <= BUFFER_CHAIN_MAX_AUTO_SIZE/2)
        to_alloc <<= 1;
    if (datlen > to_alloc)
        to_alloc = datlen;
    tmp = buf_chain_new(to_alloc);
    if (tmp == NULL)
        goto done;
    if (remain) {
        memcpy(chain->buffer + chain->misalign + chain->off, data, remain);
        chain->off += remain;
        buf->total_len += remain;
        // buf->n_add_for_cb += remain;
    }

    data += remain;
    datlen -= remain;

    memcpy(tmp->buffer, data, datlen);
    tmp->off = datlen;
    buf_chain_insert(buf, tmp);
    // buf->n_add_for_cb += datlen;
out:
    result = 0;
done:
    return result;
}
  • 功能:将数据添加到链式缓冲区中。
  • 步骤
    1. 检查缓冲区是否已满
      • 如果要添加的数据 datlen 超过缓冲区的最大允许长度(BUFFER_CHAIN_MAX),则返回错误 -1
    2. 定位当前可用的缓冲区块
      • 如果 last_with_datap 指向 NULL,则使用 last 缓冲区块。
      • 否则,使用 last_with_datap 指向的缓冲区块。
    3. 如果当前缓冲区块为空,则创建一个新的缓冲区块并插入链表。
    4. 计算当前缓冲区块的剩余空间 remain
    5. 数据拷贝
      • 情况1 :如果剩余空间足够,直接将数据拷贝到缓冲区块中,并更新 offtotal_len
      • 情况2 :如果需要重新对齐,并且剩余空间足够,调用 buf_chain_align 重新对齐缓冲区块,然后拷贝数据。
      • 情况3:如果剩余空间不足,创建一个新的缓冲区块,并将部分数据拷贝到当前缓冲区块,剩余数据拷贝到新缓冲区块中。
    6. 更新结果 :成功添加数据后,返回 0

4.5 从缓冲区移除数据

cpp 复制代码
int buffer_remove(buffer_t *buf, void *data_out, uint32_t datlen) {
    uint32_t n = buf_copyout(buf, data_out, datlen);
    if (n > 0) {
        if (buffer_drain(buf, n) < 0)
            n = -1;
    }
    return (int)n;
}
  • 功能:从链式缓冲区中读取并移除数据。
  • 步骤
    1. 调用 buf_copyout 从缓冲区中读取数据到 data_out,读取长度为 datlen
    2. 如果成功读取 (n > 0),则调用 buffer_drain 移除已读取的数据。
    3. 返回实际读取的数据长度 n,如果移除失败,返回 -1

4.6 清空缓冲区的一部分数据

cpp 复制代码
int buffer_drain(buffer_t *buf, uint32_t len) {
    buf_chain_t *chain, *next;
    uint32_t remaining, old_len;
    old_len = buf->total_len;
    if (old_len == 0)
        return 0;

    if (len >= old_len) {
        len = old_len;
        for (chain = buf->first; chain != NULL; chain = next) {
            next = chain->next;
            free(chain);
        }
        ZERO_CHAIN(buf);
    } else {
        buf->total_len -= len;
        remaining = len;
        for (chain = buf->first; remaining >= chain->off; chain = next) {
            next = chain->next;
            remaining -= chain->off;

            if (chain == *buf->last_with_datap) {
                buf->last_with_datap = &buf->first;
            }
            if (&chain->next == buf->last_with_datap)
                buf->last_with_datap = &buf->first;

            free(chain);
        }

        buf->first = chain;
        chain->misalign += remaining;
        chain->off -= remaining;
    }
    
    // buf->n_del_for_cb += len;
    return len;
}
  • 功能 :从链式缓冲区中清除 len 字节的数据,而不读取到用户空间。
  • 步骤
    1. 检查缓冲区是否为空
      • 如果缓冲区为空,返回 0
    2. 清除操作
      • 情况1 :如果要清除的数据 len 大于或等于缓冲区中的总数据量 old_len,则清空整个缓冲区,释放所有缓冲区块,并调用 ZERO_CHAIN 重置缓冲区结构体。
      • 情况2 :如果要清除的数据 len 小于总数据量,则逐个缓冲区块地清除数据,直到清除完 len 字节。
        • 更新 total_len
        • 移除完全清除的缓冲区块。
        • 对部分清除的缓冲区块,更新 misalignoff
    3. 返回 :返回实际清除的数据长度 len

4.7 搜索特定分隔符

cpp 复制代码
int buffer_search(buffer_t *buf, const char* sep, const int seplen) {
    buf_chain_t *chain;
    int i;
    chain = buf->first;
    if (chain == NULL)
        return 0;
    int bytes = chain->off;
    while (bytes <= buf->last_read_pos) {
        chain = chain->next;
        if (chain == NULL)
            return 0;
        bytes += chain->off;
    }
    bytes -= buf->last_read_pos;
    int from = chain->off - bytes;
    for (i = buf->last_read_pos; i <= buf->total_len - seplen; i++) {
        if (check_sep(chain, from, sep, seplen)) {
            buf->last_read_pos = 0;
            return i+seplen;
        }
        ++from;
        --bytes;
        if (bytes == 0) {
            chain = chain->next;
            from = 0;
            if (chain == NULL)
                break;
            bytes = chain->off;
        }
    }
    buf->last_read_pos = i;
    return 0;
}
  • 功能 :在链式缓冲区中搜索特定的分隔符 sep,用于界定数据包的边界(例如,查找换行符 \n)。
  • 步骤
    1. 初始化
      • 从第一个缓冲区块 first 开始。
      • 如果缓冲区为空,返回 0
      • 计算当前缓冲区块中有数据的字节数 bytes
    2. 定位开始搜索的位置
      • 跳过已读的位置 last_read_pos,找到当前搜索的起始缓冲区块和偏移量 from
    3. 遍历缓冲区数据
      • last_read_pos 开始,逐个字节检查是否匹配分隔符 sep
      • 使用 check_sep 函数检查分隔符是否完整匹配。
      • 如果找到匹配,更新 last_read_pos 并返回分隔符结束的位置 i + seplen
    4. 更新搜索位置
      • 如果未找到匹配,更新 last_read_pos 为当前检查的位置 i
    5. 返回
      • 返回找到的分隔符结束的位置,或 0 表示未找到。

4.8 获取写入缓冲区的可写指针

cpp 复制代码
uint8_t * buffer_write_atmost(buffer_t *p) {
    buf_chain_t *chain, *next, *tmp, *last_with_data;
    uint8_t *buffer;
    uint32_t remaining;
    int removed_last_with_data = 0;
    int removed_last_with_datap = 0;

    chain = p->first;
    uint32_t size = p->total_len;

    if (chain->off >= size) {
        return chain->buffer + chain->misalign;
    }

    remaining = size - chain->off;
    for (tmp=chain->next; tmp; tmp=tmp->next) {
        if (tmp->off >= (size_t)remaining)
            break;
        remaining -= tmp->off;
    }
    if (chain->buffer_len - chain->misalign >= (size_t)size) {
        /* already have enough space in the first chain */
        size_t old_off = chain->off;
        buffer = chain->buffer + chain->misalign + chain->off;
        tmp = chain;
        tmp->off = size;
        size -= old_off;
        chain = chain->next;
    } else {
        if ((tmp = buf_chain_new(size)) == NULL) {
            return NULL;
        }
        buffer = tmp->buffer;
        tmp->off = size;
        p->first = tmp;
    }

    last_with_data = *p->last_with_datap;
    for (; chain != NULL && (size_t)size >= chain->off; chain = next) {
        next = chain->next;

        if (chain->buffer) {
            memcpy(buffer, chain->buffer + chain->misalign, chain->off);
            size -= chain->off;
            buffer += chain->off;
        }
        if (chain == last_with_data)
            removed_last_with_data = 1;
        if (&chain->next == p->last_with_datap)
            removed_last_with_datap = 1;

        free(chain);
    }

    if (chain != NULL) {
        memcpy(buffer, chain->buffer + chain->misalign, size);
        chain->misalign += size;
        chain->off -= size;
    } else {
        p->last = tmp;
    }

    tmp->next = chain;

    if (removed_last_with_data) {
        p->last_with_datap = &p->first;
    } else if (removed_last_with_datap) {
        if (p->first->next && p->first->next->off)
            p->last_with_datap = &p->first->next;
        else
            p->last_with_datap = &p->first;
    }
    return tmp->buffer + tmp->misalign;
}
  • 功能:获取当前缓冲区中可写入数据的位置指针,最多可写入的字节数。
  • 步骤
    1. 初始化
      • 获取第一个缓冲区块 first
      • 计算当前总数据量 size
    2. 检查是否有足够的空间
      • 如果第一个缓冲区块的 off 大于或等于总数据量 size,则返回当前写入位置的指针。
    3. 定位剩余空间
      • 计算 remaining 字节,寻找可以连续写入的缓冲区块。
    4. 检查并扩展缓冲区
      • 如果当前缓冲区块有足够的空间,直接返回可写入的位置。
      • 否则,创建一个新的缓冲区块并插入链表。
    5. 复制数据并更新指针
      • 将数据从旧缓冲区块复制到新缓冲区块中,确保数据的连续性。
      • 更新 last_with_datap 指针,确保下一次添加数据时能正确定位。
    6. 返回可写入的位置指针

注意:链式缓冲区通过动态添加缓冲区块,实现了高效的数据写入管理,避免了单一缓冲区块空间不足导致的阻塞。

5. 其他辅助函数

5.1 创建新的缓冲区块

cpp 复制代码
static buf_chain_t * buf_chain_new(uint32_t size) {
    buf_chain_t *chain;
    uint32_t to_alloc;
    if (size > BUFFER_CHAIN_MAX - BUFFER_CHAIN_SIZE)
        return (NULL);
    size += BUFFER_CHAIN_SIZE;

    if (size < BUFFER_CHAIN_MAX / 2) {
        to_alloc = MIN_BUFFER_SIZE;
        while (to_alloc < size) {
            to_alloc <<= 1;
        }
    } else {
        to_alloc = size;
    }
    if ((chain = malloc(to_alloc)) == NULL)
        return (NULL);
    memset(chain, 0, BUFFER_CHAIN_SIZE);
    chain->buffer_len = to_alloc - BUFFER_CHAIN_SIZE;
    chain->buffer = BUFFER_CHAIN_EXTRA(uint8_t, chain);
    return (chain);
}
  • 功能:创建并初始化一个新的缓冲区块。
  • 步骤
    1. 检查缓冲区块大小
      • 如果请求的大小 size 超过最大允许大小 BUFFER_CHAIN_MAX - BUFFER_CHAIN_SIZE,则返回 NULL
    2. 计算实际分配大小
      • 包括缓冲区块结构体的大小。
      • 如果请求大小小于一半的自动扩展最大大小,则将其向上舍入为2的幂次方。
    3. 分配内存并初始化
      • 使用 malloc 分配内存。
      • 使用 memset 清零缓冲区块结构体部分。
      • 设置 buffer_len 为实际数据存储区的大小。
      • 设置 buffer 指针指向实际数据存储区。
    4. 返回缓冲区块指针

5.2 释放所有缓冲区块

cpp 复制代码
static void buf_chain_insert(buffer_t *buf, buf_chain_t *chain) {
    if (*buf->last_with_datap == NULL) {
        buf->first = buf->last = chain;
    } else {
        buf_chain_t **chp;
        chp = free_empty_chains(buf);
        *chp = chain;
        if (chain->off)
            buf->last_with_datap = chp;
        buf->last = chain;
    }
    buf->total_len += chain->off;
}
  • 功能 :将新的缓冲区块 chain 插入到链式缓冲区 buf 中。
  • 步骤
    1. 检查是否存在可用的缓冲区块指针
      • 如果 last_with_datap 指向 NULL,则更新 firstlast 为新的缓冲区块。
      • 否则,调用 free_empty_chains 寻找并释放空闲缓冲区块指针。
    2. 插入缓冲区块
      • 将新的缓冲区块 chain 插入到找到的位置。
      • 如果缓冲区块中有数据 (off > 0),则更新 last_with_datap 指针为当前缓冲区块的位置。
      • 更新 last 为新的缓冲区块。
    3. 更新总数据长度 :将 chain->off 加到 total_len 上。

5.4 插入新的缓冲区块

cpp 复制代码
static inline buf_chain_t * buf_chain_insert_new(buffer_t *buf, uint32_t datlen) {
    buf_chain_t *chain;
    if ((chain = buf_chain_new(datlen)) == NULL)
        return NULL;
    buf_chain_insert(buf, chain);
    return chain;
}
  • 功能:创建并插入一个新的缓冲区块。
  • 步骤
    1. 调用 buf_chain_new 创建一个新的缓冲区块。
    2. 调用 buf_chain_insert 将新缓冲区块插入链表。
    3. 返回新缓冲区块的指针。

5.5 判断是否需要重新对齐缓冲区块

cpp 复制代码
static int buf_chain_should_realign(buf_chain_t *chain, uint32_t datlen) {
    return chain->buffer_len - chain->off >= datlen &&
        (chain->off < chain->buffer_len / 2) &&
        (chain->off <= MAX_TO_REALIGN_IN_EXPAND);
}
  • 功能:判断当前缓冲区块是否需要重新对齐,以便腾出足够的空间添加新数据。
  • 条件
    1. 缓冲区块剩余空间 buffer_len - off 大于等于新数据长度 datlen
    2. 当前数据量 off 小于缓冲区块大小的一半,表示有足够的空间可以重新对齐。
    3. 当前数据量 off 小于等于最大允许重新对齐的字节数 MAX_TO_REALIGN_IN_EXPAND

5.6 重新对齐缓冲区块

cpp 复制代码
static void buf_chain_align(buf_chain_t *chain) {
    memmove(chain->buffer, chain->buffer + chain->misalign, chain->off);
    chain->misalign = 0;
}
  • 功能 :将缓冲区块中的数据重新对齐到缓冲区起始位置,释放 misalign 部分的空间。
  • 步骤
    1. 使用 memmove 将数据从 buffer + misalign 复制到 buffer,实现数据的左移。
    2. misalign 更新为 0,表示数据已对齐到缓冲区起始位置。

6. 数据读写操作的详细实现

6.1 从缓冲区复制数据到用户空间

cpp 复制代码
static uint32_t buf_copyout(buffer_t *buf, void *data_out, uint32_t datlen) {
    buf_chain_t *chain;
    char *data = data_out;
    uint32_t nread;
    chain = buf->first;
    if (datlen > buf->total_len)
        datlen = buf->total_len;
    if (datlen == 0)
        return 0;
    nread = datlen;

    while (datlen && datlen >= chain->off) {
        uint32_t copylen = chain->off;
        memcpy(data,
            chain->buffer + chain->misalign,
            copylen);
        data += copylen;
        datlen -= copylen;

        chain = chain->next;
    }
    if (datlen) {
        memcpy(data, chain->buffer + chain->misalign, datlen);
    }

    return nread;
}
  • 功能 :将缓冲区中的数据复制到用户提供的输出缓冲区 data_out,最多复制 datlen 字节。
  • 步骤
    1. 初始化
      • 获取第一个缓冲区块 first
      • 如果请求的读取长度 datlen 大于总数据量 total_len,则调整 datlentotal_len
      • 如果 datlen0,返回 0
      • 设置 nread 为实际要读取的字节数。
    2. 遍历缓冲区块
      • 对于每个缓冲区块,复制其数据到输出缓冲区。
      • 如果当前缓冲区块的 off 大于或等于剩余的 datlen,则复制部分数据。
      • 更新 data 指针和剩余的 datlen
    3. 返回 :返回实际复制的字节数 nread

6.2 清空缓冲区的一部分数据

cpp 复制代码
int buffer_drain(buffer_t *buf, uint32_t len) {
    buf_chain_t *chain, *next;
    uint32_t remaining, old_len;
    old_len = buf->total_len;
    if (old_len == 0)
        return 0;

    if (len >= old_len) {
        len = old_len;
        for (chain = buf->first; chain != NULL; chain = next) {
            next = chain->next;
            free(chain);
        }
        ZERO_CHAIN(buf);
    } else {
        buf->total_len -= len;
        remaining = len;
        for (chain = buf->first; remaining >= chain->off; chain = next) {
            next = chain->next;
            remaining -= chain->off;

            if (chain == *buf->last_with_datap) {
                buf->last_with_datap = &buf->first;
            }
            if (&chain->next == buf->last_with_datap)
                buf->last_with_datap = &buf->first;

            free(chain);
        }

        buf->first = chain;
        chain->misalign += remaining;
        chain->off -= remaining;
    }
    
    // buf->n_del_for_cb += len;
    return len;
}
  • 功能 :从链式缓冲区中清除 len 字节的数据,而不读取到用户空间。
  • 步骤
    1. 检查缓冲区是否为空
      • 如果缓冲区为空,返回 0
    2. 清除操作
      • 情况1 :如果要清除的数据 len 大于或等于缓冲区中的总数据量 old_len,则清空整个缓冲区,释放所有缓冲区块,并调用 ZERO_CHAIN 重置缓冲区结构体。
      • 情况2 :如果要清除的数据 len 小于总数据量,则逐个缓冲区块地清除数据,直到清除完 len 字节。
        • 更新 total_len
        • 移除完全清除的缓冲区块。
        • 对部分清除的缓冲区块,更新 misalignoff
    3. 返回 :返回实际清除的数据长度 len

6.3 从缓冲区移除并读取数据

cpp 复制代码
int buffer_remove(buffer_t *buf, void *data_out, uint32_t datlen) {
    uint32_t n = buf_copyout(buf, data_out, datlen);
    if (n > 0) {
        if (buffer_drain(buf, n) < 0)
            n = -1;
    }
    return (int)n;
}
  • 功能:从链式缓冲区中读取并移除数据。
  • 步骤
    1. 调用 buf_copyout 从缓冲区中读取数据到 data_out,读取长度为 datlen
    2. 如果成功读取 (n > 0),则调用 buffer_drain 移除已读取的数据。
    3. 返回实际读取的数据长度 n,如果移除失败,返回 -1

6.4 检查分隔符是否匹配

cpp 复制代码
static bool check_sep(buf_chain_t * chain, int from, const char *sep, int seplen) {
    for (;;) {
        int sz = chain->off - from;
        if (sz >= seplen) {
            return memcmp(chain->buffer + chain->misalign + from, sep, seplen) == 0;
        }
        if (sz > 0) {
            if (memcmp(chain->buffer + chain->misalign + from, sep, sz)) {
                return false;
            }
        }
        chain = chain->next;
        sep += sz;
        seplen -= sz;
        from = 0;
    }
}
  • 功能 :在链式缓冲区中检查分隔符 sep 是否完整匹配。
  • 步骤
    1. 循环检查
      • 计算当前缓冲区块中从 from 位置开始的数据长度 sz
      • 如果 sz 大于或等于分隔符长度 seplen,则进行内存比较,检查是否匹配。
      • 如果 sz 小于分隔符长度,但有部分数据匹配,则继续检查下一个缓冲区块,确保分隔符跨越缓冲区块时也能正确匹配。
    2. 返回
      • 如果匹配成功,返回 true
      • 如果匹配失败,返回 false

7. 总结

链式缓冲区通过链接多个缓冲区块,实现了动态的、灵活的数据管理。与环形缓冲区相比,链式缓冲区具有以下优势:

  • 更高的灵活性:能够动态添加或移除缓冲区块,适应不同的数据量需求。
  • 更好的内存利用:根据实际数据量动态分配缓冲区块,减少内存浪费。
  • 减少数据移动:通过分散存储数据,避免了大量的数据拷贝和移动操作。

然而,链式缓冲区也存在一些挑战:

  • 复杂性增加:需要维护多个缓冲区块的链接,增加了代码的复杂性。
  • 系统调用开销:在缓冲区块不足时,需要频繁地进行内存分配和释放,可能增加系统调用的开销。
  • 碎片化问题:长期运行可能导致内存碎片化,影响性能。

通过结合Reactor 模式,链式缓冲区能够高效地管理和传输网络数据,特别是在高并发和多连接的场景中。它确保了数据的完整性和可靠性,即使在生产者和消费者速度不匹配的情况下,也能有效地管理数据流动,避免数据丢失和阻塞。

8. 结合之前的内容

用户态缓存:环形缓冲区(Ring Buffer)-CSDN博客文章浏览阅读168次,点赞13次,收藏13次。环形缓冲区是一种高效的数据结构,广泛应用于生产者-消费者模型中。在网络通信中,尤其是用户态缓存区中,环形缓冲区通过循环使用固定大小的内存区域,减少数据移动和内存管理开销,提升数据传输效率。#endif作用:为定义一个别名buffer_t,简化后续代码的书写。高效的数据管理:通过固定大小的缓冲区和双指针机制,环形缓冲区实现了高效的数据读写操作。减少数据移动:利用环形地址计算和分段拷贝,避免了大量的数据拷贝和移动操作,提升了性能。灵活的空间管理:通过动态调整和优化(如。https://blog.csdn.net/weixin_43925427/article/details/142358862?fromshare=blogdetail&sharetype=blogdetail&sharerId=142358862&sharerefer=PC&sharesource=weixin_43925427&sharefrom=from_link在之前的讲解中,我们深入解析了 环形缓冲区 的实现及其在网络通信中的应用。链式缓冲区作为另一种常用的数据结构,提供了不同的优势和适用场景。让我们将链式缓冲区与环形缓冲区进行对比,进一步理解它们在用户态缓存区设计中的应用。

8.1 环形缓冲区 vs 链式缓冲区

特性 环形缓冲区(Ring Buffer) 链式缓冲区(Chain Buffer)
内存管理 固定大小,通常为2的幂次方 动态添加缓冲区块,灵活调整大小
数据移动 通过环形地址计算,避免大规模数据拷贝 分散存储数据,减少数据移动
内存利用率 可能存在内存浪费,尤其在数据量波动大时 高效内存利用,根据需求动态分配缓冲区块
复杂性 相对简单,实现容易 较复杂,需要维护链表结构和缓冲区块链接
适用场景 适用于数据量固定且高效的数据流管理 适用于数据量不固定,需处理不同大小数据包

8.2 在 Reactor 模式中的应用

Reactor 模式 中,无论是环形缓冲区还是链式缓冲区,都扮演着重要的数据管理角色。它们确保了从网络读取的数据能够高效、可靠地传输到用户空间,并且在需要发送数据时能够及时、完整地写入网络。

  • 环形缓冲区 适用于数据量相对固定、读写速度相匹配的场景,通过减少数据拷贝提升性能。
  • 链式缓冲区 适用于数据量不固定、需要动态扩展的场景,通过灵活的缓冲区块管理提升内存利用率和适应性。

9. 具体应用解析

让我们将链式缓冲区的实现与之前的 服务器代码 结合起来,理解其在实际工作中的具体应用。

9.1 服务器主程序中的链式缓冲区

服务器代码 中,链式缓冲区被用于管理每个客户端连接的接收缓冲区 in 和发送缓冲区 out。下面是关键部分的解析:

9.1.1 接受连接回调函数

cpp 复制代码
void accept_cb(int fd, int events, void *privdata) {
    event_t *e = (event_t*) privdata;

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));
    socklen_t len = sizeof(addr);

    int clientfd = accept(fd, (struct sockaddr*)&addr, &len);
    if (clientfd <= 0) {
        printf("accept failed\n");
        return;
    }

    char str[INET_ADDRSTRLEN] = {0};
    printf("recv from %s at port %d\n", inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str)),
        ntohs(addr.sin_port));

    event_t *ne = new_event(event_base(e), clientfd, read_cb, 0, 0);
    add_event(event_base(e), EPOLLIN, ne);
    set_nonblock(clientfd);
}
  • 功能
    1. 接受新连接 :调用 accept 函数接受新的客户端连接,获取 clientfd
    2. 打印客户端信息 :使用 inet_ntop 将客户端 IP 地址转换为字符串,并打印其端口号。
    3. 创建新事件对象 :调用 new_event 为新连接创建一个事件对象,关联 read_cb 作为读事件的回调函数。
    4. 注册事件 :将新事件对象添加到 reactor 中,监听 EPOLLIN 事件(有数据可读)。
    5. 设置非阻塞模式:将客户端套接字设置为非阻塞模式,确保事件循环不会被单个连接阻塞。

9.1.2 读取数据回调函数

cpp 复制代码
void read_cb(int fd, int events, void *privdata) {
    event_t *e = (event_t *)privdata;
    int n = event_buffer_read(e); // 将网络中读缓冲区的数据拷贝到用户态缓冲区
    if (n > 0) {
        // buffer_search 检测是否是一个完整的数据包
        int len = buffer_search(evbuf_in(e), "\n", 1);
        if (len > 0 && len < 1024) {
            char buf[1024] = {0};
            buffer_remove(evbuf_in(e), buf, len);
            event_buffer_write(e, buf, len);
        }
    }
}
  • 功能
    1. 读取数据 :调用 event_buffer_read 从网络读取数据,并将其添加到接收缓冲区 in
    2. 搜索分隔符 :使用 buffer_search 在接收缓冲区中查找换行符 \n,判断是否收到完整的数据包。
    3. 处理完整数据包
      • 如果找到完整的数据包且长度合理(len < 1024),则:
        • 从接收缓冲区中移除该数据包,存储到本地缓冲区 buf
        • 将数据包写入发送缓冲区 out,准备发送回客户端。

9.1.3 读取数据到用户态缓冲区

cpp 复制代码
int event_buffer_read(event_t *e) {
    int fd = e->fd;
    int num = 0;
    while (1) {
        char buf[1024] = {0};
        int n = read(fd, buf, 1024);
        if (n == 0) {
            printf("close connection fd = %d\n", fd);
            if (e->error_fn)
                e->error_fn(fd, "close socket");
            del_event(e->r, e);
            close(fd);
            return 0;
        } else if (n < 0) {
            if (errno == EINTR)
                continue;
            if (errno == EWOULDBLOCK)
                break;
            printf("read error fd = %d err = %s\n", fd, strerror(errno));
            if (e->error_fn)
                e->error_fn(fd, strerror(errno));
            del_event(e->r, e);
            close(fd);
            return 0;
        } else {
            printf("recv data from client:%s", buf);
            buffer_add(evbuf_in(e), buf, n);
        }
        num += n;
    }
    return num;
}
  • 功能
    1. 持续读取数据 :使用 read 系统调用从套接字读取数据,直到没有更多数据可读。
    2. 处理读取结果
      • n == 0:表示客户端关闭连接,打印信息,触发错误回调,删除事件并关闭套接字。
      • n < 0
        • EINTR:被信号中断,继续读取。
        • EWOULDBLOCK:非阻塞模式下没有更多数据可读,退出循环。
        • 其他错误,打印错误信息,触发错误回调,删除事件并关闭套接字。
      • n > 0 :成功读取数据,将数据添加到接收缓冲区 in
    3. 返回 :返回读取的数据总量 num

9.1.4 写入数据到套接字

cpp 复制代码
int event_buffer_write(event_t *e, void * buf, int sz) {
    buffer_t *out = evbuf_out(e);
    if (buffer_len(out) == 0) {
        int n = _write_socket(e, buf, sz);
        if (n == 0 || n < sz) {
            // 发送失败,除了将没有发送出去的数据写入缓冲区,还要注册写事件
            buffer_add(out, (char *)buf+n, sz-n);
            enable_event(e->r, e, 1, 1);
            return 0;
        } else if (n < 0) 
            return 0;
        return 1;
    }
    buffer_add(out, (char *)buf, sz);
    return 1;
}
  • 功能
    1. 获取发送缓冲区 out
    2. 尝试直接写入套接字
      • 如果发送缓冲区为空,尝试调用 _write_socket 将数据直接写入套接字。
      • 发送成功且全部发送 :返回 1
      • 发送部分失败n < sz):将未发送的数据添加到发送缓冲区 out,并注册写事件 EPOLLOUT,等待后续发送。
    3. 发送缓冲区不为空 :将数据添加到发送缓冲区 out,等待后续发送。
    4. 返回:根据发送结果返回相应的值。

9.1.5 实际写入套接字

cpp 复制代码
static int _write_socket(event_t *e, void * buf, int sz) {
    int fd = e->fd;
    while (1) {
        int n = write(fd, buf, sz);
        if (n < 0) {
            if (errno == EINTR)
                continue;
            if (errno == EWOULDBLOCK)
                break;
            if (e->error_fn)
                e->error_fn(fd, strerror(errno));
            del_event(e->r, e);
            close(e->fd);
        }
        return n;
    }
    return 0;
}
  • 功能:尝试将数据写入套接字。
  • 步骤
    1. 尝试写入 :调用 write 系统调用将数据写入套接字。
    2. 处理写入结果
      • n < 0
        • EINTR:被信号中断,继续写入。
        • EWOULDBLOCK :非阻塞模式下无法立即写入,退出循环,返回 0
        • 其他错误,打印错误信息,触发错误回调,删除事件并关闭套接字。
      • n >= 0 :返回实际写入的字节数 n
    3. 返回 :返回写入的字节数 n,如果无法写入则返回 0

10. 综合应用

10.1 在用户态缓存区中的应用

在链式缓冲区中,buffer_t 结构体管理着多个缓冲区块,每个缓冲区块存储一定量的数据。当有新的数据到达时,通过 buffer_add 将数据添加到适当的缓冲区块中;当需要读取数据时,通过 buffer_remove 从链表中按顺序读取数据。这种设计能够灵活地应对不同大小的数据包和动态的数据量需求。

10.2 处理生产者与消费者速度不匹配

在网络通信中,生产者(如内核协议栈)生成数据的速度可能快于消费者(如应用程序)的处理速度,或反之。链式缓冲区通过以下方式有效地处理这种不匹配:

  • 生产者速度快于消费者
    • 链式缓冲区通过动态添加缓冲区块,暂存大量数据,避免数据丢失。
    • 确保缓冲区块的灵活扩展,适应高峰数据量。
  • 消费者速度快于生产者
    • 链式缓冲区可以高效地移除已处理的数据,腾出空间给新的数据。
    • 通过释放已清除的缓冲区块,避免内存浪费。

10.3 搜索分隔符和数据包处理

链式缓冲区中的 buffer_search 函数通过查找特定的分隔符(如换行符 \n),实现数据包的界定和拆分。这对于基于协议的通信(如 HTTP、SMTP 等)尤为重要,确保应用程序能够正确解析和处理每个完整的数据包。

11. 总结

通过详细解析这段链式缓冲区的代码,我们深入理解了链式缓冲区的结构和工作原理:

  • 高效的数据管理:通过链接多个缓冲区块,链式缓冲区实现了高效的数据读写操作,适应不同的数据量需求。
  • 减少数据移动:通过分散存储数据,链式缓冲区避免了大规模的数据拷贝和移动操作,提升了性能。
  • 灵活的空间管理:通过动态添加和释放缓冲区块,链式缓冲区能够灵活地适应不同的数据量需求,保持高效运行。
  • 可靠的数据传输:在生产者和消费者速度不匹配的情况下,链式缓冲区通过暂存和管理数据,确保数据的完整性和可靠性。

结合之前对 环形缓冲区 的解析,我们可以看到链式缓冲区在处理动态和不规则数据流方面具有更大的优势。然而,链式缓冲区也带来了更高的实现复杂性和潜在的系统调用开销,需要在具体应用中权衡选择。

理解和掌握链式缓冲区的实现和应用,对于优化网络应用程序的性能,提升系统的响应速度和稳定性具有重要意义。结合 Reactor 模式,链式缓冲区能够高效地管理和传输网络数据,特别是在高并发和多连接的场景中,确保数据传输的流畅性和可靠性。

参考:

0voice · GitHub

GitHub - TryTryTL/buffer_design

用户态缓存:高效数据交互与性能优化-CSDN博客

用户态缓存:环形缓冲区(Ring Buffer)-CSDN博客

相关推荐
岁月变迁呀9 小时前
Redis梳理
数据库·redis·缓存
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭10 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
Code apprenticeship11 小时前
怎么利用Redis实现延时队列?
数据库·redis·缓存
fpcc13 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
Ewen Seong13 小时前
mysql系列5—Innodb的缓存
数据库·mysql·缓存
安全二次方security²13 小时前
SMMU软件指南SMMU编程之虚拟机结构和缓存
缓存·cache·smmu·arm安全架构·系统mmu·虚拟机结构·vms
.生产的驴16 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
kong790692821 小时前
电商项目-网站首页高可用(二)
缓存·二级缓存·网站首页高可用
一个懒鬼1 天前
Windows脚本清理C盘缓存
windows·缓存
科马1 天前
【Redis】缓存
数据库·redis·spring·缓存