Linux 中的直接内存访问

Posted

技术标签:

【中文标题】Linux 中的直接内存访问【英文标题】:Direct Memory Access in Linux 【发布时间】:2010-10-13 11:08:21 【问题描述】:

我正在尝试直接访问嵌入式 Linux 项目的物理内存,但我不确定如何最好地指定内存供我使用。

如果我定期启动我的设备并访问 /dev/mem,我可以轻松地读写几乎任何我想要的地方。但是,在这里,我正在访问可以轻松分配给任何进程的内存;我不想这样做

我的 /dev/mem 代码是(删除所有错误检查等):

mem_fd = open("/dev/mem", O_RDWR));
mem_p = malloc(SIZE + (PAGE_SIZE - 1));
if ((unsigned long) mem_p % PAGE_SIZE) 
    mem_p += PAGE_SIZE - ((unsigned long) mem_p % PAGE_SIZE);

mem_p = (unsigned char *) mmap(mem_p, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, mem_fd, BASE_ADDRESS);

这很有效。但是,我想使用其他人不会触及的内存。我尝试通过使用 mem=XXXm 启动来限制内核看到的内存量,然后将 BASE_ADDRESS 设置为高于该值(但低于物理内存),但它似乎并没有始终如一地访问相同的内存。

根据我在网上看到的情况,我怀疑我可能需要一个使用 ioremap() 或 remap_pfn_range() (或两者都使用???)的内核模块(没关系),但我完全不知道如何;谁能帮忙?

编辑: 我想要的是一种始终访问相同物理内存(例如,价值 1.5MB)的方法,并将该内存放在一边,以便内核不会将其分配给任何其他进程。

我正在尝试重现我们在其他操作系统中拥有的系统(没有内存管理),我可以通过链接器在内存中分配一个空间,并使用类似的东西访问它

*(unsigned char *)0x12345678

编辑2: 我想我应该提供更多细节。此内存空间将用于 RAM 缓冲区,用于嵌入式应用程序的高性能日志记录解决方案。在我们拥有的系统中,在软重启期间没有任何东西可以清除或扰乱物理内存。因此,如果我向物理地址 X 写入位并重新启动系统,在重新启动后仍会设置相同的位。这已经在运行 VxWorks 的完全相同的硬件上进行了测试(该逻辑在不同平台上的 Nucleus RTOS 和 OS20 中也能很好地工作,FWIW)。我的想法是通过直接寻址物理内存在 Linux 中尝试同样的事情。因此,每次启动时我都必须获得相同的地址。

我可能应该澄清一下,这是针对内核 2.6.12 和更高版本的。

编辑3: 这是我的代码,首先是内核模块,然后是用户空间应用程序。

要使用它,我使用 mem=95m 启动,然后是 insmod foo-module.ko,然后是 mknod mknod /dev/foo c 32 0,然后运行 ​​foo-user,它就死了。在 gdb 下运行表明它在分配时死亡,尽管在 gdb 中,我无法取消引用从 mmap 获得的地址(尽管 printf 可以)

foo-module.c

#include <linux/module.h>
#include <linux/config.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <asm/io.h>

#define VERSION_STR "1.0.0"
#define FOO_BUFFER_SIZE (1u*1024u*1024u)
#define FOO_BUFFER_OFFSET (95u*1024u*1024u)
#define FOO_MAJOR 32
#define FOO_NAME "foo"

static const char *foo_version = "@(#) foo Support version " VERSION_STR " " __DATE__ " " __TIME__;

static void    *pt = NULL;

static int      foo_release(struct inode *inode, struct file *file);
static int      foo_open(struct inode *inode, struct file *file);
static int      foo_mmap(struct file *filp, struct vm_area_struct *vma);

