curl 内部 bufq 缓冲区队列模块详解:结构、API 与内存管理
2026/9/10 20:44:45 网站建设 项目流程

curl 内部 bufq 缓冲区队列模块详解:结构、API 与内存管理

【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl

导读

bufq(buffer queue)是 curl 库内部用于管理 I/O 缓冲区的核心模块,它对外提供可写可读、带读写位置游标、且有容量上限的字节队列抽象。本文以 docs/internals/BUFQ.md 为骨架,结合 lib/bufq.h 与 lib/bufq.c 的源码实现,系统讲解 bufq 的读写 API、零拷贝回调通道(slurp/pass)、peek/skip 语义、基于 chunk 的内存管理、full/empty 判定逻辑、软上限选项以及可跨 bufq 共享的 chunk 池(bufc_pool),最后给出其在 curl 各连接过滤器(cfilter)、HTTP/2、WebSocket 等模块中的真实使用场景。读完本文,你将掌握 bufq 的设计意图与全部公开接口,并理解 curl 底层收发路径为何需要这样一套缓冲区抽象。

bufq 是什么:定位与基本能力

bufq是一个内部模块,专门用于管理 I/O 缓冲区。一个bufq可以被写入、也可以被读出;它内部维护读位置与写位置,并有一个最大容量上限。它与 curl 中另一个缓冲区模块dynbuf定位不同:dynbuf面向动态增长的字符串拼接,而bufq面向"有界、可流式消费"的字节队列,是 curl 连接过滤器(cfilter)体系下网络读写缓冲的基础设施。

从数据结构看,lib/bufq.h 中struct bufq只保存了队头head(读)、队尾tail(写)、空闲块链表spare、可选池pool、当前块数chunk_count、上限max_chunks、块大小chunk_size与选项opts。所有数据实体都放在struct buf_chunk(见下文"chunk 与内存管理"一节)中,bufq 本身并不持有大块内存,因此它很轻量。

核心读写 API:write 与 read

bufq的基础读写函数,其签名与返回码处理和 curl 内部大量 read/write 函数保持一致:都以CURLcode为返回类型,并通过一个size_t *出参报告实际传输字节数。

Curl_bufq_write

CURLcode Curl_bufq_write(struct bufq *q, const uint8_t *buf, size_t len, size_t *pnwritten);

语义要点(与文档一致):

  • 成功时把buf中的字节拷贝q,并在pnwritten中写入实际写入的长度;
  • 出错时pnwritten被置为 -1;
  • q已满时,pnwritten置为 -1 并返回CURLE_AGAIN

源码实现位于 lib/bufq.c:它在while(len)循环中反复取"未满的尾块"(get_non_full_tail),用chunk_append向尾块拷贝数据;若拿不到尾块,则分两种情形——chunk_count < max_chunks或开启了BUFQ_OPT_SOFT_LIMIT时说明内存分配失败,返回CURLE_OUT_OF_MEMORY;否则说明真的满了,跳出循环。函数末尾return (!*pnwritten && len) ? CURLE_AGAIN : CURLE_OK;精确实现了"一个字都没写进去且还有数据要写才报 AGAIN"的语义——部分写入是允许的,这为上层做非阻塞 I/O 提供了便利。

Curl_bufq_cwrite(lib/bufq.h)是面向char *数据的便捷包装,内部直接转调Curl_bufq_write

Curl_bufq_read

CURLcode Curl_bufq_read(struct bufq *q, uint8_t *buf, size_t len, size_t *pnread);

语义要点:

  • 成功时把q中的数据拷贝buf,在pnread中写入实际读出的长度;
  • 出错时pnread置为 -1;
  • q为空时,pnread置为 -1 并返回CURLE_AGAIN

实现见 lib/bufq.c:循环从队头块chunk_read拷贝,每读完一块就用prune_head清理空块,最后同样以"没读出任何字节才返回 AGAIN"收尾。配套的Curl_bufq_cread(lib/bufq.h)提供char *版本。

