драйвер устройства ядра linux для dma с устройства в пользовательскую память

<p>я хочу, чтобы данные из dma с включенным аппаратным устройством pcie в пространство пользователя были как можно быстрее.</p><p>в: как мне объединить «прямой ввод-вывод в пространство пользователя с/и/через dma передачу»</p><ol> <li><p>читая ldd3, мне кажется, что мне нужно выполнить несколько различных типов операций io !? </p> <p><code>dma\_alloc\_coherent</code> дает мне физический адрес, который я могу передать аппаратному устройству . но потребуется завершить настройку <code>get\_user\_pages</code> и выполнить вызов типа <code>copy\_to\_user</code> после завершения передачи. это кажется пустой тратой, когда устройство запрашивает dma в памяти ядра (действует как буфер), а затем снова переносит его в пространство пользователя . ldd3 p453: <code>/* only now is it safe to access the buffer, copy to user, etc. */</code></p></li> <li><p>в идеале я хочу немного памяти, которая:</p> <ul> <li>я могу использовать в пространстве пользователя (может быть, запросить драйвер через вызов ioctl для создания dma'able памяти/буфера?) </li> <li>я могу получить физический адрес для передачи на устройство, чтобы все пользовательское пространство могло выполнять чтение драйвера </li> <li>метод чтения активирует передачу dma, блокирует ожидание полного прерывания dma и освобождает после чтения пространство пользователя (пространство пользователя теперь безопасно для использования/чтения памяти).</li> </ul></li> </ol><p>нужны ли мне одностраничные потоковые сопоставления, установочные сопоставления и буферы пространства пользователя, сопоставленные с <code>get\_user\_pages</code> <code>dma\_map\_page</code>?</p><p>мой код до сих пор устанавливает <code>get\_user\_pages</code> по указанному адресу из пространства пользователя (я называю это частью прямого ввода/вывода). затем <code>dma\_map\_page</code> со страницей из <code>get\_user\_pages</code>. я даю устройству возвращаемое значение из <code>dma\_map\_page</code> в качестве dma физического адреса передачи.</p><p>я использую некоторые модули ядра для справки: <code>drivers\_scsi\_st.c</code> и <code>drivers-net-sh\_eth.c</code>. я бы посмотрел на код infiniband, но не могу найти, какой из них является самым основным!</p><p>спасибо заранее.</p>

<p>на самом деле я сейчас работаю над тем же самым и иду по маршруту <code>ioctl()</code>. общая идея состоит в том, чтобы пользовательское пространство выделяло буфер, который будет использоваться для передачи dma, а <code>ioctl()</code> будет использоваться для передачи размера и адреса этого буфера драйверу устройства. затем драйвер будет использовать списки сбора рассеяния вместе с потоковым api dma для передачи данных непосредственно на устройство и из него и в буфер пространства пользователя.</p><p>стратегия реализации, которую я использую, заключается в том, что <code>ioctl()</code> в драйвере входит в цикл, в котором dma является буфером пространства пользователя в кусках по 256 кб (что является аппаратным ограничением для количества записей разброса/сбора, которые он может обработать). он изолирован внутри функции, которая блокирует до завершения каждой передачи (см. ниже). когда все байты переданы или функция инкрементной передачи возвращает ошибку, функция <code>ioctl()</code> завершается и возвращается в пространство пользователя</p><p>псевдокод для <code>ioctl()</code></p><pre><code>/*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); </code></pre><p>псевдокод для функции инкрементальной передачи:</p><pre><code>/*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(¤t->mm->mmap\_sem); ret = get\_user\_pages(current, current->mm, udata, npages, is\_writing\_to\_userspace, 0, &pages\_array, null); up\_read(¤t->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; } </code></pre><p>обработчик прерываний является исключительно коротким:</p><pre><code>/* 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); </code></pre><p>обратите внимание, что это всего лишь общий подход, я работал над этим драйвером в течение последних нескольких недель, и мне еще предстоит его протестировать ... поэтому, пожалуйста, не рассматривайте этот псевдокод как евангелие и обязательно удвойте проверьте всю логику и параметры ;-).</p>

<p>в принципе, у вас правильная идея: в 2.1 вы можете просто выделить пространство пользователя для любой старой памяти. вы хотите, чтобы он был выровнен по страницам, поэтому <code>posix\_memalign()</code> - удобный api для использования.</p><p>затем пусть userpace передаст виртуальный адрес userpace и размер этого буфера каким-либо образом; ioctl () - хороший быстрый и грязный способ сделать это. в ядре выделите буферный массив соответствующего размера из записей <code>struct page*</code> - <code>user\_buf\_size/page\_size</code> - и используйте <code>get\_user\_pages()</code>, чтобы получить список struct page * для буфера пользовательского пространства.</p><p>когда у вас есть это, вы можете выделить массив <code>struct scatterlist</code> того же размера, что и ваш массив страниц, и просмотреть список страниц, выполняющих функцию <code>sg\_set\_page()</code>. после того, как список sg настроен, вы делаете <code>dma\_map\_sg()</code> в массиве scatterlist, а затем вы можете получить <code>sg\_dma\_address</code> и <code>sg\_dma\_len</code> для каждой записи в scatterlist (обратите внимание, что вы должны использовать возвращаемое значение <code>dma\_map\_sg()</code>, потому что вы можете получить меньше отображенных записи, потому что вещи могут быть объединены кодом отображения dma).</p><p>это дает вам все адреса шины для передачи на ваше устройство, а затем вы можете активировать dma и ждать его, как захотите. схема read (), которая у вас есть, вероятно, в порядке.</p><p>вы можете обратиться к drivers/infiniband/core/umem.c, в частности, к <code>ib\_umem\_get()</code>, для получения некоторого кода, который создает это отображение, хотя общность, с которой должен работать этот код, может сделать его немного запутанным.</p><p>в качестве альтернативы, если ваше устройство не обрабатывает списки разброса/сбора слишком хорошо и вам нужна непрерывная память, вы можете использовать <code>get\_free\_pages()</code> для выделения физически непрерывного буфера и использовать <code>dma\_map\_page()</code> для этого. чтобы предоставить пользователям доступ к этой памяти, вашему драйверу нужно просто реализовать метод <code>mmap</code> вместо ioctl, как описано выше.</p>

<p>в какой-то момент я хотел позволить приложению пользовательского пространства выделять буферы dma и отображать его в пространстве пользователя и получать физический адрес, чтобы иметь возможность управлять моим устройством и выполнять транзакции dma (мастеринг шины ) полностью из пользовательского пространства, полностью минуя ядро linux. я использовал немного другой подход, хотя. сначала я начал с минимального модуля ядра, который инициализировал/проверял устройство pcie и создавал символьное устройство. затем этот драйвер позволил приложению пользовательского пространства сделать две вещи:</p><ol> <li>отобразите панель ввода-вывода устройства pcie в пространство пользователя с помощью функции <code>remap\_pfn\_range()</code>.</li> <li>выделите и освободите dma буферы, сопоставьте их с пользовательским пространством и передайте физический адрес шины приложению пользовательского пространства.</li> </ol><p>по сути, это сводится к пользовательской реализации вызова <code>mmap()</code> (хотя <code>file\_operations</code>). один для панели ввода/вывода прост:</p><pre><code>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; } </code></pre><p>а еще один, который выделяет буферы dma с использованием <code>pci\_alloc\_consistent()</code>, немного сложнее:</p><pre><code>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! */ } </code></pre><p>как только они будут созданы, приложение пользовательского пространства может в значительной степени делать все - управлять устройством, читая/записывая из/в регистры ввода/вывода, выделяя и освобождая dma буферы произвольного размера, и заставляя устройство выполнять dma транзакции. единственная недостающая часть - обработка прерываний. я проводил опрос в пользовательском пространстве, сжигал свой процессор, и прерывания были отключены.</p><p>надеюсь, поможет. удачи!</p>