struct file_operations foo_fops = 
    .owner = THIS_MODULE,
    .llseek = NULL,
    .read = NULL,
    .write = NULL,
    .readdir = NULL,
    .poll = NULL,
    .ioctl = NULL,
    .mmap = foo_mmap,
    .open = foo_open,
    .flush = NULL,
    .release = foo_release,
    .fsync = NULL,
    .fasync = NULL,
    .lock = NULL,
    .readv = NULL,
    .writev = NULL,
;

static int __init foo_init(void)

    int             i;
    printk(KERN_NOTICE "Loading foo support module\n");
    printk(KERN_INFO "Version %s\n", foo_version);
    printk(KERN_INFO "Preparing device /dev/foo\n");
    i = register_chrdev(FOO_MAJOR, FOO_NAME, &foo_fops);
    if (i != 0) 
        return -EIO;
        printk(KERN_ERR "Device couldn't be registered!");
    
    printk(KERN_NOTICE "Device ready.\n");
    printk(KERN_NOTICE "Make sure to run mknod /dev/foo c %d 0\n", FOO_MAJOR);
    printk(KERN_INFO "Allocating memory\n");
    pt = ioremap(FOO_BUFFER_OFFSET, FOO_BUFFER_SIZE);
    if (pt == NULL) 
        printk(KERN_ERR "Unable to remap memory\n");
        return 1;
    
    printk(KERN_INFO "ioremap returned %p\n", pt);
    return 0;

static void __exit foo_exit(void)

    printk(KERN_NOTICE "Unloading foo support module\n");
    unregister_chrdev(FOO_MAJOR, FOO_NAME);
    if (pt != NULL) 
        printk(KERN_INFO "Unmapping memory at %p\n", pt);
        iounmap(pt);
     else 
        printk(KERN_WARNING "No memory to unmap!\n");
    
    return;

static int foo_open(struct inode *inode, struct file *file)

    printk("foo_open\n");
    return 0;

static int foo_release(struct inode *inode, struct file *file)

    printk("foo_release\n");
    return 0;

static int foo_mmap(struct file *filp, struct vm_area_struct *vma)

    int             ret;
    if (pt == NULL) 
        printk(KERN_ERR "Memory not mapped!\n");
        return -EAGAIN;
    
    if ((vma->vm_end - vma->vm_start) != FOO_BUFFER_SIZE) 
        printk(KERN_ERR "Error: sizes don't match (buffer size = %d, requested size = %lu)\n", FOO_BUFFER_SIZE, vma->vm_end - vma->vm_start);
        return -EAGAIN;
    
    ret = remap_pfn_range(vma, vma->vm_start, (unsigned long) pt, vma->vm_end - vma->vm_start, PAGE_SHARED);
    if (ret != 0) 
        printk(KERN_ERR "Error in calling remap_pfn_range: returned %d\n", ret);
        return -EAGAIN;
    
    return 0;

module_init(foo_init);
module_exit(foo_exit);
MODULE_AUTHOR("Mike Miller");
MODULE_LICENSE("NONE");
MODULE_VERSION(VERSION_STR);
MODULE_DESCRIPTION("Provides support for foo to access direct memory");

foo-user.c

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

int main(void)

    int             fd;
    char           *mptr;
    fd = open("/dev/foo", O_RDWR | O_SYNC);
    if (fd == -1) 
        printf("open error...\n");
        return 1;
    
    mptr = mmap(0, 1 * 1024 * 1024, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 4096);
    printf("On start, mptr points to 0x%lX.\n",(unsigned long) mptr);
    printf("mptr points to 0x%lX. *mptr = 0x%X\n", (unsigned long) mptr, *mptr);
    mptr[0] = 'a';
    mptr[1] = 'b';
    printf("mptr points to 0x%lX. *mptr = 0x%X\n", (unsigned long) mptr, *mptr);
    close(fd);
    return 0;

【问题讨论】:

