环境:Linux kernel 5.15.78 aarch64 armv8
现象:使用vmap()以nocache的形式映射一段物理地址,使用memcpy()往其中写入数据,在写入最后64字节时发生如下错误
Unable to handle kernel paging request at virtual address ffffffc01a13567c
确认并未发生溢出,且使用memcpy_toio()或以cache形式映射则不会有这个问题。
网上搜了一圈没发现很好的解释,遂read the fxxking source code,趁热记录下来。
本文主要探讨两个问题:
1.memcpy()及memcpy_toio()的差别;
2.何时该使用memcpy()或memcpy_toio();
首先来看一下memcpy()的实现,在__HAVE_ARCH_MEMCPY未定义的情况下实现如下
lib/string.c
#ifndef __HAVE_ARCH_MEMCPY
/**
* memcpy - Copy one area of memory to another
* @dest: Where to copy to
* @src: Where to copy from
* @count: The size of the area.
*
* You should not use this function to access IO space, use memcpy_toio()
* or memcpy_fromio() instead.
*/
void *memcpy(void *dest, const void *src, size_t count)
{
char *tmp = dest;
const char *s = src;
while (count--)
*tmp++ = *s++;
return dest;
}
EXPORT_SYMBOL(memcpy);
#endif
这是一个通用的memcpy()实现,每次循环只copy一个字节。函数说明了在访问IO space时应该用memcpy_toio()或memcpy_fromio()来替代。但是WHY???这里并没有说明原因,先接着往下看。
通常为了更高的效率,memcpy()都由架构自己实现,arm64下的memcpy代码片段如下
arch/arm64/lib/memcpy.S
/* ... */
#define A_l x6
#define A_lw w6
#define A_h x7
#define B_l x8
#define B_lw w8
#define B_h x9
#define C_l x10
#define C_lw w10
#define C_h x11
#define D_l x12
#define D_h x13
/* ... */
L(copy128):
ldp E_l, E_h, [src, 32]
ldp F_l, F_h, [src, 48]
cmp count, 96
b.ls L(copy96)
ldp G_l, G_h, [srcend, -64]
ldp H_l, H_h, [srcend, -48]
stp G_l, G_h, [dstend, -64]
stp H_l, H_h, [dstend, -48]
L(copy96):
stp A_l, A_h, [dstin]
stp B_l, B_h, [dstin, 16]
stp E_l, E_h, [dstin, 32]
stp F_l, F_h, [dstin, 48]
stp C_l, C_h, [dstend, -32]
stp D_l, D_h, [dstend, -16]
ret
/* ... */
arm64下最多可以一次拷贝128个字节,同前者相比,效率可谓大大提升。
接下来看一下memcpy_toio()的实现
arch/arm64/kernel/io.c
void __memcpy_toio(volatile void __iomem *to, const void *from, size_t count)
{
非8字节对齐则一个个字节copy
while (count && !IS_ALIGNED((unsigned long)to, 8)) {
__raw_writeb(*(u8 *)from, to);
from++;
to++;
count--;
}
对齐则每次copy8字节
while (count >= 8) {
__raw_writeq(*(u64 *)from, to);
from += 8;
to += 8;
count -= 8;
}
剩余不足8字节部分一个个字节copy
while (count) {
__raw_writeb(*(u8 *)from, to);
from++;
to++;
count--;
}
}
EXPORT_SYMBOL(__memcpy_toio);
效率比通用memcpy()高,但和架构下的实现相比还是有所差距。
每次发生问题都是在copy最后64字节的时候,来看看memcpy()这时候在干嘛
/* Write the last iteration and copy 64 bytes from the end. */
L(copy64_from_end):
ldp E_l, E_h, [srcend, -64]
stp A_l, A_h, [dst, 16]
ldp A_l, A_h, [srcend, -48]
stp B_l, B_h, [dst, 32]
ldp B_l, B_h, [srcend, -32]
stp C_l, C_h, [dst, 48]
ldp C_l, C_h, [srcend, -16]
stp D_l, D_h, [dst, 64]
stp E_l, E_h, [dstend, -64]<--执行到这句出错
stp A_l, A_h, [dstend, -48]
stp B_l, B_h, [dstend, -32]
stp C_l, C_h, [dstend, -16]
ret
memcpy()会提前计算好dstend(dst+conut),即目标地址加上要写的大小。然后从dstend往前64字节开始copy最后64字节的内容。
其中E_l,E_h分别为寄存器x14,x15,都是64bit的寄存器。
这里就引发一个问题,如果dstend不是64bit对齐的话,就会引发一次非对齐访问。
猜测正是这个非对齐访问造成了错误。尝试将写入大小改为64bit对齐,果然不会出错了。而使用memcpy_toio()不会出错也正是因为最后非对齐的字节memcpy_toio()是采取byte by byte的方式进行copy的,不存在非对齐访问的问题。到这里算是解决了memcpy()与memcpy_toio()的问题,还剩另一个问题,为什么以cache形式映射的内存可以进行非对齐访问呢?ARM官方给出了说明https://developer.arm.com/documentation/ka004708/latest。
1.memcpy()比memcpy_toio()具有更高的效率;
2.memcpy()要求copy size对齐;
3.copy size对齐时两者可以互换;
4.copy size非对齐时,若不带缓存,则使用memcpy_toio(),若带缓存,则可使用memcpy();