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。