TCP-отправка с нулевым копированием в пространстве памяти dma_mmap_coherent ()


14

Я использую Linux 5.1 на платформе Cyclone V SoC, которая представляет собой FPGA с двумя ядрами ARMv7 в одном чипе. Моя цель - собрать много данных с внешнего интерфейса и передать (часть) эти данные через сокет TCP. Проблема в том, что скорость передачи данных очень высока и может приблизиться к насыщению интерфейса GbE. У меня есть рабочая реализация, которая просто использует write()вызовы к сокету, но она достигает 55 МБ / с; примерно половина теоретического предела GbE. Сейчас я пытаюсь заставить работать TCP-передачу с нулевым копированием, чтобы увеличить пропускную способность, но я бью об стену.

Чтобы вывести данные из FPGA в пользовательское пространство Linux, я написал драйвер ядра. Этот драйвер использует блок DMA в FPGA для копирования большого объема данных с внешнего интерфейса в память DDR3, подключенную к ядрам ARMv7. В этом драйвере выделяет память как набор последовательных буферов 1Мб при зондировании использования dma_alloc_coherent()с GFP_USER, и выставляют их в пользовательском приложении, внедряя mmap()на файл в /dev/и возвращая адрес приложению , используя dma_mmap_coherent()на предопределенных буферах.

Все идет нормально; приложение пользовательского пространства видит действительные данные, и пропускная способность более чем достаточна при> 360 МБ / с с запасом места (внешний интерфейс недостаточно быстр, чтобы действительно увидеть верхнюю границу).

Для реализации сетей TCP с нулевым копированием мой первый подход заключался в использовании SO_ZEROCOPYсокета:

sent_bytes = send(fd, buf, len, MSG_ZEROCOPY);
if (sent_bytes < 0) {
    perror("send");
    return -1;
}

Тем не менее, это приводит к send: Bad address.

После некоторого поиска в Google, мой второй подход состоял в том, чтобы использовать трубу и splice()затем vmsplice():

ssize_t sent_bytes;
int pipes[2];
struct iovec iov = {
    .iov_base = buf,
    .iov_len = len
};

pipe(pipes);

sent_bytes = vmsplice(pipes[1], &iov, 1, 0);
if (sent_bytes < 0) {
    perror("vmsplice");
    return -1;
}
sent_bytes = splice(pipes[0], 0, fd, 0, sent_bytes, SPLICE_F_MOVE);
if (sent_bytes < 0) {
    perror("splice");
    return -1;
}

Тем не менее, результат тот же: vmsplice: Bad address.

Обратите внимание, что если я заменю вызов vmsplice()или send()функцию, которая просто печатает данные, на которые указывает buf(или send() без MSG_ZEROCOPY ), все работает просто отлично; Таким образом, данные доступны для пространства пользователя, но кажется, что вызовы vmsplice()/ send(..., MSG_ZEROCOPY)не могут их обработать.

Что мне здесь не хватает? Есть ли способ использовать отправку TCP с нулевой копией с адресом пространства пользователя, полученным из драйвера ядра через dma_mmap_coherent()? Есть ли другой подход, который я мог бы использовать?

ОБНОВИТЬ

Таким образом , я нырнул немного глубже в sendmsg() MSG_ZEROCOPYпуть в ядре, и вызов , который в конечном счете терпит неудачу это get_user_pages_fast(). Этот вызов возвращается, -EFAULTпоскольку check_vma_flags()находит VM_PFNMAPфлаг, установленный в vma. Этот флаг, очевидно, устанавливается, когда страницы отображаются в пространстве пользователя с помощью remap_pfn_range()или dma_mmap_coherent(). Мой следующий подход - найти другой путь к mmapэтим страницам.

Ответы:


8

Как я писал в обновлении в моем вопросе, основная проблема заключается в том, что сеть с zerocopy не работает с памятью, которая была отображена с помощью remap_pfn_range()(которая dma_mmap_coherent()также используется внутри системы ). Причина в том, что этот тип памяти (с установленным VM_PFNMAPфлагом) не имеет метаданных в форме, struct page*связанной с каждой страницей, которая ему нужна.

Решение является то, чтобы выделить память таким образом , что struct page*ей будет ассоциирована с памятью.

Рабочий процесс, который теперь работает для меня, чтобы выделить память:

  1. Используется struct page* page = alloc_pages(GFP_USER, page_order);для выделения блока непрерывной физической памяти, где количество смежных страниц, которые будут выделены, определяется как 2**page_order.
  2. Разделите верхнюю / сложную страницу на страницы 0-го порядка, позвонив split_page(page, page_order);. Теперь это означает, что struct page* pageстал массив с 2**page_orderзаписями.

Теперь для отправки такого региона в DMA (для приема данных):

  1. dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
  2. dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
  3. dmaengine_submit(dma_desc);

Когда мы получаем обратный вызов от прямого доступа к памяти, который закончил передачу, нам нужно удалить карту региона, чтобы передать владение этим блоком памяти обратно в ЦП, который заботится о кешах, чтобы убедиться, что мы не читаем устаревшие данные:

  1. dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);

Теперь, когда мы хотим реализовать mmap(), все, что нам действительно нужно сделать, это vm_insert_page()повторно вызывать все страницы 0-го порядка, которые мы предварительно выделяем:

static int my_mmap(struct file *file, struct vm_area_struct *vma) {
    int res;
...
    for (i = 0; i < 2**page_order; ++i) {
        if ((res = vm_insert_page(vma, vma->vm_start + i*PAGE_SIZE, &page[i])) < 0) {
            break;
        }
    }
    vma->vm_flags |= VM_LOCKED | VM_DONTCOPY | VM_DONTEXPAND | VM_DENYWRITE;
...
    return res;
}

Когда файл закрыт, не забудьте освободить страницы:

for (i = 0; i < 2**page_order; ++i) {
    __free_page(&dev->shm[i].pages[i]);
}

Реализация mmap()этого способа теперь позволяет сокету использовать этот буфер sendmsg()с MSG_ZEROCOPYфлагом.

Хотя это работает, есть две вещи, которые не подходят мне при таком подходе:

  • С помощью этого метода можно выделить только буферы степени 2, хотя можно реализовать логику для вызова alloc_pagesстолько раз, сколько необходимо, с уменьшением порядка, чтобы получить буфер любого размера, состоящий из подбуферов разных размеров. Это потребует некоторой логики, чтобы связать эти буферы вместе в mmap()DMA и DMA с помощью sgвызовов scatter-collect ( ), а не single.
  • split_page() говорит в своей документации:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Эти проблемы были бы легко решены, если бы в ядре был какой-то интерфейс для выделения произвольного количества смежных физических страниц. Я не знаю, почему нет, но я не нахожу вышеупомянутые проблемы настолько важными, чтобы копаться в том, почему это не доступно / как это реализовать :-)


2

Возможно, это поможет вам понять, почему для alloc_pages требуется номер страницы степени 2.

Чтобы оптимизировать процесс выделения страниц (и уменьшить внешнюю фрагментацию), который часто задействован, ядро ​​Linux разработало кэш страниц для каждого процессора и buddy-allocator для выделения памяти (есть еще один распределитель, slab, для обслуживания выделений памяти, которые меньше, чем страница).

Страничный кеш-процессор обслуживает одностраничный запрос на выделение, а buddy-allocator хранит 11 списков, каждый из которых содержит 2 ^ {0-10} физических страниц соответственно. Эти списки хорошо работают при выделении и освобождении страниц, и, конечно, предпосылка заключается в том, что вы запрашиваете буфер степени 2.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.