澄清一下,你想(在一个模块中)将一个地址空间返回给通过vmalloc()而不是kmalloc()获取的用户空间,对吗?您实际需要多少内存? 这可能是使用 kmalloc() 最简单的方法,您要做的是将 1.5 MB 的内核空间分开并将其呈现给用户空间。如果这就是你想要做的,我会在一些内核内部刷新自己并尝试回答。 注意,使用 vmalloc() 执行此操作可能是一项非常讨厌的任务。您实际需要映射的数量会影响答案,因此您确定它是 1.5 MB 还是更少? 是的,1.5 MB。也许 2;不止于此。 编辑了我关于 remap_pfn_range 函数的答案 【参考方案1】:

我想你可以找到很多关于 kmalloc + mmap 部分的文档。 但是,我不确定您是否可以以连续的方式对这么多内存进行 kmalloc,并且始终将其放在同一个地方。当然,如果一切都是一样的,那么你可能会得到一个不变的地址。但是,每次更改内核代码都会得到不同的地址,所以我不会使用 kmalloc 解决方案。

我认为你应该在启动时保留一些内存,即保留一些物理内存,这样内核就不会触及。然后你可以ioremap这个内存,这会给你 内核虚拟地址,然后你可以映射它并编写一个不错的设备驱动程序。

这会将我们带回到 PDF 格式的linux device drivers。看看第 15 章,它在第 443 页描述了这种技术

编辑:ioremap 和 mmap。 我认为这可能更容易分两步调试:首先获取 ioremap 对,并使用字符设备操作(即读/写)对其进行测试。一旦你知道你可以安全地使用读/写访问整个 ioremapped 内存,然后你尝试 mmap 整个 ioremapped 范围。

如果你遇到麻烦可能会发布另一个关于映射的问题

编辑:remap_pfn_range ioremap 返回一个 virtual_adress,您必须将其转换为 remap_pfn_ranges 的 pfn。 现在,我不明白 pfn(页框号)是什么,但我认为您可以接听电话

virt_to_phys(pt) >> PAGE_SHIFT

这可能不是正确的方法(tm),但你应该尝试一下

您还应该检查 FOO_MEM_OFFSET 是您的 RAM 块的物理地址。也就是说,在 mmu 发生任何事情之前,您的内存在处理器的内存映射中为 0。

【讨论】:

