【问题标题】:I have a cpu cache coherency-looking problem that I can't figure out how to fix. Two cpus see different contents of the same memory我有一个 cpu 缓存一致性问题,我不知道如何解决。两个cpu看到同一个内存的不同内容
【发布时间】:2020-04-13 19:40:22
【问题描述】:

我有一个非常奇怪的问题,我无法弄清楚,我还没有看到任何无法解释的事情 在我 30 多年的编程生涯中。显然我做错了什么,但无法弄清楚是什么, 我什至想不出办法。

我编写了一个实现块设备的 linux 内核模块。 它通过 ioctl 调用用户空间为块设备提供数据(如在用户空间中) 程序通过ioctl调用内核模块获取块设备请求)

我正在测试的机器上的一些技术信息,以防万一:

它在 intel core2 i7 上完美运行。

> cat /proc/cpuinfo 
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 58
model name      : Intel(R) Core(TM) i7-3770K CPU @ 3.50GHz
stepping        : 9
microcode       : 0x21
cpu MHz         : 1798.762
cache size      : 8192 KB
physical id     : 0
siblings        : 8
core id         : 0
cpu cores       : 4
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 13
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr pdcm pcid sse4_1 sse4_2 popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm cpuid_fault epb pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms xsaveopt dtherm arat pln pts md_clear flush_l1d
bugs            : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit
bogomips        : 7139.44
clflush size    : 64
cache_alignment : 64
address sizes   : 36 bits physical, 48 bits virtual
power management:

processor 1-7 are the same

它在树莓派 0 上完美运行

> cat /proc/cpuinfo 
processor       : 0
model name      : ARMv6-compatible processor rev 7 (v6l)
BogoMIPS        : 997.08
Features        : half thumb fastmult vfp edsp java tls 
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xb76
CPU revision    : 7

Hardware        : BCM2835
Revision        : 920093
Serial          : 000000002d5dfda3

它在树莓派 3 上完美运行

> cat /proc/cpuinfo
processor       : 0
model name      : ARMv7 Processor rev 4 (v7l)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU revision    : 4

processor       : 1-3 are the same

Hardware        : BCM2835
Revision        : a02082
Serial          : 00000000e8f06b5e
Model           : Raspberry Pi 3 Model B Rev 1.2

但是在我的树莓派 4 上,它做了一些我无法解释的非常奇怪的事情,我真的很难过 关于,我不知道如何解决。

> cat /proc/cpuinfo 
processor       : 0
model name      : ARMv7 Processor rev 3 (v7l)
BogoMIPS        : 270.00
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32 
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd08
CPU revision    : 3

Hardware        : BCM2835
Revision        : c03111
Serial          : 10000000b970c9df
Model           : Raspberry Pi 4 Model B Rev 1.1

processor       : 1-3 are the same

所以我正在向更了解 CPU、多线程、缓存一致性的人寻求帮助 和记忆障碍比我做的。 也许我叫错了树,如果是这样的话,你可以告诉我。 我很确定程序没问题,我一生中写过很多复杂的多线程程序。我已经检查了很多次,也让其他人检查过。 这是我写的第一个多线程内核模块,所以这就是我新的地方 领土。

这是怎么回事:

我使用处理读写请求的 blk_queue_make_request() 注册了一个回调, 我删除了所有其他的,返回错误(但我实际上除了读/写之外什么都没得到)

    log_kern_debug("bio operation is not read or write: %d", operation);
    bio->bi_status = BLK_STS_MEDIUM; 
    return BLK_QC_T_NONE;

我从内核获得回调,我遍历 bio 中的各个部分。 对于每个段,我向用户空间应用程序(在另一个线程中)发出请求以服务读取和写入请求。 (我将在一分钟内解释它是如何工作的)然后原来的请求线程进入睡眠状态。当用户空间返回数据(用于读取)或 成功/失败(用于写入)它移交数据,唤醒原始请求线程,然后原始请求线程将 bio 返回给内核,当所有段都已被服务时:

    bio_endio(bio); // complete the bio, the kernel does the followup callback to the next guy in the chain who wants this bio
    return BLK_QC_T_NONE;

