set_track_update()是 SLUB track 机制的“写入阶段”:把已经准备好的地址、调用栈句柄、CPU、PID、时间戳写入对象的struct track。它与上一问的set_track_prepare()配合,完成完整的追踪记录。
一、函数签名
static void set_track_update(struct kmem_cache *s, void *object, enum track_item alloc, unsigned long addr, depot_stack_handle_t handle)
| 参数 | 含义 |
|---|---|
s | 缓存 |
object | 对象指针 |
alloc | TRACK_ALLOC或TRACK_FREE |
addr | 调用地址(返回地址) |
handle | 调用栈句柄,由set_track_prepare()生成 |
无返回值。
二、典型实现
static void set_track_update(struct kmem_cache *s, void *object, enum track_item alloc, unsigned long addr, depot_stack_handle_t handle) { struct track *p; if (!kmem_cache_debug_flags(s, SLAB_STORE_USER)) return; p = get_track(s, object, alloc); p->addr = addr; p->cpu = raw_smp_processor_id(); p->pid = current->pid; p->when = jiffies; #ifdef CONFIG_STACKDEPOT p->handle = handle; #endif }逻辑:
调试标志检查:未开
SLAB_STORE_USER直接返回,不写任何东西。定位 track:
get_track(s, object, alloc)返回指向track[alloc]的指针。填写字段:
addr:调用地址。cpu:当前 CPU 编号。pid:当前进程 PID。when:时间戳(jiffies)。handle:调用栈句柄(仅CONFIG_STACKDEPOT时)。
三、与set_track_prepare()的分工
两个函数把写入拆成两阶段:
set_track(s, object, alloc, addr) │ ├─ 阶段 1:采集 │ handle = set_track_prepare(gfp_flags) │ ├─ stack_trace_save() // 采集调用栈 │ └─ stack_depot_save() // 存入 depot,返回句柄 │ └─ 阶段 2:写入 set_track_update(s, object, alloc, addr, handle) ├─ get_track() // 定位 track 槽位 └─ 填写 addr/cpu/pid/when/handle
为什么拆成两个函数
关键在于gfp_flags和调用上下文:
set_track_prepare()可能需要分配内存(stack depot 存新栈),因此需要gfp_flags,且必须在合适的上下文调用(比如不能在持有某些锁时睡眠)。set_track_update()只是写内存,不分配,可以在任意上下文快速完成。
拆分后:
采集可以在安全的位置做。
写入可以在方便的位置做,甚至和采集不在同一处。
如果采集失败(handle 为 0),写入仍能进行,只是没有完整调用栈,仍有
addr保底。
这是典型的“准备工作”与“提交工作”分离。
四、get_track()回顾
static struct track *get_track(struct kmem_cache *s, void *object, enum track_item alloc) { struct track *p; p = object + get_info_end(s); return kasan_reset_tag(p + alloc); }object + get_info_end(s):跳到对象元数据区起点(对象数据 + 可能的外部 freepointer 之后)。p + alloc:按TRACK_ALLOC=0/TRACK_FREE=1索引到对应 track。kasan_reset_tag():去 KASAN tag 得到真实地址。
所以track[TRACK_ALLOC]和track[TRACK_FREE]各存一份现场。
五、写入的字段含义
| 字段 | 来源 | 用途 |
|---|---|---|
addr | 参数传入 | 单点定位调用者,%pS可解析符号 |
handle | 参数传入 | 完整调用栈,stack_depot_fetch()还原 |
cpu | raw_smp_processor_id() | 记录 CPU,分析并发/中断上下文 |
pid | current->pid | 记录进程,定位是哪个任务操作 |
when | jiffies | 时间戳,计算对象存活时长 |
raw_smp_processor_id()vssmp_processor_id()
smp_processor_id():在开启CONFIG_DEBUG_PREEMPT时检查是否在可抢占上下文误用。raw_smp_processor_id():不做检查,直接读 per-CPU 变量。这里在调试路径,且可能处于原子上下文,用
raw_版本避免检查开销和潜在告警。
六、使用场景
set_track_update()由set_track()调用,在对象分配和释放时记录现场:
static void set_track(struct kmem_cache *s, void *object, enum track_item alloc, unsigned long addr) { depot_stack_handle_t handle; if (!kmem_cache_debug_flags(s, SLAB_STORE_USER)) return; handle = set_track_prepare(GFP_ATOMIC); // 准备(采集栈) set_track_update(s, object, alloc, addr, handle); // 提交(写入) }分配:
alloc_debug_processing()里记TRACK_ALLOC。释放:
free_debug_processing()里记TRACK_FREE。
之后调试输出时:
struct track *t = get_track(s, object, TRACK_ALLOC); pr_err("Allocated by %pS, cpu %d, pid %d, jiffies %lu\n", (void *)t->addr, t->cpu, t->pid, t->when); // 有 STACKDEPOT 时还能打印完整栈 stack_depot_print(t->handle);七、与前面知识的呼应
| 前面讲过 | 这里体现 |
|---|---|
set_track_prepare() | 采集阶段,生成 handle |
get_track() | 定位 track 槽位 |
struct track | 被写入的结构 |
enum track_item | alloc参数,决定写哪个槽 |
kmem_cache_debug_flags(s, SLAB_STORE_USER) | 门控条件 |
kasan_reset_tag() | get_track()内使用 |
TRACK_ADDRS_COUNT | 采集栈深上限 |
八、设计要点总结
门控检查:未开
SLAB_STORE_USER直接返回,零开销。定位复用:用
get_track()统一算地址,与读取路径一致。字段完整:地址、句柄、CPU、PID、时间戳,覆盖定位所需全部信息。
条件编译:
handle字段仅在CONFIG_STACKDEPOT时写入。无分配:只写内存,可在原子上下文安全调用。
九、总结
set_track_update(s, object, alloc, addr, handle)是 track 机制的写入端:检查SLAB_STORE_USER后,用get_track()定位track[alloc],把调用地址、栈句柄、CPU、PID、时间戳写入struct track。它与set_track_prepare()分工——前者只管采集栈生成句柄,后者只管落盘写入,合起来构成 SLUB 完整的分配/释放追踪。