怎么读这一卷
主线位置:两条主线的总览。本页把共享主线和构建主线各画一遍,标出每一步落在哪个函数上;下一部分的「一块缓冲的一生」用一个程序把两条线同时跑一遍,并打印每一步之后的内部形态。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|会用| use1[01 全景]
use1 --> use2["02 的 vtable 与存储"]
use2 --> use3[04 Buf 与 BufMut]
use3 --> use4[06 常见坑]
q -->|读懂实现| all[00 到 08 按顺序]
锁定版本
| 项 | 值 |
|---|---|
| crate | bytes 1.12.1 |
| tag | v1.12.1 |
| commit | 76c0fbb(2026-07-08,「Release bytes v1.12.1 (#838)」) |
| 依赖 | 默认 feature 下没有;可选的 serde、portable-atomic(feature 名 extra-platforms),见源码地图 |
| 实验用的 tokio | 1.54.1,和 tokio 卷相同(只有少数实验用到) |
源码链接都用 tag,形如 https://github.com/tokio-rs/bytes/blob/v1.12.1/src/bytes.rs#L101。想在本地对照着读:
git clone https://github.com/tokio-rs/bytes
git -C bytes checkout v1.12.1
仓库的 master 已经走过了这个 tag。写作时 master 比 v1.12.1 多 4 个提交,都还没有发版:「Simplify shared alignment assertions」(#841)、「Release shared reference on zero truncate」(#842,改了 Bytes::truncate)、「fix: sign-extend n-byte integers in try_get_int/try_get_int_le」(#839)、「inline (hint) put_slice of BytesMut」(#860)。这些都不在本卷的版本里,读 master 会看到对不上的代码和行号。
本站自己的 Cargo.lock 锁的是 bytes 1.12.0,比本卷早一个补丁版本。两者的差异就是下一节这一个修复;它对本站有没有影响,放在「在本站中」一部分讲。
1.12.0 到 1.12.1:只改了一件事
git diff v1.12.0 v1.12.1 只碰了四个文件:CHANGELOG.md、Cargo.toml(版本号)、src/bytes.rs、src/bytes_mut.rs。两个源文件的改动来自同一个提交「Handle unwinding from Box::new」(#837),CHANGELOG 里只有一行:
# 1.12.1 (July 8th, 2026)
### Fixed
- Properly handle when `Box::new` panics (#837)
问题出在「先构造一个已经拥有缓冲的值,再 Box::new 它」这个顺序上。1.12.0 的 BytesMut::promote_to_shared 是这样写的:
let shared = Box::new(Shared {
vec: rebuild_vec(self.ptr.as_ptr(), self.len, self.cap, off),
original_capacity_repr,
ref_count: AtomicUsize::new(ref_cnt),
});
let shared = Box::into_raw(shared);
rebuild_vec 先用 self 的 ptr、len、cap 拼出一个 Vec<u8>,它和 self 指向同一块缓冲。这时如果 Box::new 分配失败并且 panic(默认是 abort,但可以把分配器配置成 OOM 时 panic),栈展开会先 drop 这个临时的 Shared(连同里面的 Vec),再 drop self 这个 BytesMut,同一块缓冲被释放两次。报告问题的 #836 标题就是「Exception safety violations in BytesMut, leading to Double-Free」,它指出了两处:BytesMut::promote_to_shared 和 Bytes 的 shallow_clone_vec。#837 的说明:
This fixes a few cases where a panic from `Box::new` has incorrect behavior
because it runs the destructor of the argument passed to `new`. They are
fixed by ensuring that the allocation is performed before creating the value
to be stored in the box.
1.12.1 的写法是先分配一块未初始化的 Box<MaybeUninit<Shared>>,分配成功之后才构造 Shared,再用新加的 Shared::init_to_raw 原地写进去。promote_to_shared 现在是:
// Explicitly allocate before invoking rebuild_vec() so that
// the vector is not dropped if Box::new() panics.
let shared = Box::new(MaybeUninit::<Shared>::uninit());
let shared = Shared::init_to_raw(
shared,
Shared {
vec: rebuild_vec(self.ptr.as_ptr(), self.len, self.cap, off),
original_capacity_repr,
ref_count: AtomicUsize::new(ref_cnt),
},
);
bytes.rs 里的 Shared::init_to_raw 和 bytes_mut.rs 里的那份一模一样,因为两个文件各有一个自己的 Shared:
fn init_to_raw(b: Box<MaybeUninit<Self>>, v: Self) -> *mut Self {
let shared = Box::into_raw(b).cast::<Self>();
// SAFETY: The Box has the right layout.
unsafe { shared.write(v) };
shared
}
同样的改法用在了三处:BytesMut::promote_to_shared(BytesMut 第一次 split 时)、shallow_clone_vec(可提升的 Bytes 第一次 clone 时),以及 From<Vec<u8>> for Bytes。后者在 1.12.0 里已经把 vec 包进了 ManuallyDrop,issue 没有点它的名,这次是一并按同样的顺序改写:先判断 len == cap,再分配,再把 vec 交给 ManuallyDrop。正常运行时三处的行为、分配次数都和 1.12.0 一样,只有「Box::new 本身 panic」这条路径不同。本卷没有复现这个 panic:它要求把 OOM 配置成 panic 并让分配器在恰好那一次失败。
这三个函数在后面各有一页:提升见 clone 与提升和 split 与 freeze,From<Vec<u8>> 见几种存储。
两条主线
bytes 只有两个核心类型,方向相反:Bytes 只读、可以随便 clone 和切片,BytesMut 可写、同一时刻只有一个句柄能写某一段。全卷沿这两个类型展开,每页开头都会标出本页在哪条主线上。
共享主线(Bytes 的 clone / slice / drop 经由 vtable):
flowchart TB mk["构造:选一张 vtable"] --> st["from_static:STATIC"] mk --> vec["From Vec:PROMOTABLE 或 SHARED"] mk --> own["from_owner:Owned T"] mk --> fz["BytesMut::freeze"] st --> cl["clone:vtable.clone"] vec --> cl own --> cl fz --> cl cl --> sl["slice:clone 后改 ptr 与 len"] sl --> dr["drop:vtable.drop"] dr --> rel["计数归零才释放"]
- 一个
Bytes有四个字段:ptr、len、data: AtomicPtr<()>和vtable: &'static Vtable。Vtable是一个手写的函数指针表:clone、into_vec、into_mut、is_unique、drop。源码在data字段上的注释是// inlined "trait object"。见结构与 Vtable。 - 构造时选定 vtable,之后不再变。
from_static用STATIC_VTABLE,data为空,不分配;From<Vec<u8>>在len == cap时转成Box<[u8]>,按地址奇偶选PROMOTABLE_EVEN_VTABLE或PROMOTABLE_ODD_VTABLE,否则马上分配一个Shared、用SHARED_VTABLE;from_owner把任意AsRef<[u8]>包进Owned<T>,每个T一张Owned::<T>::VTABLE;BytesMut::freeze来的用bytes_mut.rs里的另一张SHARED_VTABLE。见几种存储。 Clone for Bytes只有一行:调用vtable.clone。静态存储什么都不做;共享存储把引用计数加一;可提升存储第一次被 clone 时才走shallow_clone_vec,分配Shared、用一次compare_exchange把data换过去,抢输的一方释放自己的Shared。见 clone 与提升。slice、split_to、split_off都是「先self.clone(),再改ptr和len」;切出来的某一段为空时不 clone,空的那一段是一个挂在STATIC_VTABLE上、不指向任何存储的Bytes。slice_ref用地址判断一个&[u8]是不是自己的子切片。见切片。Drop for Bytes调用vtable.drop。共享存储走release_shared:fetch_sub(1, Release),减到零的那个句柄再做一次Acquire读,然后释放Shared和缓冲。- 反方向:
is_unique、try_into_mut、Vec::from(bytes)分别走 vtable 的is_unique、into_mut、into_vec,唯一拥有时可以不拷贝地拿回可写的缓冲。
构建主线(BytesMut 的写入、split、reserve、freeze):
flowchart TB
wc["with_capacity:KIND_VEC"] --> wr["写入:chunk_mut 与 advance_mut"]
wr --> sp{"split_to"}
sp -->|第一次| pr["promote_to_shared:KIND_ARC"]
sp -->|已是 KIND_ARC| inc[引用计数加一]
pr --> fr["freeze:换成 Bytes"]
inc --> fr
fr --> rs["剩下的部分 reserve"]
rs -->|唯一拥有| reuse[原地复用]
rs -->|还有别人| alloc["新分配:回到 KIND_VEC"]
reuse --> wr
alloc --> wr
BytesMut的字段是ptr、len、cap、data: *mut Shared。with_capacity只是Vec::with_capacity再交给from_vec:这时data不是指针,而是一组位,最低位是KIND_VEC,往上是「原始容量」的编码和「前面被切掉了多少字节」。见 data 字段的编码。- 写入走
impl BufMut for BytesMut:chunk_mut交出未初始化的尾部(满了先reserve(64)),调用方写完再advance_mut;put_slice是extend_from_slice。见 BufMut 与 UninitSlice。 split_to调用shallow_clone:KIND_VEC时先promote_to_shared,分配一个引用计数为 2 的Shared,data变成指向它的指针(对齐保证最低位是 0,即KIND_ARC);已经是KIND_ARC就只把计数加一。然后两个句柄各自改ptr、len、cap,分到不重叠的两段。split_off和split同理。见 split 与 freeze。freeze:KIND_ARC时把同一个Shared指针挂到bytes_mut.rs的SHARED_VTABLE上,不拷贝也不分配;KIND_VEC时用rebuild_vec拼回Vec,走Bytes::from(vec),再advance掉前面切掉的偏移。这一步把构建主线接到共享主线上。- 被切走前半段的
BytesMut继续用。容量不够时reserve进入reserve_inner:KIND_VEC时要么把数据挪回开头,要么交给Vec::reserve;KIND_ARC且只剩自己一个句柄时,就地复用那块分配(必要时把数据挪回开头或扩大它);还有别的句柄活着,就按max(需要的容量, 原始容量)新分配一个Vec,拷贝数据、释放自己那份引用,回到KIND_VEC。见 reserve。
两条线在 hyper 里是这样接起来的(hyper 1.12.0,见 读写缓冲和解析与编码):
| 步骤 | hyper 的代码 | bytes 的主线 |
|---|---|---|
| 读 socket | poll_read_from_io:read_buf.reserve(next),chunk_mut() 交给 poll_read,读到 n 字节后 advance_mut(n) | 构建主线 2、5 |
| 切出请求头 | buf.split_to(len).freeze() | 构建主线 3、4 |
| 取 URI | 紧接着的 slice_ref:slice.slice_ref(&slice[path_range]) | 共享主线 4 |
| 取每个头部的值 | slice.slice(header.value.0..header.value.1) | 共享主线 4 |
| body 分块 | read_mem:read_buf.split_to(n).freeze() | 构建主线 3、4 |
请求头、URI、每个头部的值和 body 分块都是对同一块读缓冲的 split_to、slice 或 slice_ref,切的时候不拷贝字节;这块缓冲在最后一个引用它的句柄 drop 之后才释放。
用分配次数看两条主线
照上表写一个最小程序,用一个只数次数的全局分配器,看每一步分配了几次。依赖:
bytes = "=1.12.1"
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
use bytes::{BufMut, Bytes, BytesMut};
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
fn step(name: &str, before: &mut usize) {
let now = ALLOCS.load(Ordering::Relaxed);
println!("{name:<28} +{} alloc", now - *before);
*before = ALLOCS.load(Ordering::Relaxed);
}
fn main() {
println!("start");
let mut n = ALLOCS.load(Ordering::Relaxed);
let mut buf = BytesMut::with_capacity(1024);
step("BytesMut::with_capacity", &mut n);
buf.put_slice(b"GET /a HTTP/1.1\r\n\r\nbody");
step("put_slice", &mut n);
let head = buf.split_to(19);
step("split_to (first split)", &mut n);
let head: Bytes = head.freeze();
step("freeze", &mut n);
let path = head.slice(4..6);
step("slice", &mut n);
let path2 = path.clone();
step("clone", &mut n);
let body = buf.split_to(4).freeze();
step("split_to + freeze (again)", &mut n);
println!("path = {:?}, body = {:?}", path2, body);
println!("same allocation: {}", head.as_ptr() as usize + 4 == path.as_ptr() as usize);
drop((head, path, path2, body, buf));
step("drop all", &mut n);
let v = b"hello".to_vec();
step("vec (len == cap)", &mut n);
let a = Bytes::from(v);
step("Bytes::from(vec)", &mut n);
let b = a.clone();
step("first clone", &mut n);
let c = a.clone();
step("second clone", &mut n);
drop((a, b, c));
let s = Bytes::from_static(b"static");
let t = s.clone();
step("from_static + clone", &mut n);
drop((s, t));
}
开头先打印一行 start,让标准输出在计数开始前就分配好自己的缓冲;step 打印完再重新读一次计数,打印本身的分配不算进下一步。M2 Max、rustc 1.99.0,cargo run --release 跑 3 次,输出完全相同:
start
BytesMut::with_capacity +1 alloc
put_slice +0 alloc
split_to (first split) +1 alloc
freeze +0 alloc
slice +0 alloc
clone +0 alloc
split_to + freeze (again) +0 alloc
path = b"/a", body = b"body"
same allocation: true
drop all +0 alloc
vec (len == cap) +1 alloc
Bytes::from(vec) +0 alloc
first clone +1 alloc
second clone +0 alloc
from_static + clone +0 alloc
- 构建主线上只有两次分配:
with_capacity分配 1024 字节的缓冲,第一次split_to时promote_to_shared分配Shared。之后的freeze、slice、clone,以及第二次split_to+freeze,都只动指针和引用计数。same allocation: true说明path就指在head的第 4 个字节上。 Bytes::from(vec)在len == cap时不分配(走可提升存储),第一次 clone 才分配那个Shared,第二次 clone 只加计数。这就是「可提升」的意思。from_static和它的 clone 都不分配。
每一步之后 data 里到底是什么、引用计数是几,在一块缓冲的一生里逐步打印出来。
阅读路线
- 只想会用:读 01 全景、02 的前两章(结构与 Vtable、几种存储)、04 Buf 与 BufMut,以及常见坑。读完能判断一个
Bytes的 clone 会不会分配,知道BytesMut::with_capacity给的是容量不是长度,知道Buf::chunk只给当前这一段。 - 想读懂实现:按顺序读。02 是重点,四页分别讲 vtable 的形状、每张 vtable 的函数、可提升存储第一次 clone 时的竞争、切片为什么只改两个字段;03 讲
BytesMut怎么把元数据塞进data的指针位;05 把各章的取舍汇总。
前置知识
这一卷要求会写 Rust,读得懂 unsafe 块。bytes 不依赖任何运行时,下面几样东西在正文里反复出现:
| 概念 | 讲它的页 | 本卷用到它的地方 |
|---|---|---|
Arc 的引用计数与 Release / Acquire | 一个 usize 的状态机最后一节「引用计数:和 Arc 一样的取舍」 | Bytes 和 BytesMut 的 Shared 都是去掉了弱引用的手写 Arc;release_shared 照抄标准库的 drop 顺序 |
| 手写 vtable 擦掉类型 | 任务的内存布局「RawTask 和 Harness」一节 | Bytes 的 Vtable 是同一种做法:一个裸指针加一张 'static 函数表 |
| 裸指针与对齐 | — | 用指针的最低位当标记:KIND_VEC / KIND_ARC,可提升存储的奇偶两张表 |
MaybeUninit 与未初始化内存 | — | BufMut::chunk_mut 返回 &mut UninitSlice;1.12.1 的 Box<MaybeUninit<Shared>> |
tokio 的 ReadBuf 与 poll_read | 从 TcpStream::read 到挂起 | hyper 把 BytesMut 的未初始化尾部交给 poll_read |
本卷的约定
- 每页开头的「主线位置」说明本页在共享主线(
Bytes)、构建主线(BytesMut)上的哪一步,还是两条都不在(比如源码地图)。 - 摘自 bytes 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照,删掉的行用
// ...标出;本站自己写的示例用 2 空格缩进。 - 关于行为的说法(哪张 vtable、什么时候分配、
reserve之后容量是多少、什么时候 panic)尽量配一个小程序和它的真实输出。每个示例上方写出依赖,版本用=锁死:bytes = "=1.12.1"。实验的平台是 M2 Max、rustc 1.99.0。 - 分配次数用上面那种只数次数的
#[global_allocator]观察。内部状态尽量只通过公开 API 看:as_ptr、len、capacity、is_unique、try_into_mut、指针相等。个别实验要读私有字段(data的位、用的是哪张 vtable)时,会先核对size_of再transmute到一个同布局的镜像结构体,并在页面上注明:这依赖 1.12.1 的私有布局,只用于观察,换个版本可能就不对了。 - panic 信息照源码和实际输出原样引用,不翻译。
目录
| 部分 | 内容 |
|---|---|
| 00 导读 | 本页;源码地图 |
| 01 全景 | bytes 解决什么问题;一块缓冲的一生 |
| 02 Bytes | 本卷重点:结构与 Vtable、几种存储、clone 与提升、切片 |
| 03 BytesMut | data 字段的编码、split 与 freeze、reserve |
| 04 Buf 与 BufMut | Buf、BufMut 与 UninitSlice、适配器 |
| 05 设计取舍 | 把各章的取舍汇总到一起 |
| 06 常见坑 | 用 panic 原文或现象做标题,每个都有最小复现 |
| 07 在本站中 | 本站(开源)代码里的 bytes;1.12.0 与 1.12.1 的差异对本站的影响 |
| 08 动手 | 不用 bytes,从零写一个最小的 Bytes 和迷你 BytesMut |
小结
- 全卷对着
v1.12.1(76c0fbb,2026-07-08)读;master 上还有 4 个没发版的提交,读 master 会对不上行号。本站锁的是 1.12.0。 - 1.12.1 只修了一件事(#837):先分配
Box<MaybeUninit<Shared>>、再构造拥有缓冲的Shared,避免Box::newpanic 时同一块缓冲被释放两次。改动落在promote_to_shared、shallow_clone_vec和From<Vec<u8>>,正常路径行为不变。 - 共享主线:构造时选定 vtable →
clone走vtable.clone(静态不做事、共享加计数、可提升的第一次才分配)→slice/split_*先 clone 再改ptr、len→drop走vtable.drop,计数归零才释放。 - 构建主线:
with_capacity得到KIND_VEC→chunk_mut/advance_mut写入 → 第一次split_to提升成KIND_ARC→freeze换一张 vtable 变成Bytes→ 剩下的部分reserve时能复用就复用,否则新分配回到KIND_VEC。 - 照 hyper 的路径写的最小程序里,bytes 只在
with_capacity和第一次split_to时分配;请求头、URI、头部值、body 分块都切自同一块读缓冲。