单元测试的印证

tests/unit/unit2601.c 是 bufq 的专门单元测试(由 tests/data/test2601 驱动)。其中用循环写满、循环读空的用例验证了写满后CURLE_AGAIN、读空后CURLE_AGAIN以及读出的字节总数与写入总数严格相等(nread == nwritten);read empty fail用例直接断言"对空 bufq 调用 read 返回 CURLE_AGAIN",与文档语义一一对应。

零拷贝通道:slurp 与 pass 回调

write/read都存在一次拷贝。为避免不必要的内存复制,bufq提供了两个基于回调的通道:读侧slurp、写侧pass

读侧:Curl_bufq_slurp

typedef CURLcode Curl_bufq_reader(void *reader_ctx, uint8_t *buf, size_t len, size_t *pnread); CURLcode Curl_bufq_slurp(struct bufq *q, Curl_bufq_reader *reader, void *reader_ctx, size_t *pnread);

Curl_bufq_slurp()会调用传入的reader回调,并把bufq 自己的内部缓冲区内存交给它直接写入——数据无需先落到临时 buffer 再拷贝进 bufq。它可能多次调用reader,条件是 bufq 还有空间、且reader每次都返回了所请求的完整长度。此外还有变体:

  • Curl_bufq_sipn(q, max_len, reader, ctx, pnread)(lib/bufq.h):至多调用reader一次,且最多读入max_len字节;max_len为 0 表示除块空间外不设上限。实现见 lib/bufq.c:先取非满尾块,若拿不到且块数未达上限则报CURLE_OUT_OF_MEMORY,否则视为"已满、阻塞"返回CURLE_AGAIN
  • 内部函数bufq_slurpn(lib/bufq.c)实现了"读到阻塞或队列满为止"的循环逻辑,并遵循一条重要原则:当某次返回的字节数少于请求量时立即停止if(q->tail && !chunk_is_full(q->tail)) break;),避免对慢速 reader 空转。

写侧:Curl_bufq_pass

typedef CURLcode Curl_bufq_writer(void *writer_ctx, const uint8_t *buf, size_t len, size_t *pwritten); CURLcode Curl_bufq_pass(struct bufq *q, Curl_bufq_writer *writer, void *writer_ctx, size_t *pwritten);

Curl_bufq_pass()把 bufq 内部内存直接交给writer,并删除writer报告的已消费字节数,同样免去中间拷贝。实现见 lib/bufq.c:循环Curl_bufq_peek拿到队头内存,调用writer,成功后Curl_bufq_skip跳过对应字节;若writer中途返回CURLE_AGAIN,只要此前已有字节成功写出,就把整体结果归为CURLE_OK(已部分消费),否则原样返回阻塞状态。注意文档与头文件均提示:出错时可能已有部分块被写出,队列长度与调用前不同,调用方需以pwritten为准。

融合通道:Curl_bufq_write_pass

Curl_bufq_write_pass(q, buf, len, writer, ctx, pwritten)(lib/bufq.h)是写侧的"组合拳":当bufq满了,先尝试用writer直接排空一部分(Curl_bufq_pass),腾出空间后再Curl_bufq_write写入新数据;writer阻塞且队列仍满则放弃。这意味着数据可能被直接透传给 writer,也可能先入队再统一写出,取决于len、当前缓冲量与块大小的组合——这正是 lib/cfilters.c 中Curl_cf_send_bufq的用法:有缓冲区就write_pass合并写入,无缓冲数据则直接Curl_bufq_pass

不消费数据的访问:peek 与 skip

有些场景(如发送端要等数据凑齐、或接收端要预看数据)不希望 read 消费队列,此时用 peek:

