TCP-отправка с нулевым копированием в пространстве памяти dma_mmap_coherent () - PullRequest
14 голосов
/ 30 октября 2019

Я использую 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 этим страницам.

Ответы [ 2 ]

8 голосов
/ 04 ноября 2019

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

Тогда решение заключается в выделениипамяти таким образом, что struct page* s связаны с памятью.

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

  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.

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

  • Вы можете выделить только power-of-2-sizс помощью этого метода можно использовать буферы ed, хотя можно реализовать логику для вызова alloc_pages столько раз, сколько необходимо, с уменьшением порядка, чтобы получить буфер любого размера, состоящий из подбуферов разных размеров. Это потребует некоторой логики для связывания этих буферов в mmap() и их прямого доступа к памяти с помощью вызовов scatter-collect (sg), а не 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 голосов
/ 05 ноября 2019

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

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

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

...