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

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

    • I can use in user-space (Maybe request driver via a ioctl call to create 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. Я бы посмотрел на код Infiniband, но не могу найти, какой из них самый простой!

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


person Ian Vaughan    schedule 04.04.2011    source источник
comment
Прошло много времени с тех пор, как я разработал последний драйвер под Linux, но я всегда зарезервировал используемую память DMA, которая затем отображалась на пользовательских страницах. Раньше не все адреса могли использоваться для передачи DMA. В наши дни это может быть не так. Еще одна подсказка - посмотреть видео о драйверах для Linux, особенно о тех, которые связаны с чипами BT848 / BT878. Надеюсь, ты найдешь там что-нибудь полезное.   -  person jdehaan    schedule 04.04.2011
comment
Я знаю, что этот вопрос похож: stackoverflow.com/q/3333959/119790   -  person Ian Vaughan    schedule 04.04.2011
comment
Работаю над аналогичной проблемой. Мне было бы любопытно узнать, по какому маршруту вы в итоге пошли.   -  person mksuth    schedule 30.06.2011
comment
если мы используем постоянную память (не SG) с dma_alloc_coherent (), которая возвращает непрерывную память, то не можем ли мы просто вызвать mmap в пользовательском пространстве?   -  person ransh    schedule 19.12.2016
comment
@ransh: да, если ваш драйвер реализует операцию с файлом mmap.   -  person Woodrow Barlow    schedule 03.04.2017


Ответы (6)


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

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

person Rakis    schedule 04.04.2011
comment
Хотя решение напрямую мне не помогло, чтение было интересным. В качестве единственного ответа я приму это по умолчанию. Но за хорошо написанный ответ следует с большим уважением и признательностью. - person Ian Vaughan; 12.04.2011
comment
Я знаю, что эта ветка была начата 5 лет назад, но надеюсь, что мой вопрос не будет проигнорирован. Кажется, что, за исключением ответа user405725, нужно дождаться завершения DMA. Если этого не сделать, это будет означать, что, возможно, пользовательские страницы будут излишне закреплены, если что-то пойдет не так. Я прав? Кажется, это очень синхронная операция. Верный? - person Andrew Falanga; 08.07.2015
comment
Верный. Я полагаю, можно было бы выполнять DMA асинхронно, но приложение пользовательского пространства должно было бы предоставить какое-то уведомление, чтобы они знали, когда память может быть безопасно использована / перезаписана. Однако синхронный режим реализовать было легко, поэтому я выбрал именно этот путь. Что касается того, что что-то идет не так, вы как бы облажались, если они это сделают, и я не уверен, что это вообще обнаружимо. Я не смотрел на это уже 5 лет, так что я определенно устарел ;-) - person Rakis; 08.07.2015
comment
для будущих читателей: обратите внимание, что ioctls обычно значительно менее производительны, чем dma (но обычно это нормально для большинства случаев использования). - person Woodrow Barlow; 23.03.2017
comment
Вам также необходимо создать потоковое отображение DMA, чтобы sg мог разговаривать с (конкретным) устройством. dma_map_sg(). В этом весь смысл построения списка разброса. - person Brad; 07.03.2018
comment
Мне удалось использовать этот ответ для создания модуля ядра DMA. Это не совсем то же самое, что заданный вопрос, но, возможно, исходный код поможет людям: github.com / esophagus-now / mpsoc_axidma - person Marco Merlini; 25.01.2020

По сути, у вас есть правильная идея: в 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 (), которая у вас есть, вероятно, подходит.

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

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

person Roland    schedule 21.05.2011
comment
если мы используем постоянную память (не SG) с dma_alloc_coherent (), которая возвращает непрерывную память, то не можем ли мы просто вызвать mmap в пользовательском пространстве? - person ransh; 19.12.2016

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

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

person Community    schedule 13.05.2013
comment
Ваша ссылка выглядит как 404? - person Chiggs; 03.01.2014

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

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

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

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

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

person fbp    schedule 17.04.2014

first_page_offset = udata & PAGE_MASK; 

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

first_page_offset = udata & ~PAGE_MASK;

or

first_page_offset = udata & (PAGE_SIZE - 1)
person Suman    schedule 16.02.2016
comment
Это правильно. В ядре linux PAGE_MASK почти повсеместно определяется как (~(PAGE_SIZE-1)), поэтому udata & PAGE_MASK маскирует смещение страницы, а не сохраняет его. - person apriori; 03.06.2017

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

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

Еще одна уловка - ограничить системную память во время загрузки, а затем использовать ее как огромный непрерывный буфер DMA. Это особенно полезно при разработке драйверов и контроллеров прямого доступа к памяти FPGA и не рекомендуется в производственной среде. Допустим, у ПК 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 с нулевой копией, но они полезны во время разработки или на некоторых менее производительных платформах.

person SlawekS    schedule 14.05.2018
comment
Зачем отказываться от дизайна с нулевым копированием? В чем проблема с отображением памяти DMA в пользовательское пространство и использованием ее из пользовательского пространства? - person Alexis; 27.04.2020
comment
Единственная разница между SGDMA и тем, что вы говорите, - это непрерывная физическая память, а не размер всей выделенной памяти. В конце концов, пользовательское пространство увидит такой же объем непрерывной виртуальной памяти, а устройство увидит фрагменты адресов памяти PAGE_SIZE. Я ошибся? - person Alexis; 27.04.2020