文章目录
-
- [0. 引言](#0. 引言)
- [1. 问题描述](#1. 问题描述)
- [2. 解决方案](#2. 解决方案)
-
- [2.1 默认使用缓存并在每次读取前invalidate缓存](#2.1 默认使用缓存并在每次读取前invalidate缓存)
- [2.2 默认使用NOCACHE并在需要时切换缓存状态](#2.2 默认使用NOCACHE并在需要时切换缓存状态)
- [2.3 代码示例](#2.3 代码示例)
- [3 测试结果与分析](#3 测试结果与分析)
-
- [3.1 分析](#3.1 分析)
- [3.2 优化策略](#3.2 优化策略)
- [4 结论](#4 结论)
- [5 附完整测试C++源码](#5 附完整测试C++源码)
0. 引言
在嵌入式Linux-ARM系统编程中,外部硬件加速器(如FPGA)和SOC(System on Chip)经常需要共享内存进行数据交换。SOC通过共享内存读取FPGA处理的数据,而FPGA则直接写入内存。这种架构在性能和实时性上有着明显的优势,特别是在需要高速数据传输的场景中,如视频处理、网络通信等。
然而,这种内存共享架构也带来了一些挑战。最主要的问题在于数据一致性:由于FPGA直接操作内存,而不经过CPU的控制,这导致CPU的缓存无法感知到内存中的数据变化。如果SOC继续使用缓存中的旧数据,将可能导致数据的不一致性,进而影响系统的稳定性和可靠性。
1. 问题描述
当SOC读取共享内存时,如果没有指定PROT_NOCACHE
(禁止缓存),CPU可能会直接从缓存中读取数据。然而,缓存中的数据可能是上一次读取时缓存下来的旧数据,这将导致SOC获取的不是FPGA最新写入的内容,从而引发一系列问题。例如,在图像处理应用中,可能导致处理的图像帧出现错乱或滞后。
QNX系统对这种问题的描述是:
你应该在共享内存区域使用
PROT_NOCACHE
,以便在访问可能由硬件(如视频帧缓冲区或内存映射的网络或通信板)修改的双端口内存时,确保读取到最新的数据。如果不使用该标志,处理器可能会返回之前缓存的"过期"数据。
因此,在涉及FPGA与SOC共享内存的场景中,如何正确管理缓存,确保数据一致性,成为了关键。
2. 解决方案
为了解决这个问题,我们可以考虑以下两种方案:
2.1 默认使用缓存并在每次读取前invalidate缓存
在每次SOC读取共享内存数据之前,先使用msync
接口将缓存失效,使得CPU在读取时必须从内存中重新获取数据。这种方法能够确保读取的数据是最新的,避免了数据不一致的问题。
然而,尽管这种方法在理论上可行,实际测试中发现其效果并不稳定。在某些情况下,invalidate操作并未成功,使得CPU仍然从缓存中获取了旧数据。
2.2 默认使用NOCACHE并在需要时切换缓存状态
另一种更为可靠的方法是,默认情况下将共享内存映射为NOCACHE,这样SOC每次读取的都是最新的数据。在需要提高读取速度时,可以临时打开缓存,在读取完数据后再将其恢复为NOCACHE状态。
这种方法可以通过mprotect
接口实现。具体步骤如下:
- 在映射内存时,使用
PROT_NOCACHE
标志,确保SOC读取的总是最新的数据。 - 在需要加速读取时,使用
mprotect
接口临时打开缓存。 - 读取完数据后,再次使用
mprotect
接口将缓存状态恢复为NOCACHE。
更多请查看之前的文章: QNX 平台下 mmap 缓存与非缓存模式的 memcpy 性能分析
2.3 代码示例
以下是使用mprotect
动态切换缓存状态的代码示例:
cpp
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) {
perror("open");
return -1;
}
size_t size = 4096;
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_NOCACHE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
close(fd);
return -1;
}
// 打开缓存
if (mprotect(ptr, size, PROT_READ | PROT_WRITE) != 0) {
perror("mprotect");
}
// 读取数据
// ... your code ...
// 恢复为NOCACHE
if (mprotect(ptr, size, PROT_READ | PROT_WRITE | PROT_NOCACHE) != 0) {
perror("mprotect");
}
munmap(ptr, size);
close(fd);
return 0;
}
通过这种方法,你可以在需要高速读取数据时临时打开缓存,同时在其他时间保持NOCACHE,以确保数据的一致性。
3 测试结果与分析
为评估上述两种方案的性能,我们进行了多组测试,测试数据如下:
memcpy cached
速度:2133.333333 MB/smemcpy nocached
速度:116.363636 MB/sinvalidate memcpy cached
速度:1333.333333 MB/sinvalidate memcpy nocached
速度:112.280702 MB/sasm memcpy cached
速度:2133.333333 MB/sasm memcpy nocached
速度:225.352113 MB/s
测试方法见之前的文章: QNX 平台下 mmap 缓存与非缓存模式的 memcpy 性能分析
3.1 分析
从测试结果可以看出,使用缓存可以显著提高数据读取的速度,例如memcpy cached
的速度达到了2133.333333 MB/s,而在不使用缓存的情况下,速度则大幅下降,仅为116.363636 MB/s。
对于数据一致性要求较高的场景,使用PROT_NOCACHE
虽然能够确保数据的一致性,但在读取速度上有明显的劣势。然而,通过动态切换缓存状态的方法,例如使用mprotect
接口,我们能够在一定程度上弥补这一缺陷。在默认NOCACHE的情况下,asm memcpy nocached
的速度为225.352113 MB/s,虽然比缓存情况下慢,但相比于直接使用memcpy nocached
已有明显改善。
3.2 优化策略
综合以上分析,对于FPGA与SOC共享内存的场景,可以采用以下优化策略:
-
数据一致性优先:对于需要确保数据一致性的场景,建议默认使用NOCACHE,并在读取数据时临时打开缓存,读取完毕后再恢复为NOCACHE。
-
性能优先 :对于性能要求更高的场景,可以默认使用缓存,但需在每次读取前使用
msync
等接口invalidate缓存,以确保数据的一致性。
4 结论
在FPGA与SOC共享内存的场景中,数据一致性和读取速度之间存在一定的权衡。使用PROT_NOCACHE
可以确保SOC读取到的总是最新的数据,但会牺牲一定的性能。通过动态切换缓存状态的方法,可以在保证数据一致性的前提下,适当提高数据读取速度。这种方法为处理数据一致性和性能之间的矛盾提供了有效的解决方案。
在实际应用中,根据具体需求选择适当的缓存管理策略,将能够更好地平衡数据一致性和系统性能。
5 附完整测试C++源码
cpp
// mmap_memcpy.c
#include <errno.h>
#include <fcntl.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#ifndef _QNX_
#define PROT_NOCACHE 0
#endif
inline void aarch64_fast_memcpy(void *dst, const void *src, size_t size) {
#ifdef _QNX_
void *ss = (void *)src, *dd = (void *)dst;
size_t sz = size;
asm volatile("loop_start: "
"ldp q3, q4,[%0,#0x0]\n"
"ldp q5, q6, [%0,#0x20]\n"
"ldp q7, q8, [%0,#0x40]\n"
"ldp q9, q10, [%0,#0x60]\n"
"stp q3, q4, [%1,#0x0]\n"
"stp q5, q6, [%1,#0x20]\n"
"stp q7, q8, [%1,#0x40]\n"
"stp q9, q10, [%1,#0x60]\n"
"add %0, %0, #0x80\n"
"add %1, %1, #0x80\n"
"subs %2, %2, #0x80\n"
"b.ne loop_start\n"
"dsb sy\n"
: /* no output */
: "r"(ss), "r"(dd), "r"(sz));
#endif
}
off_t offset(unsigned int bytes) {
static off_t base_offset = 0x1E000000;
off_t return_base_offset = base_offset;
base_offset += bytes;
return return_base_offset;
}
void *mmap_memory(unsigned int bytes, int flag) {
int fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) {
printf("open /dev/mem failed: %s\n", strerror(errno));
}
void *ptr = mmap(NULL, bytes, flag, MAP_SHARED, fd, offset(bytes));
close(fd);
if (MAP_FAILED == ptr) {
printf("mmap failed: %s\n", strerror(errno));
}
return ptr;
}
void *mmap_memory_cached(unsigned int bytes) {
return mmap_memory(bytes, PROT_READ | PROT_WRITE);
}
void *mmap_memory_nocached(unsigned int bytes) {
return mmap_memory(bytes, PROT_READ | PROT_WRITE | PROT_NOCACHE);
}
// if C++
#ifdef __cplusplus
#include <chrono>
using namespace std::chrono;
#define start() auto start_ = high_resolution_clock::now();
#define end() \
auto end_ = high_resolution_clock::now(); \
double bytes_mb = bytes * count / 1024.0 / 1024.0; \
double cost_ns = duration_cast<nanoseconds>(end_ - start_).count(); \
double mps = bytes_mb / cost_ns * 1e9;
#else
#include <time.h>
#include <unistd.h>
double time_diff_ns(struct timespec start, struct timespec end) {
return (end.tv_sec - start.tv_sec) * 1e9 + (end.tv_nsec - start.tv_nsec);
}
#define start() \
struct timespec start_, end_; \
clock_gettime(CLOCK_REALTIME, &start_);
#define end() \
clock_gettime(CLOCK_REALTIME, &end_); \
double bytes_mb = bytes * count / 1024.0 / 1024.0; \
double cost_ns = time_diff_ns(start_, end_); \
double mps = bytes_mb / cost_ns * 1e9;
#endif
double memcpy_speed(void *dst, void **src, unsigned int bytes,
unsigned int count) {
start();
for (size_t i = 0; i < count; i++) {
memcpy(dst, src[i], bytes);
}
end();
return mps;
}
double invalidate_memcpy_speed(void *dst, void **src, unsigned int bytes,
unsigned int count) {
start();
for (size_t i = 0; i < count; i++) {
msync(src[i], bytes, MS_INVALIDATE);
memcpy(dst, src[i], bytes);
}
end();
return mps;
}
double asm_memcpy_speed(void *dst, void **src, unsigned int bytes,
unsigned int count) {
start();
for (size_t i = 0; i < count; i++) {
aarch64_fast_memcpy(dst, src[i], bytes);
}
end();
return mps;
}
int main(int argc, char *argv[]) {
const unsigned int count = 2;
unsigned int bytes =
128 * 1024; // 128 KB
if (argc > 1) {
bytes = atoi(argv[1]) * 1024;
}
printf("bytes: %d\n", bytes);
printf("count: %d\n", count);
void *mmap_cached_src[count];
void *mmap_nocached_src[count];
for (size_t i = 0; i < count; i++) {
mmap_cached_src[i] = mmap_memory_cached(bytes);
mmap_nocached_src[i] = mmap_memory_nocached(bytes);
}
void *dst = malloc(bytes);
printf("memcpy cached speed: %f MB/s\n",
memcpy_speed(dst, mmap_cached_src, bytes, count));
printf("memcpy nocached speed: %f MB/s\n",
memcpy_speed(dst, mmap_nocached_src, bytes, count));
printf("invalidate memcpy cached speed: %f MB/s\n",
invalidate_memcpy_speed(dst, mmap_cached_src, bytes, count));
printf("invalidate memcpy nocached speed: %f MB/s\n",
invalidate_memcpy_speed(dst, mmap_nocached_src, bytes, count));
printf("asm memcpy cached speed: %f MB/s dst[0]=%d\n",
asm_memcpy_speed(dst, mmap_cached_src, bytes, count), ((char *)dst)[0]);
printf("asm memcpy nocached speed: %f MB/s\n",
asm_memcpy_speed(dst, mmap_nocached_src, bytes, count));
free(dst);
for (size_t i = 0; i < count; i++) {
munmap(mmap_cached_src[i], bytes);
munmap(mmap_nocached_src[i], bytes);
}
return 0;
}