Подтвердить что ты не робот

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

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

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

  • Чтение через 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. */

  • Что я в идеале хочу, это некоторая память, которая:

    • Я могу использовать в пользовательском пространстве (возможно, запрашивать драйвер через вызов ioctl для создания DMA'able memory/buffer?)
    • Я могу получить физический адрес, чтобы перейти на устройство, чтобы все пользовательское пространство должно было выполнять чтение на драйвере
    • метод чтения активирует передачу 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. Я бы посмотрел на бесконечный код, но не мог найти, какой из них самый простой!

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

4b9b3361

Ответ 1

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

Стратегия реализации, которую я использую, заключается в том, что 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);

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

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

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

Ответ 4

Я запутался в том, что нужно реализовать. Я хочу...

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

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

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

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

Ответ 5

first_page_offset = udata & PAGE_MASK; 

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

first_page_offset = udata & ~PAGE_MASK;

или

first_page_offset = udata & (PAGE_SIZE - 1)