关于mmap将物理空间映射到用户空间再落盘时使用O_DIRECT方式会提示bad address的问题如何解决?

linux中将外设物理地址mmap映射到用户空间,通过在用户进程中访问映射的虚拟地址来读取外设的数据,并落盘,落盘时open函数使用O_DIRECT方式,直接write数据会报bad address错误,但如果使用posix_memalign申请一块空间来中转数据,再write就能写进去,这是为什么呢?
因为项目追求速度要快,所以使用O_DIRECT的方式,如果在途中还需要memcpy也会影响效率,有其他方法可以不经过缓存直接落盘吗?

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>

#define SHR_MEMSIZE            0x800000
#define MEM_CLEAR               0x0
#define MEM_RESET               0x1
#define MEM_DEV_FILENAME       "/dev/mychrmem"

int main()
{
    int         fd;
    int         file_fd;
    ssize_t        ret;
    char        *shm = NULL;
    char        *rev_buf;

//    posix_memalign((void**)&rev_buf, 512, SHR_MEMSIZE);

    fd = open(MEM_DEV_FILENAME, O_RDWR);
    if (fd < 0) {
        printf("open(): %s\n", strerror(errno));
        return -1;
    }

    file_fd = open("/mnt/test",O_WRONLY|O_CREAT|__O_DIRECT,0666);
    if (file_fd < 0)
    {
        perror("open");
        return -1;
    }

    shm = mmap(NULL, SHR_MEMSIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == shm) {
        printf("mmap: %s\n", strerror(errno));
    }

    printf("Before Write, shm = %s\n", shm); 

    strcpy(shm,"User write to share memory!");

    printf("After write, shm = %s\n", shm);

    while(1)
    {
//        memcpy(rev_buf, shm, SHR_MEMSIZE);       //中转数据能成功,但影响效率
//        ret = write(file_fd, rev_buf, SHR_MEMSIZE);

        ret = write(file_fd, shm, SHR_MEMSIZE);      //直接写会提示bad address

        printf("mem addr is 0x%lx\n",(long)shm);
        if(ret==-1)
        {
            perror("write");
        }
        else
        {
            printf("write len 0x%lx\n",ret);
        }
    }

    munmap(shm, SHR_MEMSIZE);
    close(file_fd); 
    close(fd);
    return 0;    
}


该回答引用ChatGPT
问题分析:
在使用mmap将物理空间映射到用户空间后,通过直接写入文件的方式落盘时,使用O_DIRECT方式会提示bad address错误。但是如果使用posix_memalign申请一块空间来中转数据,再write就能写进去。为什么会出现这种情况呢?
原因分析:
O_DIRECT方式要求数据必须对齐到块大小,否则会报错。而mmap映射的虚拟地址并不一定是对齐的,因此直接使用O_DIRECT方式写入会出现bad address错误。
而使用posix_memalign申请一块空间来中转数据,可以保证数据对齐到块大小,因此可以成功写入。
解决方法:
1. 使用posix_memalign申请一块空间来中转数据,再write。
2. 使用O_DIRECT方式写入时,需要保证数据对齐到块大小。可以使用posix_memalign申请一块空间,将mmap映射的虚拟地址复制到该空间中,再使用O_DIRECT方式写入该空间即可。
代码示例:

#include
#include
#include
#include
#include
#include
#include
#define PAGE_SIZE 4096
#define FILE_SIZE (PAGE_SIZE * 10)
int main()
{
int fd, file_fd;
char *buf, *aligned_buf;
size_t size = FILE_SIZE;
// 打开文件
file_fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT, 0666);
if (file_fd < 0) {
perror("open");
exit(1);
}
// 分配内存
if (posix_memalign((void **)&buf, PAGE_SIZE, size)) {
perror("posix_memalign");
exit(1);
}
// 映射物理地址到用户空间
fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) {
perror("open");
exit(1);
}
aligned_buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x10000000);
if (aligned_buf == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 将物理地址映射到的数据复制到对齐的内存中
memcpy(buf, aligned_buf, size);
// 写入文件
if (write(file_fd, buf, size) < 0) {
perror("write");
exit(1);
}
// 释放资源
munmap(aligned_buf, size);
free(buf);
close(file_fd);
close(fd);
return 0;
}
该回答引用ChatGPT
问题分析:
在使用mmap将物理空间映射到用户空间后,通过直接写入文件的方式落盘时,使用O_DIRECT方式会提示bad address错误。但是如果使用posix_memalign申请一块空间来中转数据,再write就能写进去。为什么会出现这种情况呢?
原因分析:
O_DIRECT方式要求数据必须对齐到块大小,否则会报错。而mmap映射的虚拟地址并不一定是对齐的,因此直接使用O_DIRECT方式写入会出现bad address错误。
而使用posix_memalign申请一块空间来中转数据,可以保证数据对齐到块大小,因此可以成功写入。
解决方法:
1. 使用posix_memalign申请一块空间来中转数据,再write。
2. 使用O_DIRECT方式写入时,需要保证数据对齐到块大小。可以使用posix_memalign申请一块空间,将mmap映射的虚拟地址复制到该空间中,再使用O_DIRECT方式写入。
代码示例:

