it-swarm.com.ru

Драйвер устройства ядра Linux для DMA с устройства в пользовательскую память

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

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

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

    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_pagesdma_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, но не могу найти, какой из них является самым основным!

Спасибо заранее.

27
Ian Vaughan

На самом деле я сейчас работаю над тем же самым и иду по маршруту 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);

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

14
Rakis

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

Затем пусть userpace передаст виртуальный адрес userpace и размер этого буфера каким-либо образом; 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 (), которая у вас есть, вероятно, в порядке.

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

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

12
Roland

В какой-то момент я хотел позволить приложению пользовательского пространства выделять буферы 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 транзакции. Единственная недостающая часть - обработка прерываний. Я проводил опрос в пользовательском пространстве, сжигал свой процессор, и прерывания были отключены.

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

6
user405725

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

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

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

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

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

1
fbp
first_page_offset = udata & PAGE_MASK; 

Это кажется неправильным. Это должно быть либо:

first_page_offset = udata & ~PAGE_MASK;

или же 

first_page_offset = udata & (PAGE_SIZE - 1)
0
Suman

Стоит отметить, что драйвер с поддержкой 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 ГБ полностью опущены ОС и могут использоваться только процессом, который отобразил их в свое адресное пространство. Мы можем попытаться избежать этого, используя непрерывный распределитель памяти (CMA).

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

0
SlawekS