我有一个 cpu 缓存一致性问题,我不知道如何解决。两个cpu看到同一个内存的不同内容
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
我有一个很奇怪的问题我想不通,我还没见过这么无法解释的事情
在我 30 多年的编程生涯中。显然我做错了什么,但无法弄清楚是什么,
我什至想不出解决办法。
我有一个我编写的 linux 内核模块,它实现了块设备。
它调用用户空间通过 ioctl 为块设备提供数据(如在用户空间中
程序通过 ioctl 调用内核模块来获取块设备请求)
有关我正在测试的机器的一些技术信息,以备不时之需:
它在 intel core2 i7 something orother 上完美运行。
> 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
它在 raspberry pi 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
它在 raspberry pi 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
但是在我的 raspberry pi 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() 注册了一个处理读写请求的回调函数,
我放弃了所有其他的,returning 错误(但我实际上除了 read/write 什么都没得到)
log_kern_debug("bio operation is not read or write: %d", operation);
bio->bi_status = BLK_STS_MEDIUM;
return BLK_QC_T_NONE;
我从内核得到回调,我遍历 bio 中的片段。
对于每个段,我向用户空间应用程序(在另一个线程中)发出请求以服务读取和写入请求。 (稍后我会解释它是如何工作的)然后原始请求线程进入休眠状态。当用户空间 returns 包含数据(用于读取)或
success/failure (for write) 它移交数据,唤醒原始请求线程,然后原始请求线程 return 将 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调用。该线程一直处于阻塞状态,直到请求进入块设备为止。
有关请求的信息(read/write、开始位置、长度等)被复制到用户空间提供的缓冲区 copy_to_user,然后 ioctl 调用被解除阻塞并 returns。用户空间从 ioctl 的 return 获取请求,进行读取或写入,然后使用请求的结果对内核模块进行另一个 ioctl 调用,然后唤醒原始请求线程,因此它可以 return make_request 回调中的结果,然后用户空间 ioctl 再次阻塞等待下一个请求进入。
问题来了。仅在 raspberry pi 4 上,每隔一段时间,而不是所有时间,
从两个线程的角度来看,两个线程之间传递的内存内容最终看起来并不相同。
就像数据从用户空间端线程传递到原始请求线程时一样
(对于本例中的读取请求),数据的哈希值(在内存中的相同位置!)是不同的。
我认为这是一个 cpu 缓存一致性类型问题,只是我调用了 mb()、smp_mb() 和 READ_ONCE() 以及 WRITE_ONCE()我什至尝试过让原始调用线程的 cpu 有时间注意到。
它肯定会失败,但不会一直失败。我没有任何其他 raspberry pi 4 可以测试,但我很确定这台机器没问题,因为其他一切都很好。这是我做的不对,但我不知道是什么。
接下来是 kern.log 的 grep 和显示正在发生的事情的解释。
每个去往用户空间的请求都会得到一个事务 ID。起始位置是
块设备中要读取或写入的位置。长度就是长度
bio段的read/write,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)首先write进来(trans id a2d)写入数据的crc32为10e5cad0。这就是我们希望在之后的所有读取中看到的 crc32,直到下一次写入。
2) 一个读取请求进入线程 31415 上的 blk_queue_make_request 回调处理程序。此时我记录 ("read0") bio 段缓冲区内容的 crc32,然后它是写入,因此我可以在 88314387 处看到 bio 段缓冲区的更改前值。
3) 我调用copy_to_user读取请求的信息。 return 来自 ioctl,用户空间对其进行处理,将 ioctl 与结果数据一起返回到内核模块,并且该数据被 copy_from_user()ed 到 bio 段缓冲区(位于 88314387)。
它从用户空间线程 31392 的角度记录 ("read1") bio 段缓冲区的 crc32。这是预期的 10e5cad0。
4) 用户空间唤醒原始请求线程 id 31415,因为数据位于 88314387 的 bio 段缓冲区中。线程 31415 再次计算 crc32 并记录 ("read2") 它从中看到的值31415的观点。正如预期的那样,它是 10e5cad0。
5) 为了进行额外的完整性检查(原因将在下一次交易中变得清晰),用户空间线程 31392 再次对 8831487 处的 bio 缓冲区进行 crc,并得出预期值 10e5cad0 并将其记录下来("readx")。
没有理由应该更改,没有人更新它,它仍然显示 10e5cad0。
6) 作为最后的额外健全性检查,原始请求线程 31415 休眠 2ms,并再次计算 crc32 并记录它 ("read3")。
一切正常,一切顺利。
现在我们来看下一个交易id c49。这是文件系统请求两次读取同一块的情况。我在测试中使用 echo 3 > /proc/sys/vm/drop_caches 强制执行此操作。我将从 2 开始计算步数,以便步数与第一个示例对齐。
2) 一个读取请求进入线程 31417 上的 blk_queue_make_request 回调处理程序。此时我在写入之前记录 ("read0") bio 段缓冲区内容的 crc32给它。这是来自第一个事务 b40(内存位置 88314387)的相同 bio 段缓冲区,但显然自从我们上次设置它以来它已经被覆盖,这很好。它似乎也被设置为与事务 b47 开始时相同的值,crc32 值为 9b5eeca2。没关系。在任何人写入缓冲区之前,我们从线程 id 31417 的角度知道 bio 段缓冲区的初始 crc32 值。
3) 我调用copy_to_user读取请求的信息。 return 来自 ioctl,用户空间对其进行处理,将 ioctl 与结果数据一起返回到内核模块,并且该数据被 copy_from_user()ed 到 bio 段缓冲区(位于 88314387)。
它从用户空间线程 31392 的角度记录 ("read1") bio 段缓冲区的 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() 应该可以解决所有问题,但事实并非如此。
我不知道如何解决这个问题。我什至想不出一个糟糕的解决方法。
我设置了一个内存位置的内容,而另一个线程却看不到它。
我该怎么办?
帮忙?
谢谢。
奇迹中的奇迹我完全是偶然发现了答案。
我想与大家分享,以防其他人遇到这个问题并同样苦恼几个月。
在我正在使用此块驱动程序的另一个系统的完全不相关的更改中,我今天进行了更改并在 pi4 上进行了尝试,就像魔术一样,一切正常。
发生了什么变化?根本不是我在看的地方....
所以我用 blk_queue_make_request 而不是 blk_init_queue 注册了一个回调。
我不处理请求队列,我直接处理块请求中的bios
在这样做的过程中,你被告知:
https://www.kernel.org/doc/htmldocs/kernel-api/API-blk-queue-make-request.html
"The driver that does this must be able to deal appropriately with buffers in “highmemory”. This can be accomplished by either calling __bio_kmap_atomic to get a temporary kernel mapping, or by calling blk_queue_bounce to create a buffer in normal memory. "
好吧,当我想获取缓冲区时,我已经通过调用 kmap_atomic 实现了这一点。今天我读到这些内存映射的插槽数量有限,你应该只在中断上下文中调用它并且不能进入睡眠状态,因为 kmap_atomic 调用从保留堆中提取所以它不必在调用时分配并可能进入睡眠状态。
但是我的内核模块可以休眠,所以我更改了对 kmap() 的调用并且...就像魔术一样...它正在工作。
所以我认为失败案例是 kmap_atomic 失败而我没有发现或注意到,或者可能是 kmap_atomic 在 pi4 或交互上有问题在那种情况下的内核之间。
我会玩更多游戏,看看我是否可以弄清楚发生了什么,但诀窍是我打电话的方式有问题 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 returns 与 kmap 不同的值时,其他线程无法正确看到内存。我读到一些东西说这些 kmap_atomic 映射有一个 per-cpu 缓存,如果是这样的话,这可以解释这种行为。
我有一个很奇怪的问题我想不通,我还没见过这么无法解释的事情 在我 30 多年的编程生涯中。显然我做错了什么,但无法弄清楚是什么, 我什至想不出解决办法。
我有一个我编写的 linux 内核模块,它实现了块设备。 它调用用户空间通过 ioctl 为块设备提供数据(如在用户空间中 程序通过 ioctl 调用内核模块来获取块设备请求)
有关我正在测试的机器的一些技术信息,以备不时之需:
它在 intel core2 i7 something orother 上完美运行。
> 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
它在 raspberry pi 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
它在 raspberry pi 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
但是在我的 raspberry pi 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() 注册了一个处理读写请求的回调函数, 我放弃了所有其他的,returning 错误(但我实际上除了 read/write 什么都没得到)
log_kern_debug("bio operation is not read or write: %d", operation);
bio->bi_status = BLK_STS_MEDIUM;
return BLK_QC_T_NONE;
我从内核得到回调,我遍历 bio 中的片段。 对于每个段,我向用户空间应用程序(在另一个线程中)发出请求以服务读取和写入请求。 (稍后我会解释它是如何工作的)然后原始请求线程进入休眠状态。当用户空间 returns 包含数据(用于读取)或 success/failure (for write) 它移交数据,唤醒原始请求线程,然后原始请求线程 return 将 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调用。该线程一直处于阻塞状态,直到请求进入块设备为止。 有关请求的信息(read/write、开始位置、长度等)被复制到用户空间提供的缓冲区 copy_to_user,然后 ioctl 调用被解除阻塞并 returns。用户空间从 ioctl 的 return 获取请求,进行读取或写入,然后使用请求的结果对内核模块进行另一个 ioctl 调用,然后唤醒原始请求线程,因此它可以 return make_request 回调中的结果,然后用户空间 ioctl 再次阻塞等待下一个请求进入。
问题来了。仅在 raspberry pi 4 上,每隔一段时间,而不是所有时间, 从两个线程的角度来看,两个线程之间传递的内存内容最终看起来并不相同。 就像数据从用户空间端线程传递到原始请求线程时一样 (对于本例中的读取请求),数据的哈希值(在内存中的相同位置!)是不同的。 我认为这是一个 cpu 缓存一致性类型问题,只是我调用了 mb()、smp_mb() 和 READ_ONCE() 以及 WRITE_ONCE()我什至尝试过让原始调用线程的 cpu 有时间注意到。 它肯定会失败,但不会一直失败。我没有任何其他 raspberry pi 4 可以测试,但我很确定这台机器没问题,因为其他一切都很好。这是我做的不对,但我不知道是什么。
接下来是 kern.log 的 grep 和显示正在发生的事情的解释。 每个去往用户空间的请求都会得到一个事务 ID。起始位置是 块设备中要读取或写入的位置。长度就是长度 bio段的read/write,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)首先write进来(trans id a2d)写入数据的crc32为10e5cad0。这就是我们希望在之后的所有读取中看到的 crc32,直到下一次写入。
2) 一个读取请求进入线程 31415 上的 blk_queue_make_request 回调处理程序。此时我记录 ("read0") bio 段缓冲区内容的 crc32,然后它是写入,因此我可以在 88314387 处看到 bio 段缓冲区的更改前值。
3) 我调用copy_to_user读取请求的信息。 return 来自 ioctl,用户空间对其进行处理,将 ioctl 与结果数据一起返回到内核模块,并且该数据被 copy_from_user()ed 到 bio 段缓冲区(位于 88314387)。 它从用户空间线程 31392 的角度记录 ("read1") bio 段缓冲区的 crc32。这是预期的 10e5cad0。
4) 用户空间唤醒原始请求线程 id 31415,因为数据位于 88314387 的 bio 段缓冲区中。线程 31415 再次计算 crc32 并记录 ("read2") 它从中看到的值31415的观点。正如预期的那样,它是 10e5cad0。
5) 为了进行额外的完整性检查(原因将在下一次交易中变得清晰),用户空间线程 31392 再次对 8831487 处的 bio 缓冲区进行 crc,并得出预期值 10e5cad0 并将其记录下来("readx")。 没有理由应该更改,没有人更新它,它仍然显示 10e5cad0。
6) 作为最后的额外健全性检查,原始请求线程 31415 休眠 2ms,并再次计算 crc32 并记录它 ("read3")。 一切正常,一切顺利。
现在我们来看下一个交易id c49。这是文件系统请求两次读取同一块的情况。我在测试中使用 echo 3 > /proc/sys/vm/drop_caches 强制执行此操作。我将从 2 开始计算步数,以便步数与第一个示例对齐。
2) 一个读取请求进入线程 31417 上的 blk_queue_make_request 回调处理程序。此时我在写入之前记录 ("read0") bio 段缓冲区内容的 crc32给它。这是来自第一个事务 b40(内存位置 88314387)的相同 bio 段缓冲区,但显然自从我们上次设置它以来它已经被覆盖,这很好。它似乎也被设置为与事务 b47 开始时相同的值,crc32 值为 9b5eeca2。没关系。在任何人写入缓冲区之前,我们从线程 id 31417 的角度知道 bio 段缓冲区的初始 crc32 值。
3) 我调用copy_to_user读取请求的信息。 return 来自 ioctl,用户空间对其进行处理,将 ioctl 与结果数据一起返回到内核模块,并且该数据被 copy_from_user()ed 到 bio 段缓冲区(位于 88314387)。 它从用户空间线程 31392 的角度记录 ("read1") bio 段缓冲区的 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() 应该可以解决所有问题,但事实并非如此。
我不知道如何解决这个问题。我什至想不出一个糟糕的解决方法。 我设置了一个内存位置的内容,而另一个线程却看不到它。 我该怎么办?
帮忙? 谢谢。
奇迹中的奇迹我完全是偶然发现了答案。 我想与大家分享,以防其他人遇到这个问题并同样苦恼几个月。
在我正在使用此块驱动程序的另一个系统的完全不相关的更改中,我今天进行了更改并在 pi4 上进行了尝试,就像魔术一样,一切正常。
发生了什么变化?根本不是我在看的地方....
所以我用 blk_queue_make_request 而不是 blk_init_queue 注册了一个回调。 我不处理请求队列,我直接处理块请求中的bios
在这样做的过程中,你被告知: https://www.kernel.org/doc/htmldocs/kernel-api/API-blk-queue-make-request.html
"The driver that does this must be able to deal appropriately with buffers in “highmemory”. This can be accomplished by either calling __bio_kmap_atomic to get a temporary kernel mapping, or by calling blk_queue_bounce to create a buffer in normal memory. "
好吧,当我想获取缓冲区时,我已经通过调用 kmap_atomic 实现了这一点。今天我读到这些内存映射的插槽数量有限,你应该只在中断上下文中调用它并且不能进入睡眠状态,因为 kmap_atomic 调用从保留堆中提取所以它不必在调用时分配并可能进入睡眠状态。
但是我的内核模块可以休眠,所以我更改了对 kmap() 的调用并且...就像魔术一样...它正在工作。
所以我认为失败案例是 kmap_atomic 失败而我没有发现或注意到,或者可能是 kmap_atomic 在 pi4 或交互上有问题在那种情况下的内核之间。 我会玩更多游戏,看看我是否可以弄清楚发生了什么,但诀窍是我打电话的方式有问题 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 returns 与 kmap 不同的值时,其他线程无法正确看到内存。我读到一些东西说这些 kmap_atomic 映射有一个 per-cpu 缓存,如果是这样的话,这可以解释这种行为。