Драйвер устройства ядра Linux для DMA с устройства в память пространства пользователя - PullRequest
28 голосов
/ 04 апреля 2011

Я хочу как можно быстрее получить данные с аппаратного устройства PCIe с поддержкой DMA в пользовательское пространство.

В: Как мне объединить «прямой ввод-вывод в пространство пользователя с / и / через передачу DMA»

  1. Читая через LDD3, мне кажется, что мне нужно выполнить несколько разных типов операций ввода-вывода !?

    dma_alloc_coherent дает мне физический адрес, который я могу передать аппаратному устройству. Но нужно будет настроить get_user_pages и выполнить вызов типа copy_to_user, когда передача завершится. Это кажется пустой тратой, когда устройство запрашивает DMA в памяти ядра (действует как буфер), а затем снова передает его в пространство пользователя. LDD3 p453: /* Only now is it safe to access the buffer, copy to user, etc. */

  2. В идеале мне нужна память, которая:

    • Я могу использовать в пространстве пользователя (может быть, запросить драйвер через вызов ioctl для создания памяти / буфера DMA'able?)
    • Я могу получить физический адрес для передачи на устройство, чтобы все пользовательское пространство могло выполнять чтение в драйвере
    • метод чтения активирует передачу DMA, блокирует ожидание полного прерывания DMA и освобождает после чтения пространство пользователя (пространство пользователя теперь безопасно для использования / чтения памяти).

Нужны ли мне одностраничные потоковые сопоставления, установочные сопоставления и буферы пространства пользователя, сопоставленные с get_user_pages dma_map_page?

Мой код до сих пор устанавливает get_user_pages по данному адресу из пространства пользователя (я называю это частью прямого ввода-вывода). Затем dma_map_page со страницей из get_user_pages. Я даю устройству возвращаемое значение от dma_map_page в качестве физического адреса передачи DMA.

Я использую некоторые модули ядра для справки: drivers_scsi_st.c и drivers-net-sh_eth.c. Я бы посмотрел на код Infiniband, но не могу найти, какой из них самый простой!

Заранее большое спасибо.

Ответы [ 6 ]

15 голосов
/ 04 апреля 2011

На самом деле я сейчас работаю над тем же самым и иду по маршруту ioctl().Общая идея состоит в том, чтобы пользовательское пространство выделяло буфер, который будет использоваться для передачи DMA, и ioctl() будет использоваться для передачи размера и адреса этого буфера драйверу устройства.Затем драйвер будет использовать списки сбора рассеяния вместе с потоковым API DMA для передачи данных непосредственно на устройство и из него и в буфер пространства пользователя.

Стратегия реализации, которую я использую, заключается в том, что ioctl() вдрайвер зацикливается на том, что DMA является буфером пользовательского пространства кусками по 256 Кб (что является аппаратным пределом для количества записей разброса / сбора, которые он может обработать).Он изолирован внутри функции, которая блокирует до завершения каждой передачи (см. Ниже).Когда все байты передаются или функция инкрементной передачи возвращает ошибку, ioctl() выходит и возвращается в пользовательское пространство

Псевдокод для ioctl()

/*serialize all DMA transfers to/from the device*/
if (mutex_lock_interruptible( &device_ptr->mtx ) )
    return -EINTR;

chunk_data = (unsigned long) user_space_addr;
while( *transferred < total_bytes && !ret ) {
    chunk_bytes = total_bytes - *transferred;
    if (chunk_bytes > HW_DMA_MAX)
        chunk_bytes = HW_DMA_MAX; /* 256kb limit imposed by my device */
    ret = transfer_chunk(device_ptr, chunk_data, chunk_bytes, transferred);
    chunk_data += chunk_bytes;
    chunk_offset += chunk_bytes;
}

mutex_unlock(&device_ptr->mtx);

Псевдокод для функции инкрементной передачи:

/*Assuming the userspace pointer is passed as an unsigned long, */
/*calculate the first,last, and number of pages being transferred via*/

first_page = (udata & PAGE_MASK) >> PAGE_SHIFT;
last_page = ((udata+nbytes-1) & PAGE_MASK) >> PAGE_SHIFT;
first_page_offset = udata & PAGE_MASK;
npages = last_page - first_page + 1;

/* Ensure that all userspace pages are locked in memory for the */
/* duration of the DMA transfer */

down_read(&current->mm->mmap_sem);
ret = get_user_pages(current,
                     current->mm,
                     udata,
                     npages,
                     is_writing_to_userspace,
                     0,
                     &pages_array,
                     NULL);
up_read(&current->mm->mmap_sem);

/* Map a scatter-gather list to point at the userspace pages */

/*first*/
sg_set_page(&sglist[0], pages_array[0], PAGE_SIZE - fp_offset, fp_offset);

/*middle*/
for(i=1; i < npages-1; i++)
    sg_set_page(&sglist[i], pages_array[i], PAGE_SIZE, 0);

/*last*/
if (npages > 1) {
    sg_set_page(&sglist[npages-1], pages_array[npages-1],
        nbytes - (PAGE_SIZE - fp_offset) - ((npages-2)*PAGE_SIZE), 0);
}

/* Do the hardware specific thing to give it the scatter-gather list
   and tell it to start the DMA transfer */

/* Wait for the DMA transfer to complete */
ret = wait_event_interruptible_timeout( &device_ptr->dma_wait, 
         &device_ptr->flag_dma_done, HZ*2 );

if (ret == 0)
    /* DMA operation timed out */
else if (ret == -ERESTARTSYS )
    /* DMA operation interrupted by signal */
else {
    /* DMA success */
    *transferred += nbytes;
    return 0;
}

Обработчик прерываний исключительно короткий:

/* Do hardware specific thing to make the device happy */

/* Wake the thread waiting for this DMA operation to complete */
device_ptr->flag_dma_done = 1;
wake_up_interruptible(device_ptr->dma_wait);

Обратите внимание, что это всего лишь общий подход, я работал над этим драйвером в течение последних нескольких недель иЯ еще не проверил это ... Так что, пожалуйста, не рассматривайте этот псевдокод как евангелие и обязательно перепроверьте всю логику и параметры; -).