当你说“我认为你应该在启动时保留一些内存,即保留一些物理内存以便内核不会触及。”你的意思是用 mem=XXXm 启动,其中 XXX小于实际地址?这就是我最初的想法。 [cont] 我看到一些代码使用 ioremap();我会试试这个。所以要确认一下:使用 mem=XXX、ioremap(XXX+1) 启动,然后将其转换为用户空间地址的最佳方法是什么?1 嗯。我试过这个,并按如下方式实现 mmap(所有错误检查已删除): static int foo_mmap(struct file *filp, struct vm_area_struct *vma) remap_pfn_range(vma, vma->vm_start, (unsigned long) pt, vma-> vm_end - vma->vm_start, PAGE_SHARED); 当我执行 mmap 时,我得到了似乎是一个有效地址,但任何写入它的尝试都会导致:arch/mips/kernel/unaligned.c 中的内核未对齐指令访问:: do_ade,第 544 行 [#1]: mmap 有点棘手,您最终应该编辑您的问题并发布您的代码,或者发布一个新问题。【参考方案2】:

很抱歉回答,但没有完全回答,我注意到您已经编辑了问题。请注意,当您编辑问题时,SO 不会通知我们。我在这里给出一个通用的答案,当您更新问题时请发表评论,然后我将编辑我的答案。

是的,您将需要编写一个模块。归根结底是使用kmalloc()(在内核空间中分配区域)或vmalloc()(在用户空间中分配区域)。

暴露先验很容易,如果需要根据需要描述的那种界面,暴露后者可能会很麻烦。您注意到 1.5 MB 是您实际需要保留多少的粗略估计,那是铁包的吗?即您是否愿意从内核空间中获取它?您能否充分处理来自用户空间(甚至磁盘睡眠)的 ENOMEM 或 EIO? IOW,这个地区发生了什么?

另外,并发会成为这个问题吗?如果是这样,你会使用 futex 吗?如果任何一个的答案都是“是”(尤其是后者),那么您可能不得不硬着头皮选择vmalloc()(或者冒着内核腐烂的风险)。此外,如果您甚至在考虑 char 设备的 ioctl() 接口(特别是对于一些临时锁定想法),您真的想使用 vmalloc()

另外,你读过this吗?另外,我们甚至没有谈到 grsec / selinux 会怎么想(如果正在使用)。

【讨论】:

这是一个嵌入式系统;我不担心selinux。关于你的 Q 的其余部分,我现在添加更多细节。【参考方案3】:

/dev/mem 对于简单的寄存器窥视和戳是可以的,但是一旦你进入中断和 DMA 领域,你真的应该编写一个内核模式驱动程序。您为以前的无内存管理操作系统所做的事情并不能很好地移植到像 Linux 这样的通用操作系统上。

您已经考虑过 DMA 缓冲区分配问题。现在,考虑一下来自您的设备的“DMA 完成”中断。您将如何安装中断服务程序?

此外,/dev/mem 通常对非 root 用户是锁定的,因此对于一般用途来说不是很实用。当然,您可以对其进行 chmod,但是您在系统中打开了一个很大的安全漏洞。

如果您试图保持操作系统之间的驱动程序代码库相似,您应该考虑将其重构为单独的用户和内核模式层,中间使用类似 IOCTL 的接口。如果您将用户模式部分编写为 C 代码的通用库,那么它应该很容易在 Linux 和其他操作系统之间移植。特定于操作系统的部分是内核模式代码。 (我们为驱动程序使用这种方法。)

您似乎已经得出结论,是时候编写内核驱动程序了,所以您走在正确的轨道上。我能补充的唯一建议是从头到尾阅读这些书。

Linux Device Drivers

Understanding the Linux Kernel

(请记住,这些书大约是 2005 年出版的,所以信息有点过时了。)

【讨论】:

【参考方案4】:

到目前为止,我还不是这些问题的专家,所以这将是一个问题,而不是答案。有什么理由不能只制作一个小的 ram 磁盘分区并将其仅用于您的应用程序?这不会让您保证访问相同的内存块吗?我不确定是否会有任何 I/O 性能问题,或者与这样做相关的额外开销。这也假设你可以告诉内核在内存中划分一个特定的地址范围,不确定这是否可能。

我为 newb 的问题道歉,但我发现你的问题很有趣,并且很好奇 ram 磁盘是否可以以这种方式使用。

【讨论】:

并发可能是一个问题,尤其是在使用不支持写入障碍的 DM 时。这个问题(尽管进行了编辑)确实很模棱两可,缺乏他们将如何实际使用具有那种界面的那种区域。 另外,(参考我的第一条评论)写入缓存可能是个问题,尤其是在排序方面。【参考方案5】:

你看过'memmap'内核参数吗?在 i386 和 X64_64 上,您可以使用 memmap 参数来定义内核将如何处理非常特定的内存块(请参阅Linux kernel parameter 文档)。在您的情况下,您希望将内存标记为“保留”,以便 Linux 根本不会触及它。然后,您可以编写代码来使用该绝对地址和大小(如果您超出该空间,您将有祸了)。

【讨论】:

我实际上确实使用了 memmap [或者只是 mem=XXm@YY,我的内核支持在中间保留一个块。] 悬而未决的问题是如何直接访问内存。

以上是关于Linux 中的直接内存访问的主要内容,如果未能解决你的问题,请参考以下文章

Linux 读写memory操作,devmem直接访问物理内存地址

三指针

Linux内存管理——段页式访问

linux性能优化linux内存如何工作

Linux DMA访问的一致性

Linux进程通信 | 共享内存