errseq_t 数据类型¶
errseq_t 是一种在一个地方记录错误的方法,并允许任意数量的“订阅者”判断自上次采样以来它是否发生了变化。
最初的使用场景是跟踪文件同步系统调用(fsync、fdatasync、msync 和 sync_file_range)的错误,但它可能也适用于其他情况。
它被实现为一个无符号 32 位值。低位用于保存错误代码(1 到 MAX_ERRNO 之间)。高位用作计数器。这是通过原子操作而不是加锁实现的,因此这些函数可以在任何上下文中使用。
请注意,如果频繁记录新错误,由于我们用于计数器的位数很少,存在发生冲突的风险。
为了缓解这个问题,错误值和计数器之间的位被用作一个标志,以指示自记录新值以来该值是否已被采样。这使得我们可以在自上次记录错误以来没有人采样过该值时,避免递增计数器。
因此,我们最终得到一个看起来像这样的值:
31..13 |
12 |
11..0 |
计数器 |
SF |
错误码 |
大致思路是让“观察者”采样一个 errseq_t 值并将其作为正在运行的游标。该值以后可以用于判断自上次采样以来是否发生了任何新错误,并原子地记录检查时的状态。这使得我们可以在一个地方记录错误,然后有许多“观察者”可以判断该值自上次检查以来是否发生了变化。
一个新的 errseq_t 应该始终清零。全零的 errseq_t 值是一个特殊(但常见)的情况,表示从未发生过错误。因此,全零值可以作为“纪元”,如果有人想知道自首次初始化以来是否设置过错误。
API 用法¶
让我讲一个关于一名工人无人机的故事。他总体来说是个好工人,但公司有点……管理层重。今天他得向 77 个主管汇报,明天“大老板”要从外地过来,他肯定也会考验这个可怜的家伙。
他们都把工作交给他——工作多到他记不清谁给了他什么,但这并不是一个大问题。主管们只是想知道他何时完成了他们目前交给他所有工作,以及他自上次被问及以来是否犯了任何错误。
他可能在他们实际没交给他做的工作上犯了错误,但他无法在如此详细的层面上记录事情,他能记住的只是他最近犯的错误。
这是我们的 worker_drone 表示
struct worker_drone {
errseq_t wd_err; /* for recording errors */
};
每天,worker_drone 都从一个空白的状态开始
struct worker_drone wd;
wd.wd_err = (errseq_t)0;
主管们进来,获取当天最初的读数。他们不关心在他们监视开始之前发生的任何事情。
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);
现在他们开始把任务交给他。每隔几分钟,他们就要求他完成他们目前交给他所有工作。然后他们问他是否在其中犯了任何错误。
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。
现在,这家公司的老板相当吝啬,给了他劣质设备来完成工作。偶尔设备出故障,他就会犯错。他重重地叹了口气,然后把它记录下来。
errseq_set(&wd.wd_err, -EIO);
……然后回去工作。主管们最终会再次轮询,他们在下次检查时都会得到这个错误。随后的调用将返回 0,直到记录了另一个错误,此时它会向每个主管报告一次。
请注意,主管们无法得知他犯了多少错误,只能知道自他们上次检查以来是否犯了错误,以及记录的最新值。
偶尔大老板会来突击检查,让工人为他做一份一次性工作。他不像主管那样全天候监视工人,但他确实需要知道在处理他的工作时是否发生了错误。
他只需采样 worker 中当前的 errseq_t,然后用它来判断稍后是否发生了错误。
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”值,所以他不需要在这里推进它。他也不需要任何加锁,因为它不会被其他人使用。
序列化 errseq_t 游标更新¶
请注意,errseq_t API 在 check_and_advance 操作期间不保护 errseq_t 游标。只有规范的错误代码是原子处理的。在多个任务可能同时使用同一个 errseq_t 游标的情况下,序列化对该游标的更新很重要。
如果不这样做,游标可能会后退,从而导致同一个错误被报告多次。
因此,通常最好先执行 errseq_check 来查看是否有任何变化,然后再在获取锁之后执行 errseq_check_and_advance。例如:
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);
}
这在自上次检查以来没有发生任何变化的常见情况下避免了自旋锁。
函数¶
-
errseq_t errseq_set(errseq_t *eseq, int err)¶
设置 errseq_t 以便后续报告
参数
errseq_t *eseq
应设置的 errseq_t 字段
int err
要设置的错误(必须在 -1 和 -MAX_ERRNO 之间)
描述
此函数设置 eseq 中的错误,如果上次序列在过去的某个时间点被采样过,则递增序列计数器。
任何设置的错误都将始终覆盖现有错误。
返回
前一个值,主要用于调试目的。返回值不应在后续调用中用作先前采样值,因为它将不会设置 SEEN 标志。
-
errseq_t errseq_sample(errseq_t *eseq)¶
获取当前 errseq_t 值。
参数
errseq_t *eseq
指向要采样的 errseq_t 的指针。
描述
此函数允许调用者初始化其 errseq_t 变量。如果错误已被“看到”,新的调用者将不会看到旧错误。如果在 eseq 中存在一个未见的错误,此函数的调用者将在下次检查错误时看到它。
上下文
任何上下文。
返回
当前的 errseq 值。
-
int errseq_check(errseq_t *eseq, errseq_t since)¶
自某个特定采样点以来是否发生过错误?
参数
errseq_t *eseq
指向要检查的 errseq_t 值的指针。
errseq_t since
先前采样的 errseq_t,用于检查。
描述
获取 eseq 指向的值,并查看它是否自给定值采样以来发生了变化。since 值不会前进,因此无需将该值标记为已见。
返回
errseq_t 中设置的最新错误,如果未改变则为 0。
-
int errseq_check_and_advance(errseq_t *eseq, errseq_t *since)¶
检查 errseq_t 并前进到当前值。
参数
errseq_t *eseq
指向正在检查和报告的值的指针。
errseq_t *since
指向先前采样的 errseq_t 的指针,用于检查和前进。
描述
获取 eseq 值,并查看它是否与 since 指向的值匹配。如果匹配,则返回 0。
如果不匹配,则该值已更改。设置“seen”标志,并尝试将其交换为新的 eseq 值。然后,将该值设置为新的“since”值,并返回错误部分的设置值。
请注意,此处未提供对“since”值并发更新的锁定。如果需要,调用者必须提供。因此,调用者可能希望在获取锁并调用此函数之前,进行无锁的 errseq_check。
返回
如果已存储错误,则返回负 errno;如果未发生新错误,则返回 0。