<blockquote> <p>я запутался в направлении реализации. я бы хотел...</p> </blockquote><p>рассмотрим приложение при разработке драйвера.<br> какова природа перемещения данных, частота, размер и что еще может происходить в системе?</p><p>достаточно ли традиционного api для чтения/записи? является ли прямое сопоставление устройства с пользовательским пространством нормальным? желательна ли отражающая (полукогерентная) разделяемая память?</p><p>ручное манипулирование данными (чтение/запись) является довольно хорошим вариантом, если данные хорошо поддаются пониманию. использование общего назначения vm и чтение/запись может быть достаточным для встроенной копии. прямое сопоставление не кэшируемых доступов к периферийным устройствам удобно, но может быть неуклюжим. если доступ является относительно редким перемещением больших блоков, может иметь смысл использовать обычную память, иметь вывод привода, переводить адреса, dma и освобождать страницы. в качестве оптимизации страницы (возможно огромные) могут быть предварительно закреплены и переведены; затем привод может распознать подготовленную память и избежать сложностей динамического перевода. если имеется много небольших операций ввода-вывода, имеет смысл использовать привод асинхронно. если важна элегантность, флаг грязной страницы vm можно использовать для автоматической идентификации того, что необходимо переместить, и вызов (meta\_sync ()) для сброса страниц. возможно, смесь вышеперечисленных работ ...</p><p>слишком часто люди не смотрят на большую проблему, прежде чем копаться в деталях. часто простейших решений достаточно. небольшое усилие по построению поведенческой модели может помочь определить, какой api предпочтительнее.</p>

