multithreading - 我有一个看起来像cpu缓存一致性的问题,我不知道该如何解决。两个cpus看到同一个内存的不同内容

标签 multithreading linux-kernel cpu cpu-cache smp

我有一个很奇怪的问题,我无法解决,我还没有看到任何无法解释的东西
在我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

因此,我向寻求更多关于cpus,多线程,缓存一致性的人寻求帮助。
和内存障碍比我要大。
也许是这样,我正在吠错一棵树,您可以告诉我。
我非常确定该程序还可以,我一生中编写了许多复杂的多线程程序。我已经检查了很多次,还有其他人也对其进行了检查。
不过,这是我编写的第一个多线程内核模块,因此这就是我要使用的新模块。
领土。

这是怎么回事:

我在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()进行了调用外,我什至尝试了普通 sleep 以提供原始调用线程的cpu时间通知。
它会可靠地失败,但并非总是如此。我没有其他树莓派4可以测试,但是我很确定这台机器还不错,因为其他一切都很好。这是我做错的事情,但我不知道该怎么办。

接下来是kern.log的grep,并说明了发生了什么。
每个进入用户空间的请求都会获得一个事务ID。开始位置是
块设备中要读取或写入的位置。长度就是长度
要读取/写入的生物段的值,crc32列是生物中数据的crc32
段缓冲区(对于列出的长度,始终为4k)。地址栏是地址
从用户空间读取的生物段缓冲区中的数据复制到(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

因此,该过程的步骤如下,让我们看一下第一个事务,标识为b40,因为该事务可以正常工作。然后,我们看一下第二个无效的c49。事务ID总是增加,上面的日志按时间顺序排列。

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

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

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

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

5)为了进行额外的完整性检查(其原因将在下一次事务中变得清楚),用户空间线程31392在8831487处再次执行生物缓冲区的crc,并得出期望值10e5cad0并将其记录下来(“readx”) 。
没有理由应该更改它,没有人更新它,它仍然显示为10e5cad0。

6)作为最终的额外检查,原始请求线程31415 sleep 2ms,然后再次计算crc32并将其记录下来(“read3”)。
一切正常,一切都很好。

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

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

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

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

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

6)作为最终的额外检查,原始请求线程31417 sleep 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()应该可以解决所有问题,但不能解决所有问题。

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

帮助?
谢谢。

最佳答案

如此奇迹的奇迹我完全是偶然地碰到了答案。
我想分享一下,以防其他任何人遇到这个问题,并用同样的方法敲打几个月。

我正在使用此块驱动程序对另一个系统进行完全无关的更改,今天我进行了更改并在pi4上进行了尝试,就像魔术一样,它们都可以正常工作。

发生了什么变化?根本不在我所寻找的地方...

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

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

“执行此操作的驱动程序必须能够正确处理“高内存”中的缓冲区。这可以通过调用__bio_kmap_atomic获得临时内核映射,或者通过调用blk_queue_bounce在普通内存中创建缓冲区来实现。

好吧,当我想要获取缓冲区时,我通过调用kmap_atomic实现了这一目标。今天,我读到那些内存映射的插槽数量有限,并且仅当您处于中断上下文中且无法进入休眠状态时才应调用该插槽,因为kmap_atomic调用从保留的堆中拉出,因此不会必须分配给通话,并且有可能进入休眠状态。

但是我的内核模块可以 sleep ,所以我将调用更改为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映射有一个每CPU缓存,如果是的话,这可以解释这种行为。

关于multithreading - 我有一个看起来像cpu缓存一致性的问题,我不知道该如何解决。两个cpus看到同一个内存的不同内容,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59439689/

相关文章:

python - 键盘同时中断多个线程

java - java fx 检测线程是否正在运行

c - Linux设备驱动编程中使用struct inode和struct file传递数据的原因

c++ - 函数在特定时间内消耗墙时间

math - 数学指令如何在硬件上实现?

c - 如何将参数从线程传递到c中的函数

Java多线程: Implementing of important methods in Thread class in Native Libraries?

BeDS 支持的最大文件大小的计算

由于未释放 Linux RAM 磁盘缓存导致的 Java OutOfMemoryError

检查CPU型号以执行特定的C代码