ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux 内核 errseq_t 详解:基于原子操作的多订阅者错误跟踪机制

Linux 内核 errseq_t 详解:基于原子操作的多订阅者错误跟踪机制 Linux 内核 errseq_t 详解基于原子操作的多订阅者错误跟踪机制【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxerrseq_t 是 Linux 内核 lib/errseq.c 中实现的一种 32 位错误记录数据类型它允许把错误记录在一个地方让任意数量的订阅者subscriber/watcher各自判断自上次采样以来该错误是否发生了变化。它最初被设计用于跟踪文件同步类系统调用fsync、fdatasync、msync、sync_file_range的写回writeback错误但也适用于其他一错多报的场景。读完本文你将掌握 errseq_t 的位布局与计数器/采样标志设计、四个核心 API 的语义与使用姿势以及它在内核 fs 层syncfs、filemap 写回错误上报中的真实落地方式。一、为什么需要 errseq_t一次写回错误要报告给谁设想这样的场景一个文件系统超级块super_block上有多个文件描述符被写入写回writeback过程中发生了一次 I/O 错误。按照传统做法错误信息要么以标志位形式如AS_EIO、AS_ENOSPC挂在 address_space 上要么记录在超级块上但当多个进程分别对各自 fd 调用fsync()时内核需要回答一个关键问题这个错误该报告给谁已经报告过谁旧方案用filemap_check_errors()检查并清除标志位存在竞态与错误丢失/重复报告问题。errseq_t 正是为这个一处记录、多处查询、每人只报一次的需求而生的记录方只需原子地写一个值errseq_set()不需要加锁可从任何上下文包括中断/软中断调用每个订阅者各自持有一个游标cursor记录自己上次看到的值查询时通过比较当前值 vs 我的游标即可判断我是否见过这个错误并在检查的同时原子地推进游标保证每个订阅者恰好报告一次。二、位布局计数器 采样标志 错误码errseq_t 本质上就是typedef u32 errseq_t;见 include/linux/errseq.h一个无符号 32 位整数。其位划分如下引用自 Documentation/core-api/errseq.rst31..131211..0counter计数器SFseen flag已被采样标志errno错误码对应源码 lib/errseq.c 中的宏定义/* The low bits are designated for error code (max of MAX_ERRNO) */ #define ERRSEQ_SHIFT (ilog2(MAX_ERRNO) 1) /* This bit is used as a flag to indicate whether the value has been seen */ #define ERRSEQ_SEEN (1 ERRSEQ_SHIFT) /* Leverage macro ERRSEQ_SEEN to define errno mask macro here */ #define ERRSEQ_CTR_INC (1 (ERRSEQ_SHIFT 1))其中MAX_ERRNO在内核中为 4095因此ERRSEQ_SHIFT ilog2(4095) 1 12低 12 位bit 011存放错误码取值 1MAX_ERRNO对应负数 errno 的绝对值如-EIO存为EIObit 12ERRSEQ_SEEN采样标志。表示自上次记录错误以来是否至少有一个订阅者采样过这个值高 19 位bit 1331单调递增的序列计数器用于让订阅者区分新错误与旧错误。计数器为何必须有区分新旧错误如果只记录最新错误码订阅者无法区分这个-EIO是新发生的还是我上次已经看过的那次。计数器的作用就是让每次被观察到的错误更新都产生一个新的序列值即使两次错误码相同只要计数器不同订阅者就能判断出这是一个新错误。SEEN 标志缓解计数器溢出碰撞由于只有 19 位计数器如果频繁记录错误存在计数器回绕wrap around导致碰撞——订阅者可能把新错误误判为旧错误。SEEN 标志用于缓解这一点当错误被记录后如果没有任何人采样过那么再次记录错误时不递增计数器错误码直接覆盖即可反正没人关心旧值只有当错误被某个订阅者采样errseq_check_and_advance置上 SEEN之后下一次记录错误才会递增计数器。这保证了计数器只在有人真正在观察时才增长大幅降低碰撞概率。相应宏ERRNO_MASKERRSEQ_SEEN - 1用于提取错误码部分。全零值 纪元epoch一个新初始化的 errseq_t 必须清零。全零值是一个特殊但常见的状态表示从未发生过错误。如果需要判断自初始化以来是否出过错误全零值就充当纪元基准见 lib/errseq.c。三、核心 API 与使用模式worker_drone 的故事Documentation/core-api/errseq.rst 用一个打工机器人向 77 个主管汇报的寓言来讲解 API 用法我们完整复述并配以源码注释。故事设定worker_drone 记不住每个人给它派了什么活只能记住最近一次犯的错主管们只想知道自上次询问以来你有没有犯错。3.1 记录错误errseq_set()worker_drone 用一个 errseq_t 字段记录错误每天开始时清零struct worker_drone { errseq_t wd_err; /* for recording errors */ }; struct worker_drone wd; wd.wd_err (errseq_t)0;干活出错时调用errseq_set()记录errseq_set(wd.wd_err, -EIO);从实现看errseq_set()会校验错误码合法性err 0或(unsigned int)-err MAX_ERRNO时抛出WARN并直接返回旧值错误码 0 会被视为清除错误因此不接受越界错误码无处安放也不记录在循环中构造新值new (old ~(ERRNO_MASK | ERRSEQ_SEEN)) | -err;—— 清掉旧错误码与 SEEN 位写入新错误码仅当old ERRSEQ_SEEN即旧错误已被采样过时才递增计数器new ERRSEQ_CTR_INC;若新值等于旧值同码错误且未被采样则直接结束否则用cmpxchg原子换入与并发写者竞争时重试。函数返回旧值主要供调试使用——注意返回值不能当作后续比较的采样值因为它不会带有 SEEN 标志。3.2 订阅者采样errseq_sample()每个主管上班时取一个初始读数作为自己的私有游标从此刻开始观察struct supervisor { errseq_t s_wd_err; /* private cursor for wd_err */ spinlock_t s_wd_err_lock; /* protects s_wd_err */ }; struct supervisor su; su.s_wd_err errseq_sample(wd.wd_err); spin_lock_init(su.s_wd_err_lock);errseq_sample()的实现非常简洁READ_ONCE读取当前值如果该值尚未被任何订阅者看过!(old ERRSEQ_SEEN)则返回 0。这样做的意义是新加入的订阅者不会看到旧的、没人看过的错误——它只关心我上岗之后发生的事。3.3 检查并推进errseq_check_and_advance()主管每隔几分钟询问一次我派给你的活有没有出错spin_lock(su.su_wd_err_lock); err errseq_check_and_advance(wd.wd_err, su.s_wd_err); spin_unlock(su.su_wd_err_lock);在没有错误时这个调用一直返回 0。当 worker 记录了一个-EIO后主管们下次轮询时各自都会拿到一次-EIO之后的调用返回 0直到又一个新错误被记录届时每人再报告一次。errseq_check_and_advance()的语义是读取当前值若与*since相同则无事发生返回 0若不同则构造new old | ERRSEQ_SEEN用cmpxchg尝试把 SEEN 标志换入当前值换入失败也无所谓——要么被写者改了计数器/错误码要么被其他读者抢先置了 SEEN两种结果都可接受把*since new游标推进到最新值并返回-(new ERRNO_MASK)作为最新错误码。需要强调的是主管无法知道 worker 犯了几次错只能知道自上次检查以来有没有犯错以及最近一次错误是什么——这是 errseq_t 的能力边界。3.4 一次性检查errseq_check()偶尔大老板来做突击检查交给 worker 一个一次性任务。他并不像主管那样长期盯梢只需要知道任务处理期间是否出过错。此时只需采样一次之后无锁地检查errseq_t since errseq_sample(wd.wd_err); /* submit some work and wait for it to complete */ err errseq_check(wd.wd_err, since);由于检查完since就被丢弃、不需要推进所以这里不调用check_and_advance也不需要加锁该游标只有大老板自己用。errseq_check()的实现cur since返回 0否则返回-(cur ERRNO_MASK)。四、游标更新的串行化避免错误重复上报errseq_t API并不保护订阅者自己的游标since指针。在errseq_check_and_advance()中只有被查询的 errseq_t 值本身是原子处理的游标更新*since new不是原子的。如果多个任务同时使用同一个游标比如同一个 file 结构上的f_wb_err必须由调用者自己对游标更新加锁串行化——否则游标可能倒退导致同一个错误被重复报告见 Documentation/core-api/errseq.rst 的 Serializing errseq_t cursor updates 一节。正因如此推荐先无锁检查、后取锁推进的两段式写法把加锁延迟到确实有变化时if (errseq_check(wd.wd_err, READ_ONCE(su.s_wd_err))) { /* su.s_wd_err is protected by s_wd_err_lock */ spin_lock(su.s_wd_err_lock); err errseq_check_and_advance(wd.wd_err, su.s_wd_err); spin_unlock(su.s_wd_err_lock); }这样在自上次检查以来没有新错误的常见情况下完全避开自旋锁只有真正发生错误时才走慢路径。这也是 mm/filemap.c 中file_check_and_advance_wb_err()采用的模式。五、源码级实现剖析四个导出函数一览errseq_t 的全部实现集中在 lib/errseq.c约 200 行公共接口声明在 include/linux/errseq.h四个函数均通过EXPORT_SYMBOL导出可供内核模块使用函数原型职责原子性errseq_seterrseq_t errseq_set(errseq_t *eseq, int err)记录新错误必要时递增计数器内部 cmpxchg 循环任意上下文可调用errseq_sampleerrseq_t errseq_sample(errseq_t *eseq)返回当前值作为订阅游标未采样过的错误对新人不可见仅 READ_ONCEerrseq_checkint errseq_check(errseq_t *eseq, errseq_t since)检查自since以来是否有新错误不推进游标仅 READ_ONCEerrseq_check_and_advanceint errseq_check_and_advance(errseq_t *eseq, errseq_t *since)检查并推进游标置 SEEN 标志返回新错误码值本身原子游标需调用者保护四个函数都声明为可从任意上下文调用见 lib/errseq.c 的注释 This is done with atomics instead of locking so that these functions can be called from any context这是它区别于错误标志位 锁方案的关键优势。六、内核中的真实应用从 fsync 到 writeback 错误上报6.1 syncfs 系统调用超级块级错误游标在 fs/sync.c 中syncfs系统调用在完成文件系统同步后将超级块上的s_wb_err与当前文件描述符私有的f_sb_err游标做一次检查并推进ret2 errseq_check_and_advance(sb-s_wb_err, fd_file(f)-f_sb_err); return ret ? ret : ret2;这里s_wb_err是超级块上全局记录的写回错误errseq_tf_sb_err是每个 file 结构私有的游标见 include/linux/fs.h 的f_sb_err字段。多个进程各自 fsync 同一个文件系统时每个 fd 的游标独立推进从而保证每个调用者恰好报告一次新错误。6.2 filemapaddress_space 写回错误的记录与上报写回错误的上报链路位于 mm/filemap.c记录__filemap_set_wb_err()mm/filemap.c把错误记录到mapping-wb_errvoid __filemap_set_wb_err(struct address_space *mapping, int err) { errseq_t eseq errseq_set(mapping-wb_err, err); trace_filemap_set_wb_err(mapping, eseq); }在 mm/filemap.c 附近I/O 错误路径会调用errseq_set(filp-f_mapping-wb_err, -EIO)记录错误替代了旧的mapping_set_error()标志位方式。上报file_check_and_advance_wb_err()mm/filemap.c实现无锁快路径 有锁慢路径int file_check_and_advance_wb_err(struct file *file) { int err 0; errseq_t old READ_ONCE(file-f_wb_err); struct address_space *mapping file-f_mapping; /* Locklessly handle the common case where nothing has changed */ if (errseq_check(mapping-wb_err, old)) { /* Something changed, must use slow path */ spin_lock(file-f_lock); old file-f_wb_err; err errseq_check_and_advance(mapping-wb_err, file-f_wb_err); trace_file_check_and_advance_wb_err(file, old); spin_unlock(file-f_lock); } clear_bit(AS_EIO, mapping-flags); clear_bit(AS_ENOSPC, mapping-flags); return err; }这段代码正是上一节先errseq_check无锁探测、变化后再取锁errseq_check_and_advance模式的教科书式应用f_wb_err受f_lock保护确保它反映本 fd 最新的游标值但常见无错误路径完全不碰锁。同时它还会清掉旧方案遗留的AS_EIO/AS_ENOSPC标志实现与旧filemap_check_errors()行为的平滑兼容函数注释明确写道 Were mostly using this function as a drop in replacement for filemap_check_errors。配套辅助函数还包括filemap_sample_wb_err()、filemap_check_wb_err()供一次性采样、事后检查场景使用。6.3 NFS 客户端flush 时的错误回报fs/nfs/file.c 的nfs_file_flush()展示了采样—干活—检查模式先filemap_sample_wb_err()取基准游标执行nfs_wb_all()刷出全部脏页再用filemap_check_wb_err()检查任务期间是否出现了新错误并返回。6.4 更多使用者errseq_t 在仓库中还有多处落地可通过grep errseq fs/ mm/找到例如fs/jbd2/journal.c日志子系统跟踪 I/O 错误fs/gfs2/lops.cGFS2 日志操作错误跟踪fs/ext4/super.c 与 fs/ext4/ext4_jbd2.cext4 通过s_wb_err上报写回错误fs/overlayfs/super.coverlayfs 记录底层文件系统错误fs/nfsd/vfs.c 与 fs/nfsd/nfs4proc.cNFS 服务器端 COMMIT/写回错误报告。七、设计权衡与注意事项碰撞风险19 位计数器在错误被频繁记录时可能回绕碰撞。缓解手段是 SEEN 标志无人采样不递增计数器但文档明确指出 there is a risk of collisions if new errors are being recorded frequently。对于写回错误这种低频事件该风险可接受设计者正是据此做出 12 位 errno 1 位标志 19 位计数器的取舍。能力边界errseq_t 只能回答自上次采样以来是否发生过错误 最近一次错误是什么无法统计错误次数。若需要精确计数应另寻他法。游标串行化责任在调用者API 只保证被查询值本身原子的读写共享游标的更新必须由调用者加锁保护否则可能重复上报错误。初始化纪律errseq_t 必须清零初始化全零即纪元状态errseq_set()拒绝err 0因为 0 无法表达曾经有过错误。错误码范围只能记录绝对值不超过MAX_ERRNO的错误码越界错误会被WARN并丢弃。八、总结errseq_t 用 32 位同时编码最新错误码 采样状态 序列号以纯原子操作支撑一处记录、多订阅者各自判新、每人恰报一次的语义是解决 fsync 族系统调用写回错误上报问题的核心基础设施。其设计精髓在于用 SEEN 标志把计数器增长与是否有人在观察绑定用无锁 check 有锁 advance的两段式模式兼顾正确性与性能。理解 errseq_t就能理解内核从文件系统错误到用户态 fsync 返回值这条完整链路的底层机制。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表