12 голосов
/ 21 мая 2011

У вас в основном правильная идея: в 2.1 вы можете просто выделить пространство пользователя для любой старой памяти.Вы действительно хотите, чтобы он был выровнен по странице, поэтому posix_memalign() - удобный API для использования.

Затем пусть пользовательское пространство передаст виртуальный адрес пользовательского пространства и размер этого буфера каким-либо образом;ioctl () - хороший быстрый и грязный способ сделать это.В ядре выделите буферный массив подходящего размера из struct page* - user_buf_size/PAGE_SIZE записей - и используйте get_user_pages(), чтобы получить список struct page * для буфера пользовательского пространства.

Как только вы это получитеВы можете выделить массив struct scatterlist того же размера, что и массив страниц, и просмотреть список страниц, выполнив sg_set_page().После того, как список sg настроен, вы делаете dma_map_sg() в массиве scatterlist, а затем вы можете получить sg_dma_address и sg_dma_len для каждой записи в scatterlist (обратите внимание, вы должны использовать возвращаемое значение dma_map_sg()потому что у вас может быть меньше отображаемых записей, потому что все может быть объединено кодом преобразования DMA.

Это дает вам все адреса шин для передачи на ваше устройство, а затем вы можете запустить DMA и ждатьэто как хочешь.Схема read (), которую вы используете, вероятно, подойдет.

Вы можете обратиться к драйверу / infiniband / core / umem.c, в частности ib_umem_get(), для некоторого кода, который создает это отображение, хотя в общем случаето, что этот код должен иметь дело, может сделать его немного запутанным.

В качестве альтернативы, если ваше устройство не обрабатывает списки разброса / сбора слишком хорошо и вам нужна непрерывная память, вы можете использовать get_free_pages() для выделенияфизически смежный буфер и используйте dma_map_page() для этого.Чтобы предоставить пользователям доступ к этой памяти, вашему драйверу нужно просто реализовать метод mmap вместо ioctl, как описано выше.

6 голосов
/ 13 мая 2013

В какой-то момент я хотел позволить приложению пользовательского пространства выделять буферы DMA и отображать его в пространстве пользователя и получать физический адрес, чтобы иметь возможность управлять моим устройством и выполнять транзакции DMA (мастеринг шины) полностью от пользователя.пространство, полностью минуя ядро ​​Linux.Я использовал немного другой подход, хотя.Сначала я начал с минимального модуля ядра, который инициализировал / проверял устройство PCIe и создавал символьное устройство.Затем этот драйвер позволял приложению пользовательского пространства выполнять две вещи:

  1. Отображение панели ввода-вывода устройства PCIe в пространство пользователя с помощью функции remap_pfn_range().
  2. Выделение и свободный DMAбуферы, сопоставьте их с пользовательским пространством и передайте физический адрес шины приложению пользовательского пространства.

По сути, это сводится к пользовательской реализации вызова mmap() (хотя file_operations).Один для панели ввода / вывода прост:

struct vm_operations_struct a2gx_bar_vma_ops = {
};

static int a2gx_cdev_mmap_bar2(struct file *filp, struct vm_area_struct *vma)
{
    struct a2gx_dev *dev;
    size_t size;

    size = vma->vm_end - vma->vm_start;
    if (size != 134217728)
        return -EIO;

    dev = filp->private_data;
    vma->vm_ops = &a2gx_bar_vma_ops;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_private_data = dev;

    if (remap_pfn_range(vma, vma->vm_start,
                        vmalloc_to_pfn(dev->bar2),
                        size, vma->vm_page_prot))
    {
        return -EAGAIN;
    }

    return 0;
}

А другой, который распределяет буферы DMA с использованием pci_alloc_consistent(), немного сложнее:

static void a2gx_dma_vma_close(struct vm_area_struct *vma)
{
    struct a2gx_dma_buf *buf;
    struct a2gx_dev *dev;

    buf = vma->vm_private_data;
    dev = buf->priv_data;

    pci_free_consistent(dev->pci_dev, buf->size, buf->cpu_addr, buf->dma_addr);
    buf->cpu_addr = NULL; /* Mark this buffer data structure as unused/free */
}

struct vm_operations_struct a2gx_dma_vma_ops = {
    .close = a2gx_dma_vma_close
};

static int a2gx_cdev_mmap_dma(struct file *filp, struct vm_area_struct *vma)
{
    struct a2gx_dev *dev;
    struct a2gx_dma_buf *buf;
    size_t size;
    unsigned int i;

    /* Obtain a pointer to our device structure and calculate the size
       of the requested DMA buffer */
    dev = filp->private_data;
    size = vma->vm_end - vma->vm_start;

    if (size < sizeof(unsigned long))
        return -EINVAL; /* Something fishy is happening */

    /* Find a structure where we can store extra information about this
       buffer to be able to release it later. */
    for (i = 0; i < A2GX_DMA_BUF_MAX; ++i) {
        buf = &dev->dma_buf[i];
        if (buf->cpu_addr == NULL)
            break;
    }

    if (buf->cpu_addr != NULL)
        return -ENOBUFS; /* Oops, hit the limit of allowed number of
                            allocated buffers. Change A2GX_DMA_BUF_MAX and
                            recompile? */

    /* Allocate consistent memory that can be used for DMA transactions */
    buf->cpu_addr = pci_alloc_consistent(dev->pci_dev, size, &buf->dma_addr);
    if (buf->cpu_addr == NULL)
        return -ENOMEM; /* Out of juice */

    /* There is no way to pass extra information to the user. And I am too lazy
       to implement this mmap() call using ioctl(). So we simply tell the user
       the bus address of this buffer by copying it to the allocated buffer
       itself. Hacks, hacks everywhere. */
    memcpy(buf->cpu_addr, &buf->dma_addr, sizeof(buf->dma_addr));

    buf->size = size;
    buf->priv_data = dev;
    vma->vm_ops = &a2gx_dma_vma_ops;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_private_data = buf;

    /*
     * Map this DMA buffer into user space.
     */
    if (remap_pfn_range(vma, vma->vm_start,
                        vmalloc_to_pfn(buf->cpu_addr),
                        size, vma->vm_page_prot))
    {
        /* Out of luck, rollback... */
        pci_free_consistent(dev->pci_dev, buf->size, buf->cpu_addr,
                            buf->dma_addr);
        buf->cpu_addr = NULL;
        return -EAGAIN;
    }

    return 0; /* All good! */
}

Как только они будут на местеПриложение пользовательского пространства может в значительной степени делать все - управлять устройством, читая / записывая из / в регистры ввода-вывода, выделяя и освобождая буферы DMA произвольного размера, и заставляя устройство выполнять транзакции DMA.Единственная недостающая часть - обработка прерываний.Я проводил опрос в пользовательском пространстве, сжигал свой процессор и отключал прерывания.

Надеюсь, это поможет.Удачи!

1 голос
/ 17 апреля 2014

Я запутался в направлении реализации.Я хочу ...

Рассмотрим приложение при разработке драйвера.
Какова природа перемещения данных, частоты, размера и что еще может происходить в системе?

Достаточно ли традиционного API для чтения / записи?Прямое сопоставление устройства с пользовательским пространством в порядке?Желательна ли отражающая (полукогерентная) разделяемая память?

Ручное манипулирование данными (чтение / запись) является довольно хорошим вариантом, если данные хорошо понимаются.Использование виртуальной машины общего назначения и чтение / запись может быть достаточным для встроенного копирования.Прямое отображение не кэшируемых доступов к периферийным устройствам удобно, но может быть неуклюжим.Если доступ представляет собой относительно редкое перемещение больших блоков, может иметь смысл использовать обычную память, иметь вывод привода, переводить адреса, DMA и освобождать страницы.В качестве оптимизации страницы (возможно огромные) могут быть предварительно закреплены и переведены;Затем привод может распознать подготовленную память и избежать сложностей динамического перевода.Если имеется много небольших операций ввода-вывода, имеет смысл использовать привод асинхронно.Если элегантность важна, флаг грязной страницы виртуальной машины может использоваться для автоматической идентификации того, что необходимо переместить, и вызов (meta_sync ()) может использоваться для очистки страниц.Возможно, смесь вышеперечисленных работ ...

Слишком часто люди не обращают внимания на большую проблему, прежде чем копаться в деталях.Часто простейших решений достаточно.Небольшое усилие по построению поведенческой модели может помочь определить, какой API предпочтительнее.

0 голосов
/ 14 мая 2018

Стоит отметить, что драйвер с поддержкой Scatter-Gather DMA и распределением памяти в пользовательском пространстве является наиболее эффективным и обладает самой высокой производительностью.Однако в случае, если нам не нужна высокая производительность или мы хотим разработать драйвер в некоторых упрощенных условиях, мы можем использовать некоторые приемы.

Откажитесь от дизайна без копирования.Стоит учитывать, когда пропускная способность данных не слишком велика.В таком проекте данные могут быть скопированы пользователю copy_to_user(user_buffer, kernel_dma_buffer, count); user_buffer может быть, например, аргументом буфера в реализации системного вызова символьного устройства read ().Нам все еще нужно позаботиться о распределении kernel_dma_buffer.Это может быть, например, память, полученная из вызова dma_alloc_coherent().

Другая хитрость - ограничить системную память во время загрузки, а затем использовать ее как огромный непрерывный буфер DMA.Это особенно полезно при разработке драйверов и контроллеров FPGA DMA и не рекомендуется в производственных средах.Допустим, ПК имеет 32 ГБ оперативной памяти.Если мы добавим mem=20GB в список параметров загрузки ядра, мы сможем использовать 12 ГБ в качестве огромного непрерывного буфера dma.Чтобы отобразить эту память в пользовательское пространство, просто реализуйте mmap () как

remap_pfn_range(vma,
    vma->vm_start,
    (0x500000000 >> PAGE_SHIFT) + vma->vm_pgoff, 
    vma->vm_end - vma->vm_start,
    vma->vm_page_prot)

Конечно, эти 12 ГБ полностью опущены ОС и могут использоваться только процессом, который отобразил его в свое адресное пространство.Мы можем попытаться избежать этого, используя Contiguous Memory Allocator (CMA).

Опять вышеперечисленные приемы не заменят полный Scatter-Gather, драйвер DMA нулевой копии, но полезны во время разработки или на некоторых менее производительных платформах.

0 голосов
/ 16 февраля 2016
first_page_offset = udata & PAGE_MASK; 

Кажется, это неправильно. Должно быть либо:

first_page_offset = udata & ~PAGE_MASK;

или

first_page_offset = udata & (PAGE_SIZE - 1)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...