<pre><code>first\_page\_offset = udata & page\_mask; </code></pre><p>это кажется неправильным. это должно быть либо:</p><pre><code>first\_page\_offset = udata & ~page\_mask; </code></pre><p>или же </p><pre><code>first\_page\_offset = udata & (page\_size - 1) </code></pre>

<p>стоит отметить, что драйвер с поддержкой scatter-gather dma и распределением памяти в пользовательском пространстве является наиболее эффективным и обладает самой высокой производительностью. однако, если нам не нужна высокая производительность или мы хотим разработать драйвер в некоторых упрощенных условиях, мы можем использовать некоторые приемы.</p><p>откажитесь от нулевого дизайна копии. стоит учитывать, когда пропускная способность не слишком велика. в таком проекте данные могут быть скопированы пользователю <code>copy\_to\_user(user\_buffer, kernel\_dma\_buffer, count);</code> user\_buffer может быть, например, аргументом буфера в реализации системного вызова символьного устройства read (). нам все еще нужно позаботиться о распределении <code>kernel\_dma\_buffer</code>. это может быть память, полученная из вызова <code>dma\_alloc\_coherent()</code>, например.</p><p>еще одна хитрость - ограничить системную память во время загрузки и затем использовать ее в качестве огромного непрерывного буфера dma. это особенно полезно при разработке драйверов и контроллеров fpga dma и довольно не рекомендуется в производственных средах. допустим, пк имеет 32 гб оперативной памяти. если мы добавим <code>mem=20gb</code> в список параметров загрузки ядра, мы сможем использовать 12 гб в качестве огромного непрерывного буфера dma. чтобы отобразить эту память в пространство пользователя, просто реализуйте mmap () как</p><pre><code>remap\_pfn\_range(vma, vma->vm\_start, (0x500000000 >> page\_shift) + vma->vm\_pgoff, vma->vm\_end - vma->vm\_start, vma->vm\_page\_prot) </code></pre><p>конечно, эти 12 гб полностью опущены ос и могут использоваться только процессом, который отобразил их в свое адресное пространство. мы можем попытаться избежать этого, используя непрерывный распределитель памяти (cma).</p><p>снова приведенные выше приемы не заменят полный драйвер scatter-gather, нулевого копирования dma, но полезны во время разработки или на некоторых менее производительных платформах.</p>