bool Curl_bufq_peek(struct bufq *q, const uint8_t **pbuf, size_t *plen);
  • 返回 TRUE 时,pbuf指向内部内存中plen字节的未读数据;
  • 该指针只在下次对 bufq 执行任何操作前有效(因为任何写/读/跳过都可能触发块的重排或释放);
  • 队列为空时返回 FALSE,并将pbuf置为 NULL、plen置为 0。

源码见 lib/bufq.c:先prune_head清理空头块,再对非空头块chunk_peek返回其内部指针与长度。另有Curl_bufq_peek_at(q, offset, ...)(lib/bufq.h)支持按偏移量跨块窥视。

与 peek 配对的是 skip——不读数据,直接丢弃:

void Curl_bufq_skip(struct bufq *q, size_t amount);

它从队头移除amount字节(lib/bufq.c):循环chunk_skipprune_head,跳过量超过缓冲总量则队列变空。Curl_bufq_pass正是依赖"peek 拿指针 + skip 消费"这两个原语实现零拷贝外发的。

生命周期:init / free / reset

bufq的初始化与释放风格与 curl 的dynbuf模块类似。使用方把struct bufq内嵌在自己的结构体中,使用前先初始化:

void Curl_bufq_init(struct bufq *q, size_t chunk_size, size_t max_chunks);

bufq被告知最多容纳多少个"chunk",以及每个 chunk 多大。变体Curl_bufq_init2(q, chunk_size, max_chunks, opts)(lib/bufq.h)额外接受选项;Curl_bufq_initp(q, pool, max_chunks, opts)(lib/bufq.h)则配合 chunk 池使用(见"pools"一节),此时 chunk 大小由池统一管理,bufq 无需再关心。

使用方有责任在不再需要时调用:

void Curl_bufq_free(struct bufq *q);

释放q持有的全部资源(lib/bufq.c 释放 head 与 spare 两条链表)。若想清空数据但保留已分配块以备复用,则用:

void Curl_bufq_reset(struct bufq *q);

实现(lib/bufq.c)把 head 链表整体摘下来挂到 spare 链表,tail置空,块内存不释放——这是高频复用场景下的重要优化。

内存管理:chunk 链表、spare 与选项

chunk 的结构

每个 chunk 是一个固定大小的内存块,定义在 lib/bufq.h:

struct buf_chunk { struct buf_chunk *next; /* 链表指针 */ size_t dlen; /* x.data[] 实际分配的长度 */ size_t r_offset; /* 第一个未读字节 */ size_t w_offset; /* 最后一个已写字节之后 */ union { uint8_t data[1]; /* 可容纳 dlen 字节的缓冲区(柔性数组风格) */ void *dummy; /* 对齐 */ } x; };

dlen为块容量,r_offset/w_offset分别标记未读区间的起止(lib/bufq.c 中chunk_len = w_offset - r_offset),因此读走的数据不必立即腾挪,块可以在部分消费状态下继续挂链。

分配与回收策略

  • bufq内部按固定大小(chunk_size)分配块,数量上限为max_chunks
  • 按需分配:写入时才取块,因此向 bufq 写入可能返回CURLE_OUT_OF_MEMORY
  • 一旦使用的块数达到上限,bufq 就报告"full"。

队列的维护规则(文档 + lib/bufq.cget_non_full_tail)是:读永远发生在头块,写永远进入尾块;头块读空即被移除,尾块写满则在链表尾追加新块成为新尾。

被读空的块默认进入spare空闲链表(prune_head,lib/bufq.c),下次需要新块时直接从 spare 取(get_spare,lib/bufq.c),避免反复 malloc/free。如果以选项BUFQ_OPT_NO_SPARES创建,空块会被立即释放;get_spare中还做了chunk_size > SIZE_MAX - sizeof(*chunk)的整数溢出防护,分配采用curlx_calloc(1, sizeof(*chunk) + chunk_size)一次性完成"头结构 + 数据区"。

选项速查