#include
#include
#include
#include
#include
#include
#include
#define PAGE_SIZE 4096
#define FILE_SIZE (PAGE_SIZE * 10)
int main()
{
int fd, file_fd;
char *buf, *aligned_buf;
ssize_t ret;
fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) {
perror("open");
return -1;
}
buf = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (buf == MAP_FAILED) {
perror("mmap");
return -1;
}
posix_memalign((void **)&aligned_buf, PAGE_SIZE, FILE_SIZE);
memcpy(aligned_buf, buf, FILE_SIZE);
file_fd = open("test.txt", O_WRONLY | O_CREAT | O_DIRECT, 0666);
if (file_fd < 0) {
perror("open");
return -1;
}
ret = write(file_fd, aligned_buf, FILE_SIZE);
if (ret < 0) {
perror("write");
return -1;
}
munmap(buf, FILE_SIZE);
free(aligned_buf);
close(file_fd);
close(fd);
return 0;
}

使用0_DIRECT方式时,操作系统会绕过文件系统缓存,直接将数据写入磁盘,但是需要满足一些硬件和软件的要求。其中一个限制是,数据必须是物理上连续的,而mmap映射的虚拟内存空间可能不是物理上连续的,因此直接使用0_DIRECT方式落盘会出现bad address错误。

使用posix_memalign申请一块空间来中转数据可以解决这个问题,因为posix_memalign可以保证申请到的内存是物理上连续的,这样就可以直接使用0_DIRECT方式将数据写入磁盘了。

另外,还有一种解决方法是将mmap映射的虚拟内存空间转换为物理内存地址,然后再使用0_DIRECT方式将数据写入磁盘。这种方法需要用到Linux内核提供的一些函数,例如virt_to_phys()函数可以将虚拟地址转换为物理地址。不过这种方法相对复杂,使用posix_memalign申请一块中转内存空间更为简单和可靠。

如果使用0_DIRECT方式将数据直接写入磁盘,那么在写入过程中是无法使用memcpy等内存拷贝函数的,因为这些函数需要访问缓存,而0_DIRECT方式是绕过缓存直接将数据写入磁盘的。

如果需要在写入过程中进行内存拷贝,可以考虑使用一块中转内存空间,将外设数据先拷贝到中转内存空间中,然后再使用0_DIRECT方式将中转内存空间中的数据直接写入磁盘。这样可以避免直接访问外设数据和直接访问磁盘数据之间的冲突,同时也可以使用内存拷贝函数提高数据处理的效率。

另外,也可以考虑使用异步IO来实现高效的文件写入。异步IO可以将文件写入操作提交给操作系统后立即返回,操作系统会在后台进行文件写入操作,写入完成后再通知进程。这样可以避免进程在等待文件写入完成时的阻塞,提高文件写入的效率。但是需要注意异步IO的使用需要考虑到线程安全和错误处理等问题,需要仔细设计和实现。

以下答案由GPT-3.5大模型与博主波罗歌共同编写:
在使用O_DIRECT方式写文件时,需要满足以下条件:

1.文件打开时需要指定O_DIRECT标记,才能使用O_DIRECT方式。

2.写入数据时需要满足对齐要求,也就是写入的数据需要和硬盘的扇区大小对齐。

3.写入的数据也需要是页对齐的(通常是4K),所以在使用mmap映射物理空间到用户空间时需要对齐。如果数据没有对齐,会导致bad address错误。

我们来看一下原来的代码:

ret = write(file_fd, shm, SHR_MEMSIZE);

由于我们没有对齐,导致使用O_DIRECT方式写入数据时出现bad address错误。

下面是一种解决方法:

1.将共享内存的地址按照硬盘扇区大小(通常为512字节)对齐,可以通过以下代码进行对齐:

char aligned_buf;
posix_memalign((void
*)&aligned_buf, 512, SHR_MEMSIZE);
memcpy(aligned_buf, shm, SHR_MEMSIZE);

2.使用aligned_buf作为写入文件的缓冲区:

ret = write(file_fd, aligned_buf, SHR_MEMSIZE);

完整示例代码如下:(注释中有对齐操作的代码)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>