调用用户空间的工作方式是这样的:首先,用户空间程序对内核模块和内核模块块进行ioctl调用。该线程一直处于阻塞状态,直到收到对块设备的请求。 有关请求的信息(读/写、开始位置、长度等)通过 copy_to_user 复制到用户空间提供的缓冲区,然后 ioctl 调用被解除阻塞并返回。用户空间从 ioctl 的返回中获取请求,进行读或写,然后用请求的结果对内核模块进行另一个 ioctl 调用,然后唤醒原来的请求线程,这样它就可以在 make_request 中返回结果回调,然后用户空间ioctl再次阻塞等待下一个请求进来。

所以这就是问题所在。仅在树莓派 4 上,每隔一段时间,而不是一直, 从两个线程的角度来看,两个线程之间传递的内存内容最终看起来并不相同。 就像数据从用户空间端线程传递到原始请求线程一样 (对于本例中的读取请求),数据的哈希值(在内存中的相同位置!)是不同的。 我假设这是一个 cpu 缓存一致性类型问题,除了我调用了 mb()、smp_mb() 和 READ_ONCE() 和 WRITE_ONCE(),我什至尝试了普通的旧睡眠来给原始调用线程的 cpu 时间通知。 它会可靠地失败,但并非总是如此。我没有任何其他树莓派 4 可供测试,但我很确定这台机器很好,因为其他一切都很好。这是我做的不对,但我不知道是什么。

接下来是 kern.log 的 grep 和显示正在发生的事情的解释。 进入用户空间的每个请求都会获得一个事务 ID。起始位置是 块设备中要读取或写入的位置。长度就是长度 要读/写的 bio 段的 crc32 列是 bio 中数据的 crc32 段缓冲区,(对于列出的长度,始终为 4k)。地址栏是地址 从用户空间读取的数据被复制到 bio 段缓冲区中(crc32 来自),对于给定的事务总是相同的,最后一列是 current->tid。

oper    trans id start pos        length           crc32            address  thread
write:  00000a2d 000000000001d000 0000000000001000 0000000010e5cad0          27240

read0:  00000b40 000000000001d000 0000000000001000 000000009b5eeca2 88314387 31415
read1:  00000b40 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31392
read2:  00000b40 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31415
readx:  00000b40 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31392
read3:  00000b40 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31415

read0:  00000c49 000000000001d000 0000000000001000 000000009b5eeca2 88314387 31417
read1:  00000c49 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31392
read2:  00000c49 000000000001d000 0000000000001000 000000009b5eeca2 88314387 31417
readx:  00000c49 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31392
read3:  00000c49 000000000001d000 0000000000001000 000000009b5eeca2 88314387 31417

read0:  00000d4f 000000000001d000 0000000000001000 000000009b5eeca2 88314387 31419
read1:  00000d4f 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31392
read2:  00000d4f 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31419
readx:  00000d4f 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31392
read3:  00000d4f 000000000001d000 0000000000001000 0000000010e5cad0 88314387 31419

read0:  00000e53 000000000001d000 0000000000001000 000000009b5eeca2 1c6fcd65 31422
read1:  00000e53 000000000001d000 0000000000001000 0000000010e5cad0 1c6fcd65 31392
read2:  00000e53 000000000001d000 0000000000001000 0000000010e5cad0 1c6fcd65 31422
readx:  00000e53 000000000001d000 0000000000001000 0000000010e5cad0 1c6fcd65 31392
read3:  00000e53 000000000001d000 0000000000001000 0000000010e5cad0 1c6fcd65 31422

所以过程中的步骤如下,让我们看看第一个事务,id b40,因为那个事务正常工作。然后我们将看看第二个 c49 不起作用。交易ID总是增加,上面的日志是按时间顺序排列的。

1)首先写入进来(trans id a2d)写入数据的crc32是10e5cad0。这就是我们希望在之后的所有读取中看到的 crc32,直到下一次写入。

2) 读取请求进入线程 31415 上的 blk_queue_make_request 回调处理程序。此时,我在写入之前记录(“read0”)生物段缓冲区内容的 crc32,因此我可以看到在 88314387 处更改 bio 段缓冲区的值。

3) 我将有关读取请求的信息称为 copy_to_user。从 ioctl 返回,用户空间对其进行处理,将 ioctl 与结果数据一起返回到内核模块,并将该数据 copy_from_user() 复制到 bio 段缓冲区(在 88314387 处)。 它从用户空间线程 31392 的角度记录(“read1”)生物段缓冲区的 crc32。这是预期的 10e5cad0。