选项含义
BUFQ_OPT_NONE0默认行为,max_chunks为硬上限
BUFQ_OPT_SOFT_LIMIT1 << 0max_chunks变为软上限,见下文"soft limit"
BUFQ_OPT_NO_SPARES1 << 1不保留空闲块,读空即释放

empty、full 与"溢出":full 的真实语义

可以随时询问 bufq 的状态:Curl_bufq_is_empty(q)Curl_bufq_is_full(q)等。bufq 当前持有的数据量等于所有 chunk 中未读字节之和,由Curl_bufq_len(q)返回(lib/bufq.c 遍历 head 链表累加)。

关键点:len 与 "full" 只有松散关联。文档给出的示例非常直观:

  • 创建chunk_size=1000max_chunks=4的 bufq;
  • 写入 4000 字节,它报告 "full";
  • 读取 1 字节后,它仍然报告 "full";
  • 再读 999 字节后,才不再 "full"。

原因在于 full 的准确定义是:bufq 已用满 max_chunks 个块,且最后一个块无法再写入。看 lib/bufq.c 的Curl_bufq_is_full实现:若无 spare、chunk_count >= max_chunks且尾块已满,才返回 TRUE。上例中头块虽只读了 1 字节,但剩余 999 字节仍占着那个块,头块无法移除、新尾块无法添加,队列自然仍算 full;只有把该块读空、prune_head将其摘除,才有名额追加新块。

BUFQ_OPT_SOFT_LIMIT:软上限

如果以BUFQ_OPT_SOFT_LIMIT初始化,bufq 允许写入超过max_chunks的字节数:它照常报告 "full",但仍然可以继续写。这从get_spare(lib/bufq.c)的条件if(q->chunk_count >= q->max_chunks && (!(q->opts & BUFQ_OPT_SOFT_LIMIT)))可以看出——软上限下不因块数达标而拒绝取块;prune_head(lib/bufq.c)也会在块数超过 max 时直接释放超出的空块。

该选项用于必须避免部分写入的场景:例如一条完整的 HTTP chunked 帧、一条 SMTP 响应、一个 WebSocket 消息,宁可让队列暂时超限,也不能把帧头帧尾拆散。代价是调用方必须用其他手段(如Curl_bufq_len阈值检查)防止队列无限膨胀。文档明确提醒:"It means that you need other checks to keep the bufq from growing ever larger and larger."

单元测试 tests/unit/unit2601.c 专门验证了 SOFT_LIMIT 行为:写满后再做一次写入仍应完整成功!result && n2 == wsize),且全部数据可原样读出(nread == nwritten)。

pools:bufc_pool 跨队列共享 chunk

struct bufc_pool用于为 bufq 统一创建 chunk 并保留空闲块,初始化与使用方式:

void Curl_bufcp_init(struct bufc_pool *pool, size_t chunk_size, size_t spare_max); void Curl_bufq_initp(struct bufq *q, struct bufc_pool *pool, size_t max_chunks, int opts);
  • 池在初始化时确定 chunk 大小与最多保留的空闲块数spare_max
  • bufq 拿到池与max_chunks不再关心 chunk 大小,块的大小由池管理(lib/bufq.c 中Curl_bufq_initp直接取pool->chunk_size);
  • 需要块时 bufq 向池取(bufcp_take,从 spare 取或新分配);块用完后归还池(bufcp_put,spare 达到spare_max才真正 free),见 lib/bufq.c。

struct bufc_pool本身只维护一个 spare 链表、chunk_size、当前 spare 数与上限(lib/bufq.h)。池不是线程安全的,可以被多个 bufq 共享,前提是所有使用它的 bufq 在同一线程内运行——在 curl 中,凡是使用同一个 multi handle 的传输都满足这一前提。

池的两个核心收益(文档原文要点):

  • 当所有 bufq 都为空时,池中只占用spare_max个块的内存;空的 bufq 本身不持有任何内存;
  • 最近归还的 spare 块最先被再次分发(spare 链表采用头插法),无论哪个 bufq 需要它,都能让"最近使用过"的内存保持较小的足迹,提升缓存局部性。

