【问题标题】:How to avoid high cpu usage while reading/writing character device?读/写字符设备时如何避免高 CPU 使用率?
【发布时间】:2020-02-13 05:37:03
【问题描述】:

我需要为带有 SRAM 的 PCIe 设备编写一个 linux 内核驱动程序。

对于第一次尝试,我编写了一个驱动程序,以使用字符设备从 PCIe 访问 SRAM。

一切都按预期进行,但有一个问题。 SRAM 很慢 1MB 读/写大约需要 2 秒,这是硬件限制。 CPU 在读/写时 100% 忙。女巫是个问题。我不需要速度,读/写可能很慢,但是为什么要占用这么多CPU?

缓冲区初始化为pci_iomap:

  g_mmio_buffer[0] = pci_iomap(pdev, SRAM_BAR_H, g_mmio_length);

读/写函数如下所示:

static ssize_t dev_read(struct file *fp, char *buf, size_t len, loff_t *off) {
  unsigned long rval;
  size_t copied;

  rval = copy_to_user(buf, g_mmio_buffer[SRAM_BAR] + *off, len);

  if (rval < 0) return -EFAULT;

  copied = len - rval;
  *off += copied;

  return copied;
}

static ssize_t dev_write(struct file *fp, const char *buf, size_t len, loff_t *off) {
  unsigned long rval;
  size_t copied;

  rval = copy_from_user(g_mmio_buffer[SRAM_BAR] + *off, buf, len);

  if (rval < 0) return -EFAULT;

  copied = len - rval;
  *off += copied;

  return copied;
}

问题是 CPU 使用率高怎么办?

我应该重写驱动程序以使用块设备而不是字符吗?

在读取/保存数据时允许 CPU 处理另一个进程?

【问题讨论】:

  • 'SRAM 很慢" -- SRAM 不是慢,而是访问它的方法很慢。从/到用户空间的副本是可能一次将 PCIe 传输大小减少到一个字节。CPU 可能在等待 PCIe 上的每个字节传输时浪费了周期。
  • 第一个 错误:您不应该直接使用 iomem 地址(您必须收到编译器警告,以免弄乱地址空间)。 第2个错误,IO未缓存,每次读写都需要访问内存,并有相应的惩罚(你必须自己缓存数据)。
  • @sawdust 我把它命名错了它是 NVRAM。很明显,CPU 在等待时会浪费周期,但我能做些什么呢?
  • @0andriy 1) 我没有收到任何编译警告。是否有任何示例如何正确使用它? 2) 有没有兑现数据的例子?将数据复制到缓冲区然后复制到 IO 内存对我来说毫无意义。
  • @0andriy 向 NVRAM 读取/写入数据应该是可靠的。我需要确保数据实际上是写在 NVRAM 芯片上的。等待没问题,浪费cpu周期才是。

标签: linux linux-kernel driver linux-device-driver


【解决方案1】:

正如@0andriy 所指出的,您不应该直接访问 iomem。有memcpy_toio()memcpy_fromio()等函数可以在iomem和普通内存之间进行复制,但它们只对内核虚拟地址起作用。

注意: 对于 Linux 内核版本 5.6 及更高版本,应更改下面描述的 get_user_pages_fast()set_page_dirty_lock()put_page() 的使用。稍后将描述所需的更改。

为了在不使用中间数据缓冲区的情况下从用户空间地址复制到 iomem,需要将用户空间内存页面“固定”到物理内存中。这可以使用get_user_pages_fast() 来完成。但是,固定页面可能位于内核中永久映射内存之外的“高内存”(highmem)中。此类页面需要使用kmap_atomic() 在短时间内临时映射到内核虚拟地址空间。 (kmap_atomic() 的使用有规则,highmem 的长期映射还有其他功能。有关详细信息,请查看highmem 文档。)

一旦用户空间页面被映射到内核虚拟地址空间,memcpy_toio()memcpy_fromio() 就可以用于在该页面和 iomem 之间进行复制。

kmap_atomic() 临时映射的页面需要由kunmap_atomic() 取消映射。