4) 用户空间唤醒原始请求线程 id 31415,因为数据位于 88314387 的 bio 段缓冲区中。线程 31415 再次计算 crc32 并记录(“read2”)它从 31415 的角度看到的值.正如预期的那样,它再次是 10e5cad0。

5) 对于额外的完整性检查(其原因将在下一个事务中变得清楚),用户空间线程 31392 再次对 8831487 处的 bio 缓冲区进行 crc,并得出预期值 10e5cad0 并将其记录下来 ("阅读”)。 没有理由改变它,没有人更新它,它仍然显示 10e5cad0。

6) 作为最后的额外健全性检查,原始请求线程 31415 休眠 2ms,并再次计算 crc32 并记录它(“read3”)。 一切正常,一切顺利。

现在让我们看看下一个事务 id c49。这是文件系统请求读取同一块两次的情况。我在测试中使用 echo 3 > /proc/sys/vm/drop_caches 强制执行此操作。我将从 2 开始计算步数,因此步数与第一个示例一致。

2) 读取请求进入线程 31417 上的 blk_queue_make_request 回调处理程序。此时,我在写入之前记录(“read0”)生物段缓冲区内容的 crc32。这是与第一个事务 b40(内存位置 88314387)相同的 bio 段缓冲区,但显然自从我们上次设置它以来它已被覆盖,这很好。它似乎也被设置为与事务 b47 开始时相同的值,crc32 值为 9b5eeca2。没关系。在任何人写入缓冲区之前,我们从线程 id 31417 的角度知道该 bio 段缓冲区的初始 crc32 值。

3) 我将有关读取请求的信息称为 copy_to_user。从 ioctl 返回,用户空间对其进行处理,将 ioctl 与结果数据一起返回到内核模块,并将该数据 copy_from_user() 复制到 bio 段缓冲区(在 88314387 处)。 它从用户空间线程 31392 的角度记录(“read1”)生物段缓冲区的 crc32。这是预期的 10e5cad0。 用户空间线程 ID 将始终相同 31392,因为进行 ioctl 调用的用户空间程序是单线程的。

4) 用户空间唤醒原始请求线程 id 31417,因为数据应该在 88314387 处的 bio 段缓冲区中。 线程 31417 再次计算 crc32 并记录(“read2”)它从(线程 31417 的)角度看到的值。 但这一次,该值不是预期值 10e5cad0。相反,它与将请求发送到用户空间以更新缓冲区之前的值相同(9b5eeca2)。就好像用户空间没有写入缓冲区一样。但它确实做到了,因为我们读取了它,计算了 crc32 值并将其记录在用户空间端线程 31392 中。相同的内存位置,不同的线程,对 88314387 处的 bio 段缓冲区内容的不同感知。不同的线程,可能是不同的 cpu ,因此不同的cpu缓存。即使我搞砸了线程阻塞并唤醒日志显示事件的顺序,一个线程在另一个线程误读后读取了正确的值。

5) 再次进行额外的完整性检查,用户空间线程 31392 再次在 8831487 处对同一生物缓冲区执行 crc,获得相同的正确值 10e5cad0 (“readx”)。 日志是按时间顺序排列的,所以线程 id 31392 看到了正确的值,线程 id 31417 看到了错误的值。 线程 id 31392 得出预期值 10e5cad0 并将其记录下来(“readx”)。

6) 作为最后的额外健全性检查,原始请求线程 31417 休眠 2ms,并再次计算 crc32 并记录它(“read3”), 它仍然看到不正确的值 9b5eeca2。

在我上面记录的四个读取事务中,1、3 和 4 工作,而 2 没有。 所以我想,好吧,这一定是缓存一致性问题。但我添加了 mb() 和 smp_mb() 在 read1 和 read2 之前调用,没有任何变化。

我被难住了。我已经阅读了 linux 内核内存屏障页面

https://www.kernel.org/doc/Documentation/memory-barriers.txt

很多次,我认为 smp_mb() 应该可以解决所有问题,但事实并非如此。

我不知道如何解决这个问题。我什至想不出一个糟糕的解决方法。 我设置了一个内存位置的内容,而另一个线程只是看不到它。 我该怎么办?

