为什么 BPF hash 表满了以后,插入新 key 会报 E2BIG,而更新已存在的 key 仍然成功?
简短回答: 对于 BPF_MAP_TYPE_HASH,max_entries 是对不同 key 数量的硬上限,而它是在元素分配时执行的:新 key 需要一个全新元素,当没有可用元素时,update 返回 E2BIG(文档写明:map 的元素数量达到了 max_entries 上限)。更新已存在的 key 不需要新元素:预分配路径用 per-CPU 的 extra_elems 槽位做交换,惰性路径(BPF_F_NO_PREALLOC)里源码注释明确写着——"当 map 满了而 update() 是替换旧元素时,允许分配,因为旧元素会立即被释放"。所以两种创建方式下,满容量时更新旧 key 都照常成功。BPF_MAP_TYPE_LRU_HASH 在满时则不这样失败:它驱逐最久未用条目来腾位置,而不是返回 E2BIG——这就是为什么一个你从未 delete 过的 key 会在容量压力后突然读回 NULL。
E2BIG 从哪来
uapi 契约是明确的。bpf_map_update_elem(2) 的 man 页(由 include/uapi/linux/bpf.h 生成)把 E2BIG 的 errno 描述写成了 "map 的元素数量达到了 map 创建时指定的 max_entries 上限",与 EEXIST(对已存在 key 传了 BPF_NOEXIST)和 ENOENT(对不存在的 key 传了 BPF_EXIST)并列。
两个 map 家族在各自不同的代码里碰到这个上限:
- 预分配 hash(默认)。
prealloc_init一次性把全部max_entries个元素放进一个连续池子,并配一条 per-CPU freelist。freelist 空了以后,alloc_htab_elem里的__pcpu_freelist_pop失败并返回ERR_PTR(-E2BIG)。 - 惰性 hash(
BPF_F_NO_PREALLOC)。alloc_htab_elem先调is_map_full检查容量:它把元素计数(大 map 用 batch 为 32 的percpu_counter,否则普通atomic_t)与max_entries比较。满了且没有旧元素可替换 →ERR_PTR(-E2BIG)。惰性路径上真正缺内存是另一个错:ENOMEM,而不是E2BIG。
map 创建 时也会出现 E2BIG——key_size + value_size 大到一个元素无法 kmalloc、per-CPU 值超过 PCPU_MIN_UNIT_SIZE、或 max_entries > 1UL << 31——所以光看 errno 不能区分你碰到的是哪一种。
为什么更新已存在的 key 不碰上限
htab_map_update_elem 先做 key 查找。找到 l_old 之后,满容量只在分配这一步才有意义:
- 预分配:
alloc_htab_elem取该 CPU 的extra_elems槽位,把l_old存进去(*pl_new = old_elem),新元素被换到桶头。不弹 freelist、不做容量检查、不会E2BIG——元素总数没有增长。 - 惰性:
is_map_full返回 true,但alloc_htab_elem里的注释写了规则:map 满了且这次 update 是替换旧元素,允许分配,因为旧元素会立即被释放(bucket 链表交换之后free_htab_elem,之前check_and_cancel_fields会取消内嵌 BPF 对象字段)。计数保持持平:新元素inc_elem_count,旧元素dec_elem_count。
容量检查 is_map_full 只在"满 map + 新 key"时被参考。这就是整个不对称的全部:到达 max_entries 后,"新 key" → E2BIG,"旧 key" → 成功。
spin lock 值的例外
如果 map 的 value 内嵌 bpf_spin_lock(update 时传 BPF_F_LOCK 标志——map 的 BTF record 必须带 BPF_SPIN_LOCK,否则 update 以 -EINVAL 被拒),htab_map_update_elem 走一条捷径:key 已存在时,它取元素上的自旋锁并调用 copy_map_value_locked 就地更新值,在任何分配之前返回。更新已存在的 spin-locked 值因此从不分配、不可能被容量挡住——bpf_spin_lock(kernel 5.1+)就是文档化的、让不同 CPU 上的程序并发修改同一值时保持值一致的方式。这类 map 上的新 key 仍走正常分配路径,满时同样 E2BIG。
LRU 变体为什么不一样
BPF_MAP_TYPE_LRU_HASH / LRU_PERCPU_HASH 把满时的 E2BIG 换成了驱逐。内核文档直说:"LRU hash 在 hash 表达到容量时会自动驱逐最久未用的条目。" update 路径(htab_lru_map_update_elem / __htab_lru_percpu_map_update_elem)在取桶锁之前就从 LRU 拿元素,因为驱逐可能需要的摘链操作本身要拿桶锁:prealloc_lru_pop → bpf_lru_pop_free。
bpf_lru_pop_free(在 kernel/bpf/bpf_lru_list.c)按级升级:
- 从本 CPU 的本地 free 列表(
free_llist)取一个节点;pending 节点不在这一步直接取,它们在下面的 fetch 里刷进全局列表。 - 取不到就从全局列表取至多
target_free个 free 节点进本地 free 列表——先把本 CPU 的 pending 节点按 ref bit 刷进全局 free/active/inactive 列表,再抽干全局 free 列表;然后重取本地 free 列表。 - 全局 free 列表不够就 shrink:把 inactive 列表里 ref bit 已清除的节点经
lru->del_from_htab移入 free 列表(从 hash 表摘链——与 delete 同一条摘链并释放路径,不清 value 字节)。 - inactive 列表也榨不出时做 force shrink:无视 ref bit 摘下一个节点,优先 inactive 列表而非 active 列表,保证即使 map 很"热"也能逼出一个元素。
- 仍没有就从某个 CPU 的本地列表偷取(free 再 pending)——按内核注释,轮转范围是"current CPU 和 remote CPU"(即全部 CPU),从
next_steal开始——拿不到锁的 CPU 跳过。 - 全失败则
prealloc_lru_pop返回空,update 以-ENOMEM失败——LRU 变体满容量时的失败形态是分配失败,不是普通 hash 那个显式的E2BIGfull 检查。
文档把这组升级概括为:用 CPU 本地状态、从全局列表取空闲节点、从全局列表拉任意节点并从 hash 表摘除、再从任意 CPU 的列表拉任意节点。一个值得记住的推论:LRU map 的"满"意味着现在驱逐,所以容量压力之后,一个你从未 delete 过的 key 可能查回 NULL——是容量驱逐,不是 delete。用户态用 bpf_map_info 的 map type 字段确认自己在跟哪一类打交道。
如何验证
- 盯 errno,不要盯消息。 把普通
BPF_MAP_TYPE_HASH填到max_entries,然后发两个 update:新 key 必须以E2BIG失败(bpf_map_update_elem/ libbpf 的bpf_map__update_elem返回的-ret经strerror),对已存在 key 的 update 必须返回 0。之后bpf_map_delete_elem一次再看:新 key 又能成功。 - 确认配置的上限。
bpf_map_info会报max_entries和 map 类型,你以为在 1000 个 key 时失败,可能只是这个 map 创建时max_entries更小——下结论前先查 fd 的 info。 - 把 LRU 驱逐和 bug 区分开。 容量满载的
BPF_MAP_TYPE_LRU_HASH上,你从未 delete 的 key 可能在别的 update 驱逐之后开始查失败;用bpf_map_lookup_elem返回NULL加上一次在普通 hash 上会E2BIG的新 key 插入来交叉验证。 - 不依赖计数器数元素。 普通 hash map 的
bpf_map_info没有稳定的"当前元素数"字段;用bpf_map_get_next_key迭代 key 估算(稳定全表扫描用 batch 版),并注意若传入的cur_key恰好刚被删除,迭代会从表里第一个 key 重新开始。 - 确认是哪一类分配。 若 map 用了
BPF_F_NO_PREALLOC创建,满容量时的 update 失败可能是ENOMEM(分配器耗尽)而非E2BIG——惰性路径上的E2BIG是那个显式的 full 检查,而ENOMEM来自bpf_mem_cache_alloc真的缺内存。
答案的边界
- map 创建 时的
E2BIG(key_size + value_size过大、per-CPU 值超PCPU_MIN_UNIT_SIZE、max_entries > 1UL << 31)与满 map 时的 update 共用 errno;单看返回码分不清是哪一种。 BPF_F_NO_COMMON_LRU的 per-CPU LRU map 让每个 CPU 维护自己的 LRU 列表,改变的是每 CPU 驱逐哪个元素,而不是"驱逐代替 E2BIG"这一行为本身。is_map_full用的元素计数在大 map 上是一个 batch 为 32 的percpu_counter,"满"的比较按设计是近似的;LRU map 上驱逐的保证是 LRU 性质,不是精确大小的承诺。- 被复用元素里的 value 字节是否残留是另一条边界(2026-09-23 那篇有文档):摘链与驱逐都不清 value 区域;
E2BIG与驱逐是容量决策,不是内存清刷保证。
参考资料
- BPF_MAP_TYPE_HASH,含 PERCPU 与 LRU 变体(内核文档)——默认预分配、
BPF_F_NO_PREALLOC、"hash 表达到容量时自动驱逐最久未用条目"、LRU update 的升级步骤、bpf_spin_lock同步。 - kernel/bpf/hashtab.c——
alloc_htab_elem(freelist pop →E2BIG;"当 map 满了而 update() 是替换旧元素"注释;extra_elems交换);htab_map_update_elem(先查找、BPF_F_LOCK就地捷径);htab_lru_map_update_elem/__htab_lru_percpu_map_update_elem(先分配后锁桶);is_map_full(percpu_counterbatch 32 对atomic_t);创建时E2BIG检查。 - kernel/bpf/bpf_lru_list.c——
bpf_lru_pop_free(本地 free 列表、跨 CPU 偷取、inactive shrink 经del_from_htab、force shrink 无视 ref bit)。 - include/uapi/linux/bpf.h——
bpf_map_update_elem(2)的E2BIG契约文字、BPF_F_LOCK/BPF_F_NO_PREALLOC/BPF_F_NO_COMMON_LRU标志、struct bpf_map_info(max_entries、map 的type字段)。
当日社区讨论
如实的覆盖说明:本次运行两个 watchlist 选中的 Slack 存档可达但滚动窗口内零消息(快照 ok,0 字节,0 条消息),allowlist 里的 Discord 频道、公开 bpf 邮件列表与 r/eBPF 仍仅限 visible-browser,且本次运行没有可用的浏览器会话——因此 2026-09-25 没有任何私有社区材料可用。上文问题是回退选择:这是被监控的 eBPF 开发社区中一个真实、反复出现的从业者边界("我的 map 满了以后拒收新 key,但更新同一个 key 却没事" / "我 LRU map 里一个从没 delete 过的 key 不见了"),完全以上述公开一手资料(内核 BPF 文档与上游 kernel/bpf/hashtab.c、bpf_lru_list.c、uapi 头文件)为依据,而不是任何具体 thread。
该领域反复出现的从业者主题——"我的 hash map value 变旧了,新会话没被记录,但老会话还在计数"——正落在这条边界上:一旦 key 空间超出 max_entries,每个新 key 都在 update 处以 E2BIG 被丢弃,而热的旧 key 照常工作;症状读起来像"新会话丢了、老的还在计数",修法是给 max_entries 留出余量(或换 BPF_F_NO_PREALLOC、或换 LRU 变体并接受驱逐),而不是加重试循环。LRU 一侧则反复以另一种惊讶出现:从未被 delete 的 key 在被驱逐后读回 NULL,常被误诊为 delete bug,实际是容量驱逐。公开文档锚定的收尾规则是:普通 hash map 的 max_entries 是分配时执行的硬上限(新 key E2BIG,更新旧 key 豁免,spin-locked 值就地更新);LRU hash map 把同样的容量变成一次驱逐决策——"满"意味着"更旧的东西现在离开",而不是"下一次插入失败"。本页不复现任何私有文字、身份、频道或链接。
继续阅读
返回索引
eBPF 问答
这里每天收录一个关于 eBPF、Linux 可观测性、性能分析、运行时扩展或安全问题的回答。每篇回答都以公开的一手资料为依据,并在末尾附上匿名化的当日社区讨论摘要。
上一篇 / 上一页
为什么 BPF LPM-trie 查找会返回更不具体的已存储前缀,key 的 prefixlen 字段又是怎么决定哪条记录命中的?
简短回答: 把查找 key 的 prefixlen 设为该 map 的最大前缀长度(4 字节 IPv4 数据域是 32,16 字节 IPv6 数据域是 128)。内核会把匹配截断在 min(node-prefixlen, key-prefixlen)(kernel/bpf/lpmtrie.c)上,因此一个带较小 prefixlen 的查找 key 相当于告诉遍历"最多只匹配这么多
下一篇 / 下一页
为什么 connect 或 accept 探针会漏掉通过文件描述符交接而换了主人的 socket?
简短回答:因为 connect 和 accept 是进程侧的动作,而 socket 的归属权属于一个生命周期比任何单个进程都长的内核对象。一个 struct sock 可以经由 systemd socket 激活的描述符、SCMRIGHTS 的 sendmsg,或者继承而来的文件描述符被交给另一个进程,而接收方进程里并不会因此多出一条新的 connect/accept 系统调用。内核追踪的是这个 socket
- 最后更新
- 2026年9月25日
- 首次发布
- 2026年9月25日
- 贡献者
- Yunwei 123
这个页面有帮助吗?