get_user_pages_fast() 固定的用户内存页面需要通过调用put_page() 单独取消固定,但如果页面内存已被写入(例如由memcpy_fromio(),则必须首先由@987654337 将其标记为“脏” @在调用put_page()之前。

注意:内核版本 5.6 及以后的更改。

  1. get_user_pages_fast() 的调用应更改为pin_user_pages_fast()
  2. pin_user_pages_fast() 固定的脏页应由 unpin_user_pages_dirty_lock() 取消固定,最后一个参数设置为 true。
  3. pin_user_pages_fast() 固定的干净页面应由 unpin_user_page()unpin_user_pages()unpin_user_pages_dirty_lock() 取消固定,最后一个参数设置为 false。
  4. put_page() 不得用于取消固定由 pin_user_pages_fast() 固定的页面。
  5. 为了代码与早期内核版本兼容,pin_user_pages_fast()unpin_user_page()等的可用性可以通过FOLL_PIN宏是否已经被#include &lt;linux/mm.h&gt;定义。

将所有这些放在一起,可以使用以下函数在用户内存和 iomem 之间进行复制:

#include <linux/kernel.h>
#include <linux/uaccess.h>
#include <linux/mm.h>
#include <linux/highmem.h>
#include <linux/io.h>

/**
 * my_copy_to_user_from_iomem - copy to user memory from MMIO
 * @to:     destination in user memory
 * @from:   source in remapped MMIO
 * @n:      number of bytes to copy
 * Context: process
 *
 * Returns number of uncopied bytes.
 */
long my_copy_to_user_from_iomem(void __user *to, const void __iomem *from,
                unsigned long n)
{
    might_fault();
    if (!access_ok(to, n))
        return n;
    while (n) {
        enum { PAGE_LIST_LEN = 32 };
        struct page *page_list[PAGE_LIST_LEN];
        unsigned long start;
        unsigned int p_off;
        unsigned int part_len;
        int nr_pages;
        int i;

        /* Determine pages to do this iteration. */
        p_off = offset_in_page(to);
        start = (unsigned long)to - p_off;
        nr_pages = min_t(int, PAGE_ALIGN(p_off + n) >> PAGE_SHIFT,
                 PAGE_LIST_LEN);
        /* Lock down (for write) user pages. */
#ifdef FOLL_PIN
        nr_pages = pin_user_pages_fast(start, nr_pages, FOLL_WRITE, page_list);
#else
        nr_pages = get_user_pages_fast(start, nr_pages, FOLL_WRITE, page_list);
#endif
        if (nr_pages <= 0)
            break;

        /* Limit number of bytes to end of locked-down pages. */
        part_len =
            min(n, ((unsigned long)nr_pages << PAGE_SHIFT) - p_off);

        /* Copy from iomem to locked-down user memory pages. */
        for (i = 0; i < nr_pages; i++) {
            struct page *page = page_list[i];
            unsigned char *p_va;
            unsigned int plen;

            plen = min((unsigned int)PAGE_SIZE - p_off, part_len);
            p_va = kmap_atomic(page);
            memcpy_fromio(p_va + p_off, from, plen);
            kunmap_atomic(p_va);
#ifndef FOLL_PIN
            set_page_dirty_lock(page);
            put_page(page);
#endif
            to = (char __user *)to + plen;
            from = (const char __iomem *)from + plen;
            n -= plen;
            part_len -= plen;
            p_off = 0;
        }
#ifdef FOLL_PIN
        unpin_user_pages_dirty_lock(page_list, nr_pages, true);
#endif
    }
    return n;
}
    
/**
 * my_copy_from_user_to_iomem - copy from user memory to MMIO
 * @to:     destination in remapped MMIO
 * @from:   source in user memory
 * @n:      number of bytes to copy
 * Context: process
 *
 * Returns number of uncopied bytes.
 */
long my_copy_from_user_to_iomem(void __iomem *to, const void __user *from,
                unsigned long n)
{
    might_fault();
    if (!access_ok(from, n))
        return n;
    while (n) {
        enum { PAGE_LIST_LEN = 32 };
        struct page *page_list[PAGE_LIST_LEN];
        unsigned long start;
        unsigned int p_off;
        unsigned int part_len;
        int nr_pages;
        int i;

        /* Determine pages to do this iteration. */
        p_off = offset_in_page(from);
        start = (unsigned long)from - p_off;
        nr_pages = min_t(int, PAGE_ALIGN(p_off + n) >> PAGE_SHIFT,
                 PAGE_LIST_LEN);
        /* Lock down (for read) user pages. */
#ifdef FOLL_PIN
        nr_pages = pin_user_pages_fast(start, nr_pages, 0, page_list);
#else
        nr_pages = get_user_pages_fast(start, nr_pages, 0, page_list);
#endif
        if (nr_pages <= 0)
            break;

        /* Limit number of bytes to end of locked-down pages. */
        part_len =
            min(n, ((unsigned long)nr_pages << PAGE_SHIFT) - p_off);

        /* Copy from locked-down user memory pages to iomem. */
        for (i = 0; i < nr_pages; i++) {
            struct page *page = page_list[i];
            unsigned char *p_va;
            unsigned int plen;

            plen = min((unsigned int)PAGE_SIZE - p_off, part_len);
            p_va = kmap_atomic(page);
            memcpy_toio(to, p_va + p_off, plen);
            kunmap_atomic(p_va);
#ifndef FOLL_PIN
            put_page(page);
#endif
            to = (char __iomem *)to + plen;
            from = (const char __user *)from + plen;
            n -= plen;
            part_len -= plen;
            p_off = 0;
        }
#ifdef FOLL_PIN
        unpin_user_pages(page_list, nr_pages);
#endif
    }
    return n;
}

其次,您可能可以通过将 pci_iomap() 替换为 pci_iomap_wc() 将 iomem 映射为“写入组合”来加速内存访问。

第三,在访问慢速内存时避免等待 CPU 的唯一真正方法是不使用 CPU 而使用 DMA 传输。这在很大程度上取决于您的 PCIe 设备的总线主控 DMA 功能(如果有的话)。在 DMA 传输期间仍需要固定用户内存页面(例如,通过 get_user_pages_fast()pin_user_pages_fast() 酌情),但不需要通过 kmap_atomic() 临时映射。

【讨论】:

  • 感谢您的精彩回答! access_ok 函数中缺少第一个参数,需要是 VERIFY_READ 或 VERIFY_WRITE。需要尝试一下。
  • @fazibear 是的,在内核版本 5.0 之前,access_ok 需要初始的 VERIFY_READVERIFY_WRITE 参数。
猜你喜欢
  • 2014-03-23
  • 2015-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-02
相关资源
最近更新 更多