帮助? 谢谢。

【问题讨论】:

  • 这个关于编程的网站。有可用的(简化)模块源的 Git 存储库吗?
  • @user3666197 我会首先责备硬件,因为我几十年的软件经验从未向我展示过这样的事情。但我发现很难想象 linux 内核中没有其他任何东西会触发这个问题,因为它发生在我身上。也就是说,我订购了另一个 pi4,所以我们拭目以待。
  • @user3666197 感谢您的智慧。我喜欢来自经验的好故事,尤其是当它与我的领域相关时,但有点超出我的领域。很高兴听到你有任何更明智的故事。关于尝试其他位置的好提示。不确定我对此有多少控制权,但我可以调查一下,你是对的,内核内容所在的高内存将比用户空间的可变性更小。我想了一些我可以尝试的代码变体,但新的 pi4 会以一种或另一种方式说明一切。再次感谢您的洞察力和经验。
  • 对于任何在家里跟随的人......第二个 pi4 到了,我试了一下,然后......同样的事情发生了。所以要么我真的很不幸得到了 2 个损坏的 pi4,要么我的程序中有一个我没有看到的错误,或者它是 pi4 中的一个一致的设计缺陷。因此,我将编写一个简单的概念证明并将其发送给 pi 人员,看看他们是否有什么要说的,或者可以告诉我哪里出了问题。
  • 我倾向于同意。我想我找到了一个涉及一些中间 memcpy 的解决方法,它工作得更可靠,但最终还是失败了,所以概念证明应该更容易编写。再次感谢所有的帮助和观点。如果我发现或解决任何其他问题,我会通知您。

标签: multithreading linux-kernel cpu cpu-cache smp


【解决方案1】:

所以奇迹的奇迹我完全偶然地遇到了答案。 我想分享一下,以防其他人遇到这种情况并且几个月来类似地敲打他们的头。

我正在使用这个块驱动程序对另一个系统进行完全不相关的更改,我今天做了一个更改并在 pi4 上尝试了它,就像魔术一样,它一切正常。

有什么变化?根本不是我在看的地方......

所以我用 blk_queue_make_request 而不是 blk_init_queue 注册了一个回调。 我不处理请求队列,我直接处理块请求中的bios。

这样做,您会被告知: https://www.kernel.org/doc/htmldocs/kernel-api/API-blk-queue-make-request.html

“执行此操作的驱动程序必须能够适当地处理“highmemory”中的缓冲区。这可以通过调用 __bio_kmap_atomic 来获得临时内核映射或调用 blk_queue_bounce 来创建普通内存中的缓冲区。"

嗯,当我想获取缓冲区时,我一直通过调用 kmap_atomic 来实现这一点。今天我读到这些内存映射的插槽数量有限,你应该只在你处于中断上下文并且无法进入睡眠时调用它,因为 kmap_atomic 调用从保留堆中拉出,所以它不会必须在通话中进行分配,并且可能会进入睡眠状态。

但是我的内核模块可以休眠,所以我将调用更改为 kmap() 并且......就像魔术一样......它正在工作。

所以我认为失败的情况是 kmap_atomic 失败而我没有注意到或注意到,或者可能是 pi4 上的 kmap_atomic 出现问题,或者在这种情况下内核之间的交互出现问题等等。 我会玩更多,看看我能不能弄清楚发生了什么,但诀窍是我调用 kmap_atomic 的方式有问题。

玩了一会儿...

Feb 25 21:12:46 pi205 kernel: [86149.193899][13905] kernel:    buffer after kmap_atomic ffefd000
Feb 25 21:12:46 pi205 kernel: [86149.193912][13905] kernel:    buffer after kmap        bfe84000

所以当 kmap_atomic 返回一个与 kmap 不同的值时,就是另一个线程没有正确看到内存。我读到一些东西说这些 kmap_atomic 映射有一个 per-cpu 缓存,这可以解释这种行为,如果是这样的话。

【讨论】:

    猜你喜欢
    • 2020-07-31
    • 1970-01-01
    • 1970-01-01
    • 2010-12-30
    • 2023-02-03
    • 2020-12-28
    • 2017-06-12
    • 2021-08-07
    • 1970-01-01
    相关资源
    最近更新 更多