Curl_bufcp_free(pool)(lib/bufq.c)负责释放池中全部 spare 块。

bufq 在 curl 中的真实应用场景

bufq 是 curl 连接过滤器(cfilter)收发路径与多个协议实现的基础设施,仓库中有大量实例可循:

  • cfilter 层:lib/cfilters.c 把Curl_conn_cf_recv包装成cf_bufq_reader、把Curl_conn_cf_send包装成cf_bufq_writer,对外暴露Curl_cf_recv_bufq(内部Curl_bufq_sipn)与Curl_cf_send_bufq(内部Curl_bufq_write_pass/Curl_bufq_pass),实现"网络收发与协议缓冲解耦"的管道化设计;
  • HTTP/2:lib/http2.c 为单个连接创建stream_bufcp池,并用Curl_bufq_initp让接收/发送两个 bufq 与各 stream 的发送 bufq(lib/http2.c)共享同一池,正是文档"池可在同线程多 bufq 间共享"的实践;
  • HTTP chunked 解码:lib/http_chunks.c 以CURL_CHUNKED_MAXLEN为块大小、BUFQ_OPT_SOFT_LIMIT创建 chunkbuf——解码需要一次性拿到完整 chunk 数据,软上限保证不产生撕裂;
  • WebSocket:lib/ws.c 用WS_CHUNK_SIZEWS_CHUNK_COUNT分别初始化接收、发送 bufq(同样配合 SOFT_LIMIT 保证消息完整性),lib/ws.c 还有 16KB 级缓冲的暂存 bufq;
  • SMTP 与 sendf:lib/smtp.c 与 lib/sendf.c 均以16 * 1024为 chunk 大小、max_chunks=1BUFQ_OPT_SOFT_LIMIT创建暂存缓冲;
  • 暂停/续传:lib/cw-pause.c 用 bufq 实现写暂停时的数据暂存,chunk 数固定为 1;
  • QUIC/HTTP3:lib/vquic/cf-quiche.c、lib/vquic/vquic.c、lib/vquic/cf-ngtcp2-cmn.c 等在各自的连接与 stream 上以 bufq 管理收发缓冲;
  • SOCKS/代理隧道:lib/socks.c、lib/cf-h2-proxy.c 为握手与隧道数据提供缓冲;
  • TLS 早期数据:lib/vtls/vtls.c 用BUFQ_OPT_NO_SPARES暂存CURL_SSL_EARLY_MAX字节的 earlydata——单次使用、无需复用,正好不保留 spare。

从这些用法可以归纳出设计模式:固定字节流协议(如 chunked、WebSocket 帧)倾向 SOFT_LIMIT 保完整性,一次性暂存倾向 NO_SPARES 省内存,长生命周期多队列场景用 bufc_pool 共享 chunk

总结

bufq是 curl 内部一套设计精巧的有界字节队列:以固定大小 chunk 链表承载数据,读头写尾、按需分配、空块入 spare 复用;write/read提供带部分传输语义的基础拷贝读写,slurp/pass/write_pass借助 reader/writer 回调实现零拷贝管道化,peek/skip支持无消费的数据查看与丢弃;BUFQ_OPT_SOFT_LIMITBUFQ_OPT_NO_SPARES分别解决"避免部分写入"与"避免闲置内存"两类问题;bufc_pool则让同一线程内的多个 bufq 共享 chunk,显著降低内存占用与分配开销。理解 bufq,是读懂 curl 从 socket 到协议解析整条数据通路的一把钥匙。若需深入验证其行为,可直接运行仓库中的单元测试 tests/unit/unit2601.c(测试用例定义于 tests/data/test2601),覆盖读写往返、满/空边界、SOFT_LIMIT 越界写入等关键路径。

【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询