#define SHR_MEMSIZE            0x800000
#define MEM_CLEAR               0x0
#define MEM_RESET               0x1
#define MEM_DEV_FILENAME       "/dev/mychrmem"已黑化的小白 
int main()
{
    int         fd;
    int         file_fd;
    ssize_t        ret;
    char        *shm = NULL;
    char        *aligned_buf;

    fd = open(MEM_DEV_FILENAME, O_RDWR);
    if (fd < 0) {
        printf("open(): %s\n", strerror(errno));
        return -1;
    }

    file_fd = open("/mnt/test",O_WRONLY|O_CREAT|__O_DIRECT,0666);
    if (file_fd < 0)
    {
        perror("open");
        return -1;
    }

    shm = mmap(NULL, SHR_MEMSIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == shm) {
        printf("mmap: %s\n", strerror(errno));
    }

    // 对齐操作
    posix_memalign((void**)&aligned_buf, 512, SHR_MEMSIZE);
    memcpy(aligned_buf, shm, SHR_MEMSIZE);

    printf("Before Write, shm = %s\n", shm); 

    strcpy(shm,"User write to share memory!");

    printf("After write, shm = %s\n", shm);

    while(1)
    {
        ret = write(file_fd, aligned_buf, SHR_MEMSIZE);  // 使用对齐后的缓冲区

        printf("mem addr is 0x%lx\n",(long)shm);
        if(ret==-1)
        {
            perror("write");
        }
        else
        {
            printf("write len 0x%lx\n",ret);
        }
    }

    munmap(shm, SHR_MEMSIZE);
    close(file_fd); 
    close(fd);
    return 0;    
}

如果我的回答解决了您的问题,请采纳!

不知道你这个问题是否已经解决, 如果还没有解决的话:

如果你已经解决了该问题, 非常希望你能够分享一下解决方案, 写成博客, 将相关链接放在评论区, 以帮助更多的人 ^-^

该回答引用ChatGPT:当使用mmap将物理空间映射到用户空间时,为了避免数据在传输过程中被缓存,应该使用O_DIRECT标志。这个标志告诉内核,数据应该直接从应用程序的缓冲区传输到物理设备,而不需要通过中间缓存。

在使用O_DIRECT标志时,要注意以下几点:

1 内存对齐:应用程序缓冲区的起始地址和长度都必须是物理设备块大小的倍数,否则会出现EINVAL错误。

2 文件描述符:使用O_DIRECT标志时,必须使用文件描述符进行IO操作,不能使用标准的文件IO函数(如fread和fwrite)。

3 内存释放:使用O_DIRECT标志时,必须注意在传输完毕后,释放缓冲区的内存。在内核中,缓冲区的大小必须与块设备的大小相同,因此如果释放不完整的缓冲区,可能会造成内存泄漏。

在你的代码中,你使用了O_DIRECT标志来打开文件,但在直接将映射的内存写入文件时,出现了“bad address”错误。这个错误通常是由于内存对齐问题引起的。在使用O_DIRECT标志时,应用程序缓冲区的起始地址和长度必须是块设备大小的倍数。在你的代码中,映射的内存地址可能不是块设备大小的倍数,因此导致了错误。

解决这个问题的方法是,使用posix_memalign函数来分配内存。posix_memalign函数可以保证内存地址和长度是块设备大小的倍数。在你的代码中,你已经使用了posix_memalign来分配内存,但是注释掉了memcpy和使用rev_buf的代码,这是不必要的。你可以使用posix_memalign来分配内存,并直接将映射的内存写入文件,而不需要使用memcpy。

以下是修改后的代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>

#define SHR_MEMSIZE            0x800000
#define MEM_CLEAR               0x0
#define MEM_RESET               0x1
#define MEM_DEV_FILENAME       "/dev/mychrmem"

int main()
{
    int         fd;
    int         file_fd;
    ssize_t        ret;
    char        *shm = NULL;
    char        *rev_buf;

    posix_memalign((void**)&rev_buf, 512, SHR_MEMSIZE);

    fd = open(MEM_DEV_FILENAME, O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    file_fd = open("/mnt/test", O_WRONLY | O_CREAT | O_DIRECT, 0666);
    if (file_fd < 0) {
        perror("open");
        return -1;
    }

    shm = mmap(NULL, SHR_MEMSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == shm) {
        printf("mmap: %s\n", strerror(errno));
    }

    printf("Before Write, shm = %s\n", shm); 

    strcpy(shm, "User write to share memory!");

    printf("After write, shm = %s\n", shm);

    while(1)
    {
        ret = write(file_fd, shm, SHR_MEMSIZE);
        printf("mem addr is 0x%lx\n",(long)shm);
        if(ret==-1)
        {
            perror("write");
        }
        else
        {
            printf("write len 0x%lx\n",ret);
        }
    }

    munmap(shm, SHR_MEMSIZE);
    close(file_fd); 
    close(fd);
    return 0;    
}

修改后的代码将使用正确的标志打开文件描述符,并在while循环中使用O_DIRECT模式直接将共享内存的内容写入文件,而不需要中转数据到另一块缓冲区。