
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导读bufqbuffer 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 本身并不持有大块内存因此它很轻量。核心读写 APIwrite 与 readbufq的基础读写函数其签名与返回码处理和 curl 内部大量 read/write 函数保持一致都以CURLcode为返回类型并通过一个size_t *出参报告实际传输字节数。Curl_bufq_writeCURLcode 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_cwritelib/bufq.h是面向char *数据的便捷包装内部直接转调Curl_bufq_write。Curl_bufq_readCURLcode 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_creadlib/bufq.h提供char *版本。单元测试的印证tests/unit/unit2601.c 是 bufq 的专门单元测试由 tests/data/test2601 驱动。其中用循环写满、循环读空的用例验证了写满后CURLE_AGAIN、读空后CURLE_AGAIN以及读出的字节总数与写入总数严格相等nread nwrittenread empty fail用例直接断言对空 bufq 调用 read 返回 CURLE_AGAIN与文档语义一一对应。零拷贝通道slurp 与 pass 回调write/read都存在一次拷贝。为避免不必要的内存复制bufq提供了两个基于回调的通道读侧slurp、写侧pass。读侧Curl_bufq_slurptypedef 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_slurpnlib/bufq.c实现了读到阻塞或队列满为止的循环逻辑并遵循一条重要原则当某次返回的字节数少于请求量时立即停止if(q-tail !chunk_is_full(q-tail)) break;避免对慢速 reader 空转。写侧Curl_bufq_passtypedef 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_passCurl_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 消费队列此时用 peekbool 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_skip并prune_head跳过量超过缓冲总量则队列变空。Curl_bufq_pass正是依赖peek 拿指针 skip 消费这两个原语实现零拷贝外发的。生命周期init / free / resetbufq的初始化与释放风格与 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.hstruct 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_headlib/bufq.c下次需要新块时直接从 spare 取get_sparelib/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 limitBUFQ_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_size1000、max_chunks4的 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_sparelib/bufq.c的条件if(q-chunk_count q-max_chunks (!(q-opts BUFQ_OPT_SOFT_LIMIT)))可以看出——软上限下不因块数达标而拒绝取块prune_headlib/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。poolsbufc_pool 跨队列共享 chunkstruct 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_maxbufq 拿到池与max_chunks后不再关心 chunk 大小块的大小由池管理lib/bufq.c 中Curl_bufq_initp直接取pool-chunk_size需要块时 bufq 向池取bufcp_take从 spare 取或新分配块用完后归还池bufcp_putspare 达到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/2lib/http2.c 为单个连接创建stream_bufcp池并用Curl_bufq_initp让接收/发送两个 bufq 与各 stream 的发送 bufqlib/http2.c共享同一池正是文档池可在同线程多 bufq 间共享的实践HTTP chunked 解码lib/http_chunks.c 以CURL_CHUNKED_MAXLEN为块大小、BUFQ_OPT_SOFT_LIMIT创建 chunkbuf——解码需要一次性拿到完整 chunk 数据软上限保证不产生撕裂WebSocketlib/ws.c 用WS_CHUNK_SIZE与WS_CHUNK_COUNT分别初始化接收、发送 bufq同样配合 SOFT_LIMIT 保证消息完整性lib/ws.c 还有 16KB 级缓冲的暂存 bufqSMTP 与 sendflib/smtp.c 与 lib/sendf.c 均以16 * 1024为 chunk 大小、max_chunks1、BUFQ_OPT_SOFT_LIMIT创建暂存缓冲暂停/续传lib/cw-pause.c 用 bufq 实现写暂停时的数据暂存chunk 数固定为 1QUIC/HTTP3lib/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_LIMIT与BUFQ_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),仅供参考